Hoppa till innehållet

Guider

Webhooks

Med webhooks slipper ditt program fråga efter ändringar: NONOX skickar en POST till din server när något händer. De skickas sedan version 1.1.0 av API:t.

Så fungerar de

  • En ägare eller administratör lägger till webhooken under Anslutningar i NONOX och bekräftar med passkey: en https-adress och vilka händelser den ska få. Ägaren får ett mejl om varje ny webhook.
  • Adressen kontrolleras när den sparas och före varje försök: privata nät, loopback, link-local och andra interna adresser avvisas, anslutningen låses till den kontrollerade adressen och omdirigeringar följs aldrig.
  • Varje webhook får en egen hemlighet (nnxwh_…) som visas en gång. Den används för att signera varje leverans och kan bytas.
  • Händelsen skickas först när bokföringen är sparad. Svara med 2xx inom 10 sekunder (5 för att ansluta). Allt annat försöks igen efter 1, 5 och 15 minuter och 1, 3, 6 och 13 timmar. Misslyckas även det åttonde försöket stängs webhooken av och ägaren får ett mejl.
  • Samma händelse kan komma mer än en gång. Nonox-Event-Id är samma vid varje nytt försök; spara de id:n du har behandlat och hoppa över dubbletter. Nonox-Event-Type säger vilken händelse det är.
  • Innehållet är kort: id, nummer, datum och belopp, aldrig personuppgifter. Hämta det fullständiga objektet från API:t om du behöver mer.
  • Vill du prova utan riktig bokföring? Skapa ett testföretag under Anslutningar.

Händelserna

HändelseBetyderStatus
voucher.posted En verifikation har bokförts –
invoice.sent En kundfaktura har skickats –
invoice.paid En kundfaktura har betalats –
supplier_invoice.approved En leverantörsfaktura har attesterats –

En verifikation har bokförts

POST voucher.posted voucherPosted

Skickas med HTTPS POST till varje webhook som prenumererar på `voucher.posted`, efter att ändringen har sparats. Kontrollera Nonox-Signature, avvisa tidsstämplar äldre än fem minuter och använd Nonox-Event-Id mot dubbletter. Svara 2xx inom 10 sekunder; annars försöker NONOX igen i ungefär ett dygn och stänger sedan av webhooken.

Mer i detalj (på engelska)

Sent after a voucher is posted (manually, through the API, or by issuing an invoice, registering a payment and so on), once the transaction has committed, as an HTTPS POST to every active webhook of the company that subscribes to `voucher.posted`. Verify `Nonox-Signature` over the raw body before trusting it, reject a timestamp more than five minutes from your clock, and store `Nonox-Event-Id` to ignore duplicates (a delivery whose answer was lost is sent again). Answer 2xx within 10 seconds. Anything else is retried after 1, 5 and 15 minutes and 1, 3, 6 and 13 hours (eight attempts over about 23 hours); then the webhook is turned off and the company's owner is e-mailed. Redirects are never followed.

