Qrona Newsletter API · version 1
API for developers
Connect your web shop, your CRM or your own systems to Qrona Newsletter. With the API you read and update contacts, sign people up to lists with proof of their consent, send in the shop's purchases and keep other systems in step with unsubscriptions.
Get started
- Create a key. An administrator in the workspace goes to Settings → API keys, gives the key a name and chooses what it may do. The key is shown once – store it where it will be used, for example in
wp-config.php. - Make your first call. It tells you which workspace the key applies to and what it is allowed to do.
curl https://newsletter.qrona.se/api/v1 \
-H "Authorization: Bearer $QRONA_NEWSLETTER_KEY"- Create or update a contact with POST /contacts/upsert, and sign up to a list with POST /lists/{id}/members.
curl -X POST https://newsletter.qrona.se/api/v1/contacts/upsert \
-H "Authorization: Bearer $QRONA_NEWSLETTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "anna@example.se",
"first_name": "Anna",
"add_tags": ["kund"],
"consent_source": "Ticked the box at checkout on butik.se"
}'Keys and scopes
Send the key with every request: Authorization: Bearer qn_live_…. Keys belong on a server, never in a browser or app – the API does not respond to requests from other websites (no CORS). If a key has leaked: revoke it under API keys, and it stops working immediately.
A key applies to one workspace and acts as the person who created it. It never does more than that person is allowed to do right now: if the person leaves the workspace, the key stops working, and if the person has contact rules, the key only sees those contacts. Everything done with a key appears in the workspace's audit log, labelled with the key's name.
| Scope | Grants | Person's role |
|---|---|---|
contacts:read | Contacts, their lists and the suppression list. | all |
contacts:write | Add and change contacts, tags and list memberships, unsubscribe. | editor+ |
contacts:erase | Erase a contact's personal data for good. The address is suppressed. | editor+ |
lists:read | Lists and their members. | all |
lists:write | Create, edit and delete lists. | editor+ |
fields:read | Which custom fields the contacts have. | all |
segments:read | Distribution lists (segments) with counts. Requires the person to be an administrator. | administrator |
ecommerce:read | Stores, customers, products, orders, carts and revenue per campaign. | all |
ecommerce:write | Submit and delete stores, customers, products, orders and carts. | editor+ |
webhooks:read | The workspace's webhooks and their deliveries. Requires the person to be an administrator. | administrator |
webhooks:write | Create, edit, test and delete webhooks. Requires the person to be an administrator. | administrator |
Only Qrona's system key for the add-on Byråstöd: Klientkommunikation has messages:send · templates:render; it belongs to the organisation, not to a person. Read more in Messages from Qrona.
Format
- Base URL:
https://newsletter.qrona.se/api/v1. Everything is JSON in UTF-8; send bodies withContent-Type: application/json. - Field names are
snake_case, IDs are strings and times are ISO 8601 in UTC (2026-10-19T08:30:00.000Z). - Updates (PATCH, upsert and the e-commerce PUT) only touch the fields you send – lists such as order lines and variants are replaced when sent.
nullclears a field. Unknown fields in a body give 422, so that typos are noticed. - Email addresses are sent in the body, not in the URL, so that they do not end up in logs – that is why POST /contacts/lookup exists.
Pages (pagination)
List responses come one page at a time: limit sets how many, and the response has data, has_more and next_cursor. Fetch the next page with cursor=next_cursor until has_more is false. The pages stay stable even while contacts are being added. For ongoing sync: save the time before you start, then fetch only what has changed with updated_since.
let cursor = null;
do {
const url = new URL('https://newsletter.qrona.se/api/v1/contacts');
url.searchParams.set('limit', '500');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await fetch(url, { headers: { Authorization: `Bearer ${key}` } }).then((r) => r.json());
for (const contact of page.data) await save(contact);
cursor = page.next_cursor;
} while (cursor);Errors
Errors respond with an HTTP status and application/problem+json: code is for programs, title and detail are for humans, and errors says what is wrong per field. All codes are listed under Error codes.
Limits
- At most 600 calls per minute per key. Every response has
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(seconds); over the limit, the response is 429 withRetry-After. - Bodies at most 64 kB (products, orders and carts 256 kB, batch 5 MB). Up to 500 per page and 100 addresses per lookup.
- Retry a call that got 429 or 5xx after a while – preferably with increasing wait times. Upsert, PUT and sign-ups can be repeated without anything being duplicated; for other POST calls, send Idempotency-Key.
Consent and GDPR
You are the controller of your contacts' personal data, and Qrona Newsletter keeps the proof of every consent. Therefore:
- Only create contacts who have consented to mailings, or where you have another legal basis.
consent_sourcedescribes how – in your own words, for example “Ticked the box at checkout on butik.se”. - When signing someone up to a list, you send the text the person agreed to, where it happened and the person's IP address. On lists with double opt-in, the person receives a confirmation email.
- Anyone who has unsubscribed never comes back without confirming by email themselves, and suppressed addresses – bounces, spam complaints, erased – cannot be signed up at all.
- Unsubscriptions: let a webhook tell you when they happen, or fetch the suppression list regularly. Unsubscribe with POST /contacts/{id}/unsubscribe when the customer says no in your shop.
- Erasure: when a customer asks to be erased, call POST /contacts/{id}/erase (requires the
contacts:erasescope). Sensitive fields, such as personal identity numbers, can be neither read nor written via the API. - E-commerce: the store's customers never become contacts, whether or not they have consented. Sign up those who have consented to a list as usual. Read more in the e-commerce guide.
Example: web shop
This is how a WooCommerce shop can add customers who tick a newsletter box at checkout. The key and the list's ID are in wp-config.php. Run the calls in the background (for example with Action Scheduler) so that checkout never waits for us. The orders themselves – for purchase conditions and revenue per campaign – are sent as in the e-commerce guide.
<?php
// When an order is placed in WooCommerce and the customer has ticked the newsletter box.
add_action('woocommerce_checkout_order_processed', function ($order_id) {
$order = wc_get_order($order_id);
if ($order->get_meta('_newsletter_optin') !== 'yes') {
return; // No consent: the customer is not added.
}
$request = fn ($method, $path, $body) => wp_remote_request('https://newsletter.qrona.se/api/v1' . $path, [
'method' => $method,
'timeout' => 15,
'headers' => [
'Authorization' => 'Bearer ' . QRONA_NEWSLETTER_KEY,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode($body),
]);
// 1. Sign-up to the list, with the proof of consent.
$request('POST', '/lists/' . QRONA_NEWSLETTER_LIST . '/members', [
'email' => $order->get_billing_email(),
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'consent' => [
'text' => 'Yes, I would like to receive newsletters from Exempel AB.',
'source' => 'Checkout at ' . wp_parse_url(home_url(), PHP_URL_HOST),
'ip' => $order->get_customer_ip_address(),
'user_agent' => $order->get_customer_user_agent(),
],
]);
// 2. Custom fields and tags on the contact.
$request('POST', '/contacts/upsert', [
'email' => $order->get_billing_email(),
'fields' => ['kundnummer' => (string) $order->get_customer_id()],
'add_tags' => ['kund'],
]);
});Versions
This is version 1. We add new fields, endpoints and error codes without notice – let your program ignore fields it does not recognise. Changes that could make existing integrations stop working only come in a new version, with plenty of notice.
- 2026-10-19
- Version 1: keys, contacts, lists and sign-ups, contact fields, distribution lists and the suppression list.
- 2026-10-19
- E-commerce (stores, customers, products, orders, carts, revenue per campaign), POST /batch, Idempotency-Key and webhooks.
Questions? Write to support@qrona.se.