Qrona Newsletter API · guide
Webhooks
Låt er webbutik, ert CRM eller andra system få veta direkt när någon anmäler sig, avregistrerar sig, studsar eller anmäler ett utskick som skräppost – i stället för att fråga oss om och om igen.
Händelser
| Händelse | När |
|---|---|
contact.subscribed | Någon kom med på en lista: anmälde sig i ett formulär, bekräftade sin anmälan, lades till i appen eller via API:t. Väntande anmälningar (dubbel opt-in) räknas när de bekräftas. |
contact.unsubscribed | Någon avregistrerade sig – från en lista (list_id) eller från allt (list_id är null) – via länken i ett utskick, i appen eller via API:t. |
contact.bounced | Adressen studsar (finns inte, eller tar inte emot e-post). Den spärras i hela organisationen. |
contact.complained | Mottagaren markerade ett utskick som skräppost. Adressen spärras i hela organisationen. |
Ändringar som görs via API:t skickas bara till webhooks med include_api: true, så att en integration inte får höra om sina egna ändringar. Importer från fil skickar inga händelser.
Meddelandena som Qrona skickar genom tillägget Byråstöd: Klientkommunikation har egna kvitton (message.*), signerade på samma sätt, som bara går till Qrona: se Meddelanden från Qrona.
Skapa en webhook
En administratör skapar webhooken under Inställningar → API-nycklar (avsnittet Webhooks), eller via API:t med en nyckel som har webhooks:write: POST /webhooks. Svaret har hemligheten (whsec_…) som leveranserna signeras med – den visas bara då. En arbetsyta kan ha 10 webhooks. Adressen måste vara https, på standardporten och nåbar från internet.
curl -X POST https://newsletter.qrona.se/api/v1/webhooks \
-H "Authorization: Bearer $QRONA_NEWSLETTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://butik.se/wp-json/qrona/v1/webhook",
"events": ["contact.unsubscribed", "contact.bounced", "contact.complained"],
"description": "Avregistreringar till WooCommerce"
}'Så ser en leverans ut
En POST med JSON. id är händelsens id – samma i alla webhooks som får den – och data.list_id är listan det gällde, eller null när det gäller adressen (avregistrerad från allt, studs, skräppostanmälan).
POST /wp-json/qrona/v1/webhook HTTP/1.1
Content-Type: application/json
Qrona-Signature: t=1760862600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Qrona-Event: contact.unsubscribed
Qrona-Delivery: 9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d{
"id": "7f3c1a2e-5b4d-4e6f-8a9b-0c1d2e3f4a5b",
"type": "contact.unsubscribed",
"created_at": "2026-10-19T08:30:00.000Z",
"workspace_id": "0b8f6c2a-1d3e-4f5a-9b7c-8d6e5f4a3b2c",
"data": {
"email": "anna@example.se",
"contact_id": "1042",
"list_id": null,
"source": "one-click",
"via_api": false
}
}Kontrollera signaturen
Qrona-Signature är t=<unix-tid>,v1=<signatur>, där signaturen är HMAC-SHA256 i hex av <t>.<kroppen> med webhookens hemlighet. Räkna fram samma sak över den råa kroppen, jämför i konstant tid och neka en t som är mer än 5 minuter gammal, så kan ingen förfalska eller spela upp en leverans.
<?php
// WordPress: en egen REST-väg som tar emot händelserna. Hemligheten (whsec_…) i wp-config.php.
add_action('rest_api_init', function () {
register_rest_route('qrona/v1', '/webhook', [
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => function (WP_REST_Request $request) {
$body = $request->get_body(); // den råa kroppen – inte omkodad JSON
parse_str(str_replace(',', '&', (string) $request->get_header('qrona-signature')), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, QRONA_WEBHOOK_SECRET);
if (empty($sig['v1']) || !hash_equals($expected, $sig['v1']) || abs(time() - (int) $sig['t']) > 300) {
return new WP_REST_Response(['error' => 'signature'], 401);
}
$event = json_decode($body, true);
// Samma händelse kan komma två gånger: hoppa över ett id ni redan har sett.
if (get_transient('qrona_event_' . $event['id'])) {
return new WP_REST_Response(null, 204);
}
set_transient('qrona_event_' . $event['id'], 1, WEEK_IN_SECONDS);
if (in_array($event['type'], ['contact.unsubscribed', 'contact.bounced', 'contact.complained'], true)) {
$user = get_user_by('email', $event['data']['email']);
if ($user) update_user_meta($user->ID, 'newsletter_optin', 'no');
}
return new WP_REST_Response(null, 204);
},
]);
});Svara – och omförsök
- Svara med en 2xx inom 10 sekunder. Gör det tunga arbetet efteråt, i en kö. Omdirigeringar följs inte.
- Allt annat (fel, timeout, 3xx) görs om efter 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 1 dygn, 1 dygn – 9 försök på ungefär tre dygn – och ges sedan upp. Samma händelse kan därför komma mer än en gång: hoppa över ett
idni redan har sett. - Händelserna kommer inte alltid i ordning. Använd
created_atom ordningen spelar roll. - En webhook vars leveranser har misslyckats 100 gånger i rad stängs av. Slå på den igen i inställningarna eller med PATCH /webhooks/{id} och
{"active": true}. - Testa adressen med Skicka test i inställningarna eller POST /webhooks/{id}/test: en
pingsom skickas direkt. Leveranserna de senaste 30 dagarna finns i GET /webhooks/{id}/deliveries.