Hoppa till innehållet

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ändelseNär
contact.subscribedNå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.unsubscribedNå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.bouncedAdressen studsar (finns inte, eller tar inte emot e-post). Den spärras i hela organisationen.
contact.complainedMottagaren 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).

Headers
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
Kropp
{
  "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 id ni redan har sett.
  • Händelserna kommer inte alltid i ordning. Använd created_at om 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 ping som skickas direkt. Leveranserna de senaste 30 dagarna finns i GET /webhooks/{id}/deliveries.