Qrona Newsletter API · guide
E-handel
Koppla en webbutik: skicka in kunder, produkter, ordrar och kundkorgar, så kan ni rikta utskick efter vad folk har köpt och se hur mycket varje kampanj sålde för.
Modellen
- Allt hör till en butik som ni namnger själva, till exempel
butik-se. En arbetsyta kan ha upp till 20 butiker. Skapa den med PUT /stores/{store_id}. - Under butiken finns kunder, produkter med varianter, ordrar med rader (högst 500) och kundkorgar. Alla har butikens egna id:n – WooCommerce order 50087 blir
/stores/butik-se/orders/50087. - Allt skrivs med
PUT, som skapar eller uppdaterar. Samma anrop två gånger ger samma resultat, så en import kan köras om och en order skickas igen varje gång den ändras. Fält ni utelämnar behålls; listor (varianter, rader) ersätts när de skickas. - En order eller korg skickas med sin kund, som skapas eller uppdateras samtidigt. Produkterna behöver inte ha skickats in för att en order ska kunna nämna dem.
- Behörigheterna är
ecommerce:readochecommerce:write. Personen bakom nyckeln får inte ha kontaktvillkor: kunderna är inte kontakter och kan inte begränsas på samma sätt.
curl -X PUT https://newsletter.qrona.se/api/v1/stores/butik-se \
-H "Authorization: Bearer $QRONA_NEWSLETTER_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Exempelbutiken", "domain": "butik.se", "currency": "SEK" }'Kunder och samtycke
En kund är den som har handlat – med eller utan samtycke till nyhetsbrev. Därför hålls kunderna helt åtskilda från kontakterna:
- En kund blir aldrig en kontakt och får aldrig ett utskick för att hen har handlat.
- Har kunden kryssat i rutan för nyhetsbrev: anmäl hen till en lista med POST /lists/{id}/members, med texten hen godkände – precis som utan e-handel.
- En kund kopplas till kontakten med samma e-postadress i arbetsytan så länge en sådan kontakt finns (
contact_idpå kunden). Det är bara då köpen syns i distributionslistornas villkor. - När en kund vill bli raderad i butiken: DELETE /stores/{store_id}/customers/{customer_id}. Raderas en kontakt i Qrona Newsletter (GDPR) tas kunder med samma adress bort på samma sätt. Ordrarna finns kvar utan kund, så att kampanjernas intäkt stämmer.
Intäkt per kampanj
Ange butikens domain. Då får länkar till butiken (och dess underdomäner) i arbetsytans utskick parametern qn_cid=<kampanjens id>, som klickspårningen sedan bär med sig. Spara värdet när kunden kommer till butiken – i en kaka eller i sessionen – och skicka det som campaign_id på ordern.
- Bara betalda ordrar (
financial_status: "paid") räknas. Återbetald eller avbruten: skicka ordern igen med den statusen. - Ett
campaign_idsom inte är en av arbetsytans kampanjer sparas inte – men ordern sparas. - Intäkten syns på kampanjens sida i appen och i GET /revenue, per valuta.
Köp i distributionslistor
När en arbetsyta har en butik får distributionslistornas villkor en grupp till, Köp i webbutiken:
- Köp – har köpt något, eller har aldrig köpt något.
- Köpt produkt – har köpt (eller inte köpt) en viss produkt, eller någon av flera.
- Totalt köpbelopp – summan av alla betalda ordrar är mer eller mindre än ett belopp. ”Mindre än” räknar även den som aldrig har köpt.
- Senaste köp – inom de senaste N dagarna, eller för mer än N dagar sedan (den som har köpt men inte på länge).
Villkoren gäller kontakter, och bara betalda ordrar räknas. Kombinera dem med de andra villkoren, till exempel en lista eller en tagg.
Exempel: WooCommerce
Tre krokar räcker: spara qn_cid när kunden kommer, lägg det på ordern i kassan, och skicka ordern när den skapas eller byter status. Kör anropen i bakgrunden (till exempel med Action Scheduler) så att kassan aldrig väntar på oss.
<?php
// wp-config.php: define('QRONA_NEWSLETTER_KEY', 'qn_live_…'); define('QRONA_STORE_ID', 'butik-se');
const QRONA_API = 'https://newsletter.qrona.se/api/v1';
const QN_CID = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/';
// 1. Kunden kommer från ett utskick: spara qn_cid i 30 dagar.
add_action('init', function () {
$cid = isset($_GET['qn_cid']) ? sanitize_text_field(wp_unslash($_GET['qn_cid'])) : '';
if (preg_match(QN_CID, $cid)) {
setcookie('qn_cid', $cid, time() + 30 * DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
}
});
// 2. I kassan: kampanjen följer med ordern.
add_action('woocommerce_checkout_create_order', function ($order) {
$cid = isset($_COOKIE['qn_cid']) ? sanitize_text_field(wp_unslash($_COOKIE['qn_cid'])) : '';
if (preg_match(QN_CID, $cid)) {
$order->update_meta_data('_qn_cid', $cid);
}
});
// 3. När ordern skapas eller byter status: skicka den hel, med kunden.
add_action('woocommerce_order_status_changed', function ($order_id) {
$order = wc_get_order($order_id);
$status = [
'processing' => 'paid', 'completed' => 'paid',
'refunded' => 'refunded', 'cancelled' => 'cancelled', 'failed' => 'cancelled',
][$order->get_status()] ?? 'pending';
$lines = [];
foreach ($order->get_items() as $item_id => $item) {
$lines[] = [
'id' => (string) $item_id,
'product_id' => (string) $item->get_product_id(),
'variant_id' => $item->get_variation_id() ? (string) $item->get_variation_id() : null,
'title' => $item->get_name(),
'quantity' => $item->get_quantity(),
'price' => round($item->get_total() / max(1, $item->get_quantity()), 2),
];
}
$email = $order->get_billing_email();
$body = [
'customer' => [
// Gäster har inget kund-id i WooCommerce: adressens hash blir deras id.
'id' => $order->get_customer_id() ? (string) $order->get_customer_id() : 'guest-' . md5(strtolower($email)),
'email' => $email,
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
],
'financial_status' => $status,
'currency' => $order->get_currency(),
'order_total' => round((float) $order->get_total(), 2),
'tax_total' => round((float) $order->get_total_tax(), 2),
'shipping_total' => round((float) $order->get_shipping_total(), 2),
'processed_at' => $order->get_date_created()->format(DATE_ATOM),
'lines' => $lines,
];
if ($order->get_meta('_qn_cid')) {
$body['campaign_id'] = $order->get_meta('_qn_cid');
}
wp_remote_request(QRONA_API . '/stores/' . QRONA_STORE_ID . '/orders/' . $order->get_id(), [
'method' => 'PUT',
'timeout' => 15,
'headers' => ['Authorization' => 'Bearer ' . QRONA_NEWSLETTER_KEY, 'Content-Type' => 'application/json'],
'body' => wp_json_encode($body),
]);
});Läsa in gamla ordrar
Med POST /batch går 500 anrop i ett. Börja med produkterna (om ni vill kunna välja dem i villkor) och ta sedan ordrarna med processed_at satt till när de lades. Läs mer under Batch och idempotens.
import { randomUUID } from 'node:crypto';
// Gamla ordrar, 500 i taget. PUT gör att en avbruten import kan köras om från
// början utan dubbletter; Idempotency-Key gör att en batch som skickas om
// (efter ett nätverksfel) får samma svar utan att köras igen.
for (let i = 0; i < orders.length; i += 500) {
const operations = orders.slice(i, i + 500).map((o) => ({
method: 'PUT',
path: `/stores/butik-se/orders/${encodeURIComponent(o.id)}`,
body: toQronaOrder(o),
}));
const idempotencyKey = randomUUID();
let answer;
for (let attempt = 1; ; attempt++) {
const response = await fetch('https://newsletter.qrona.se/api/v1/batch', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.QRONA_NEWSLETTER_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({ operations }),
});
if (response.ok) { answer = await response.json(); break; }
if (attempt === 5 || (response.status !== 429 && response.status < 500)) throw new Error(`Batchen svarade ${response.status}`);
await new Promise((r) => setTimeout(r, Number(response.headers.get('retry-after') ?? 2 ** attempt) * 1000));
}
for (const [n, result] of answer.results.entries()) {
if (result.status >= 400) console.warn(operations[n].path, result.body.code, result.body.errors);
}
}