Parametrar

  • Nonox-Signature string huvud krävs

    `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the subscription secret>`. Compare in constant time.

    Exempel: t=1772444713,v1=5f8e2a1c9b7d3e6f0a4c8b2d1e9f7a3c5b0d8e6f2a4c1b9d7e3f5a0c8b6d2e4f

  • Nonox-Event-Id string (uuid) huvud krävs

    Unique per event; the same on every retry.

  • Nonox-Event-Type string huvud krävs

    The event type, as `type` in the body.

    Exempel: voucher.posted

  • User-Agent string huvud krävs

    Exempel: NONOX-Webhooks/1.0 (+https://github.com/STAFE-GROUP-AB/nonox)

Innehåll i anropet

application/jsonWebhookEvent krävs

A webhook delivery body. Events are thin: ids, references, dates and amounts, never personal data; fetch the record through the API for more.

  • id string (uuid) krävs

    Same as the Nonox-Event-Id header; use it to ignore duplicates.

  • type string krävs

    Open enum: new event types may be added within v1.

    Värden: voucher.posted, invoice.sent, invoice.paid, supplier_invoice.approved

  • created_at string (date-time) krävs
  • company_id integer krävs
  • data object krävs

Svar

2XX Received. Anything else, or no answer within 10 seconds (5 to connect), is retried.

Innehållet som NONOX skickar

HTTP
POST /din/webhook HTTP/1.1
Content-Type: application/json
Nonox-Signature: t=1772444713,v1=5f8e2a1c…
Nonox-Event-Id: c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f
JSON
{
  "id": "c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f",
  "type": "voucher.posted",
  "created_at": "2026-03-02T10:45:13+01:00",
  "company_id": 42,
  "data": {
    "voucher_id": "0b9f3c1e-6a2d-4f7b-9c8e-1d2a3b4c5d6e",
    "series": "A",
    "number": 57,
    "reference": "A57",
    "voucher_date": "2026-03-02",
    "fiscal_year_id": 7,
    "source": "MANUAL",
    "amount_ore": 125000,
    "corrects_voucher_id": null
  }
}

En kundfaktura har skickats

POST invoice.sent invoiceSent

Skickas med HTTPS POST till varje webhook som prenumererar på `invoice.sent`, efter att ändringen har sparats. Kontrollera Nonox-Signature, avvisa tidsstämplar äldre än fem minuter och använd Nonox-Event-Id mot dubbletter. Svara 2xx inom 10 sekunder; annars försöker NONOX igen i ungefär ett dygn och stänger sedan av webhooken.

Mer i detalj (på engelska)

Sent after an issued customer invoice is e-mailed to the customer, as an HTTPS POST to every active webhook of the company that subscribes to `invoice.sent`. Verify `Nonox-Signature` over the raw body before trusting it, reject a timestamp more than five minutes from your clock, and store `Nonox-Event-Id` to ignore duplicates (a delivery whose answer was lost is sent again). Answer 2xx within 10 seconds. Anything else is retried after 1, 5 and 15 minutes and 1, 3, 6 and 13 hours (eight attempts over about 23 hours); then the webhook is turned off and the company's owner is e-mailed. Redirects are never followed.

Parametrar

  • Nonox-Signature string huvud krävs

    `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the subscription secret>`. Compare in constant time.

    Exempel: t=1772444713,v1=5f8e2a1c9b7d3e6f0a4c8b2d1e9f7a3c5b0d8e6f2a4c1b9d7e3f5a0c8b6d2e4f

  • Nonox-Event-Id string (uuid) huvud krävs

    Unique per event; the same on every retry.

  • Nonox-Event-Type string huvud krävs

    The event type, as `type` in the body.

    Exempel: invoice.sent

  • User-Agent string huvud krävs

    Exempel: NONOX-Webhooks/1.0 (+https://github.com/STAFE-GROUP-AB/nonox)

Innehåll i anropet

application/jsonWebhookEvent krävs

A webhook delivery body. Events are thin: ids, references, dates and amounts, never personal data; fetch the record through the API for more.

  • id string (uuid) krävs

    Same as the Nonox-Event-Id header; use it to ignore duplicates.

  • type string krävs

    Open enum: new event types may be added within v1.

    Värden: voucher.posted, invoice.sent, invoice.paid, supplier_invoice.approved

  • created_at string (date-time) krävs
  • company_id integer krävs
  • data object krävs

Svar

2XX Received. Anything else, or no answer within 10 seconds (5 to connect), is retried.

Innehållet som NONOX skickar

HTTP
POST /din/webhook HTTP/1.1
Content-Type: application/json
Nonox-Signature: t=1772444713,v1=5f8e2a1c…
Nonox-Event-Id: c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f
JSON
{
  "id": "c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f",
  "type": "invoice.sent",
  "created_at": "2026-03-02T10:45:13+01:00",
  "company_id": 42,
  "data": {
    "invoice_id": "5c2e8a41-7b3d-4f19-a6c2-0e9d8b7a6f54",
    "invoice_number": 1042,
    "kind": "INVOICE",
    "customer_id": 18,
    "total_ore": 237500,
    "currency": "SEK",
    "invoice_date": "2026-03-02",
    "due_date": "2026-04-01",
    "ocr": "104257"
  }
}

En kundfaktura har betalats

POST invoice.paid invoicePaid

Skickas med HTTPS POST till varje webhook som prenumererar på `invoice.paid`, efter att ändringen har sparats. Kontrollera Nonox-Signature, avvisa tidsstämplar äldre än fem minuter och använd Nonox-Event-Id mot dubbletter. Svara 2xx inom 10 sekunder; annars försöker NONOX igen i ungefär ett dygn och stänger sedan av webhooken.

Mer i detalj (på engelska)

Sent after a customer invoice becomes paid in full (a registered payment or a matched bank file), as an HTTPS POST to every active webhook of the company that subscribes to `invoice.paid`. Verify `Nonox-Signature` over the raw body before trusting it, reject a timestamp more than five minutes from your clock, and store `Nonox-Event-Id` to ignore duplicates (a delivery whose answer was lost is sent again). Answer 2xx within 10 seconds. Anything else is retried after 1, 5 and 15 minutes and 1, 3, 6 and 13 hours (eight attempts over about 23 hours); then the webhook is turned off and the company's owner is e-mailed. Redirects are never followed.

Parametrar

  • Nonox-Signature string huvud krävs

    `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the subscription secret>`. Compare in constant time.

    Exempel: t=1772444713,v1=5f8e2a1c9b7d3e6f0a4c8b2d1e9f7a3c5b0d8e6f2a4c1b9d7e3f5a0c8b6d2e4f

  • Nonox-Event-Id string (uuid) huvud krävs

    Unique per event; the same on every retry.

  • Nonox-Event-Type string huvud krävs

    The event type, as `type` in the body.

    Exempel: invoice.paid

  • User-Agent string huvud krävs

    Exempel: NONOX-Webhooks/1.0 (+https://github.com/STAFE-GROUP-AB/nonox)

Innehåll i anropet

application/jsonWebhookEvent krävs

A webhook delivery body. Events are thin: ids, references, dates and amounts, never personal data; fetch the record through the API for more.

  • id string (uuid) krävs

    Same as the Nonox-Event-Id header; use it to ignore duplicates.

  • type string krävs

    Open enum: new event types may be added within v1.

    Värden: voucher.posted, invoice.sent, invoice.paid, supplier_invoice.approved

  • created_at string (date-time) krävs
  • company_id integer krävs
  • data object krävs

Svar

2XX Received. Anything else, or no answer within 10 seconds (5 to connect), is retried.

Innehållet som NONOX skickar

HTTP
POST /din/webhook HTTP/1.1
Content-Type: application/json
Nonox-Signature: t=1772444713,v1=5f8e2a1c…
Nonox-Event-Id: c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f
JSON
{
  "id": "c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f",
  "type": "invoice.paid",
  "created_at": "2026-03-02T10:45:13+01:00",
  "company_id": 42,
  "data": {
    "invoice_id": "5c2e8a41-7b3d-4f19-a6c2-0e9d8b7a6f54",
    "invoice_number": 1042,
    "customer_id": 18,
    "total_ore": 237500,
    "paid_ore": 237500,
    "currency": "SEK",
    "payment_id": 88,
    "paid_on": "2026-03-28"
  }
}

En leverantörsfaktura har attesterats

POST supplier_invoice.approved supplierInvoiceApproved

Skickas med HTTPS POST till varje webhook som prenumererar på `supplier_invoice.approved`, efter att ändringen har sparats. Kontrollera Nonox-Signature, avvisa tidsstämplar äldre än fem minuter och använd Nonox-Event-Id mot dubbletter. Svara 2xx inom 10 sekunder; annars försöker NONOX igen i ungefär ett dygn och stänger sedan av webhooken.

Mer i detalj (på engelska)

Sent after a supplier invoice is approved (attesterad), as an HTTPS POST to every active webhook of the company that subscribes to `supplier_invoice.approved`. Verify `Nonox-Signature` over the raw body before trusting it, reject a timestamp more than five minutes from your clock, and store `Nonox-Event-Id` to ignore duplicates (a delivery whose answer was lost is sent again). Answer 2xx within 10 seconds. Anything else is retried after 1, 5 and 15 minutes and 1, 3, 6 and 13 hours (eight attempts over about 23 hours); then the webhook is turned off and the company's owner is e-mailed. Redirects are never followed.

Parametrar

  • Nonox-Signature string huvud krävs

    `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the subscription secret>`. Compare in constant time.

    Exempel: t=1772444713,v1=5f8e2a1c9b7d3e6f0a4c8b2d1e9f7a3c5b0d8e6f2a4c1b9d7e3f5a0c8b6d2e4f

  • Nonox-Event-Id string (uuid) huvud krävs

    Unique per event; the same on every retry.

  • Nonox-Event-Type string huvud krävs

    The event type, as `type` in the body.

    Exempel: supplier_invoice.approved

  • User-Agent string huvud krävs

    Exempel: NONOX-Webhooks/1.0 (+https://github.com/STAFE-GROUP-AB/nonox)

Innehåll i anropet

application/jsonWebhookEvent krävs

A webhook delivery body. Events are thin: ids, references, dates and amounts, never personal data; fetch the record through the API for more.

  • id string (uuid) krävs

    Same as the Nonox-Event-Id header; use it to ignore duplicates.

  • type string krävs

    Open enum: new event types may be added within v1.

    Värden: voucher.posted, invoice.sent, invoice.paid, supplier_invoice.approved

  • created_at string (date-time) krävs
  • company_id integer krävs
  • data object krävs

Svar

2XX Received. Anything else, or no answer within 10 seconds (5 to connect), is retried.

Innehållet som NONOX skickar

HTTP
POST /din/webhook HTTP/1.1
Content-Type: application/json
Nonox-Signature: t=1772444713,v1=5f8e2a1c…
Nonox-Event-Id: c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f
JSON
{
  "id": "c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f",
  "type": "supplier_invoice.approved",
  "created_at": "2026-03-02T10:45:13+01:00",
  "company_id": 42,
  "data": {
    "supplier_invoice_id": "8d1f4b2a-3c5e-4a7b-9d0e-6f1a2b3c4d5e",
    "number": 77,
    "supplier_id": 9,
    "kind": "INVOICE",
    "total_ore": 125000,
    "currency": "SEK",
    "invoice_currency": "SEK",
    "due_date": "2026-04-02"
  }
}

Kontrollera signaturen

Varje leverans har huvudet:

HTTP
Nonox-Signature: t=1772444713,v1=5f8e2a1c9b7d3e6f0a4c8b2d1e9f7a3c5b0d8e6f2a4c1b9d7e3f5a0c8b6d2e4f

t är tiden i Unix-sekunder och v1 är HMAC-SHA256, hex, av "<t>.<rå kropp>" med prenumerationens hemlighet. Så här kontrollerar du den:

  1. Läs den råa kroppen, byte för byte, innan något tolkar JSON.
  2. Dela upp huvudet på , och =. Det kan finnas flera v1 när en hemlighet byts; godta om någon stämmer.
  3. Avvisa om t är mer än fem minuter från din klocka (skydd mot uppspelning).
  4. Räkna ut HMAC-SHA256 av t + "." + kropp och jämför med konstant tid.
PHP
/**
 * Kontrollerar Nonox-Signature för en webhook. $payload är den råa kroppen
 * ($request->getContent() i Laravel), aldrig en tolkad och omkodad JSON.
 */
function verifyNonoxSignature(string $payload, string $header, string $secret, int $tolerance = 300): bool
{
    $timestamp = null;
    $signatures = [];

    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');

        if ($key === 't' && ctype_digit($value)) {
            $timestamp = (int) $value;
        }

        if ($key === 'v1') {
            $signatures[] = $value;
        }
    }

    if ($timestamp === null || abs(time() - $timestamp) > $tolerance) {
        return false;
    }

    $expected = hash_hmac('sha256', $timestamp.'.'.$payload, $secret);

    foreach ($signatures as $signature) {
        if (hash_equals($expected, $signature)) {
            return true;
        }
    }

    return false;
}

// I en Laravel-controller:
abort_unless(verifyNonoxSignature($request->getContent(), (string) $request->header('Nonox-Signature'), config('services.nonox.webhook_secret')), 400);
JavaScript
import crypto from 'node:crypto';
import express from 'express';

export function verifyNonoxSignature(rawBody, header, secret, toleranceSeconds = 300) {
  let timestamp = null;
  const signatures = [];

  for (const part of String(header).split(',')) {
    const [key, value = ''] = part.trim().split('=', 2);
    if (key === 't' && /^\d+$/.test(value)) timestamp = Number(value);
    if (key === 'v1') signatures.push(value);
  }

  if (timestamp === null || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
    return false;
  }

  const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();

  return signatures.some((signature) => {
    const given = Buffer.from(signature, 'hex');
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
}

// Express: läs kroppen rå, inte som JSON.
const app = express();
app.post('/nonox/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyNonoxSignature(req.body, req.get('Nonox-Signature'), process.env.NONOX_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(204); // Svara snabbt, gör jobbet i en kö.
});
Python
import hashlib
import hmac
import time


def verify_nonox_signature(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    timestamp, signatures = None, []

    for part in header.split(","):
        key, _, value = part.strip().partition("=")
        if key == "t" and value.isdigit():
            timestamp = int(value)
        if key == "v1":
            signatures.append(value)

    if timestamp is None or abs(time.time() - timestamp) > tolerance:
        return False

    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + payload, hashlib.sha256).hexdigest()

    return any(hmac.compare_digest(expected, signature) for signature in signatures)


# Flask: request.get_data() är den råa kroppen.
# if not verify_nonox_signature(request.get_data(), request.headers.get("Nonox-Signature", ""), os.environ["NONOX_WEBHOOK_SECRET"]):
#     abort(400)

Testa din kontroll

Med hemligheten test_secret, tiden 1772444713 och kroppen {"id":"c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f","type":"voucher.posted"} ska signaturen bli HMAC-SHA256 av 1772444713.{"id":"c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f","type":"voucher.posted"}. Räkna ut den själv och jämför:

Shell
printf '%s' '1772444713.{"id":"c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f","type":"voucher.posted"}' \
  | openssl dgst -sha256 -hmac 'test_secret' -hex

Kom ihåg att din kontroll avvisar en så gammal tid; testa med tolerance avstängd eller med en aktuell tid.