> For the complete documentation index, see [llms.txt](https://apidocs.arionbanki.is/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://apidocs.arionbanki.is/webhooks/webhooks-api-quick-start.md).

# Webhooks API Quick Start

All the information you need to subscribe to events and receive webhooks

Register a callback URL, subscribe to events and start receiving signed, encrypted webhook deliveries.

## API authentication

First things first, follow the instructions in the link to get all the necessary prerequisites to call the Webhooks API.

{% columns %}
{% column %}

#### Production

Calling the production environment requires:

* **API key**
* **Búnaðarskilríki certificate**
* **Auth Token**
  {% endcolumn %}

{% column %}

#### Sandbox

Calling the sandbox environment requires:

* **API key**
* **Sandbox Token**
  {% endcolumn %}
  {% endcolumns %}

{% columns %}
{% column %}
Here's a guide on how to get them.

[Production authentication](https://apidocs.arionbanki.is/business-apis/authentication/production)
{% endcolumn %}

{% column %}
Here's a more thorough guide.

[Sandbox authentication](https://apidocs.arionbanki.is/business-apis/authentication/sandbox)
{% endcolumn %}
{% endcolumns %}

***

## How webhooks work

1. **Subscribe.** You create a subscription with your **callback URL** and the **event types** you care about. We return a **signing secret**. Store it, as it is shown only once.
2. **Verify & activate.** A new subscription is **disabled**. You call the **verify** endpoint, and we POST a signed, encrypted test delivery to your callback URL. The first time your endpoint answers with a `2xx`, the subscription is **activated** and real events begin to flow.
3. **Receive.** From then on, every matching event is delivered to your callback URL as an HTTP `POST`, signed with your signing secret and encrypted with a key derived from it.
4. **Fetch.** The delivery envelope carries only identifiers and `links`. Your endpoint follows the `self` link to fetch the full resource from the relevant Business API.

The base URLs for the service are:

* Production: `https://apigw.arionbanki.is/webhooks`
* Sandbox: `https://apigwsandbox.arionbanki.is/webhooks`

***

## Available methods

Here is what the Webhooks API offers.

{% columns %}
{% column %}

### Subscriptions

Create a subscription:

* `POST /api/v1/subscriptions`

List your subscriptions:

* `GET /api/v1/subscriptions`

Get a subscription:

* `GET /api/v1/subscriptions/{subscriptionId}`
  {% endcolumn %}

{% column %}

### Manage & verify

Update a subscription:

* `PUT /api/v1/subscriptions/{subscriptionId}`

Cancel a subscription:

* `DELETE /api/v1/subscriptions/{subscriptionId}`

Verify endpoint & activate:

* `POST /api/v1/subscriptions/{subscriptionId}/verify`
  {% endcolumn %}
  {% endcolumns %}

***

## Choosing your event types

There are two levels to know about, and they are deliberately different:

* **What you subscribe to:** the values you put in `eventTypes` when you create or update a subscription.
* **What you receive:** the `eventType` in the delivered envelope, which is always a single concrete action such as `claim.paid`.

Each value in `eventTypes` is either a **category** or a single **action**:

| Value you subscribe with                     | What it selects                                                |
| -------------------------------------------- | -------------------------------------------------------------- |
| `Claim` (or the explicit wildcard `claim.*`) | **Every** claim action, including new ones we could add later. |
| `claim.paid`                                 | Only that one action. It never widens.                         |

Values are case-insensitive on input. A category is echoed back in its canonical `Claim` form and an action in its canonical `claim.paid` form. Unknown values are rejected with `400 Bad Request`.

The events we currently support:

| Action       | Description          |
| ------------ | -------------------- |
| `claim.paid` | A claim was settled. |

{% hint style="info" %}
Subscribing to the `Claim` category is the future-proof choice: when an action is enabled, wildcard subscribers start receiving it with **no change on your side**. Always ignore any `eventType` you don't recognise, so new actions are never a breaking change for you.
{% endhint %}

***

## Creating a subscription

Take a look at how you might create a subscription using `curl` or other popular programming languages. These examples target the production environment.

What you need is:

* **Búnaðarskilríki Certificate** location and its **password**, see [here](https://apidocs.arionbanki.is/business-apis/authentication/production#certificate).
* **API key** from the [developer portal](https://apidocs.arionbanki.is/business-apis/authentication/production#get-your-api-key).
* **Auth key** from the [authentication service](https://apidocs.arionbanki.is/business-apis/authentication/production#authorization).
* A random **UUID** to use as a request id.
* Your publicly reachable **callback URL** (HTTPS).

{% tabs %}
{% tab title="curl" %}

```bash
curl --cert <Directory of cert>/cert.pfx:<cert password> --cert-type P12 \
    -H "accept: application/json" \
    -H "Content-Type: application/json" \
    -H "X-Request-ID: <uuid for request>" \
    -H "Ocp-Apim-Subscription-Key: <API key>" \
    -H "Authorization: Bearer <auth key from the auth service>" \
    -X POST "https://apigw.arionbanki.is/webhooks/api/v1/subscriptions" \
    -d '{
          "callbackUrl": "https://your-domain.example/webhooks/arion",
          "eventTypes": ["Claim"],
          "description": "Production claims webhook",
          "subjectScope": "Self"
        }'
```

{% endtab %}

{% tab title="Python" %}

```python
import requests
import uuid
import tempfile
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
from cryptography.hazmat.primitives.serialization.pkcs12 import load_pkcs12

CERT_PATH = "<directory of cert>/cert.pfx"
CERT_PASSWORD = "<cert password>"
SUBSCRIPTION_KEY = "<API key from Developer portal>"
BEARER_TOKEN = "<Auth Key from Auth service>"

def load_pfx_as_pem_files(pfx_path: str, password: str):
    with open(pfx_path, "rb") as f:
        pfx_data = f.read()
    p12 = load_pkcs12(pfx_data, password.encode())
    cert_pem = p12.cert.certificate.public_bytes(Encoding.PEM)
    key_pem = p12.key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
    cert_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pem")
    key_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pem")
    cert_file.write(cert_pem)
    key_file.write(key_pem)
    cert_file.close()
    key_file.close()
    return cert_file.name, key_file.name

cert_pem_path, key_pem_path = load_pfx_as_pem_files(CERT_PATH, CERT_PASSWORD)

response = requests.post(
    url="https://apigw.arionbanki.is/webhooks/api/v1/subscriptions",
    headers={
        "Accept": "application/json",
        "Content-Type": "application/json",
        "X-Request-ID": str(uuid.uuid4()),
        "Ocp-Apim-Subscription-Key": SUBSCRIPTION_KEY,
        "Authorization": f"Bearer {BEARER_TOKEN}",
    },
    json={
        "callbackUrl": "https://your-domain.example/webhooks/arion",
        "eventTypes": ["Claim"],
        "description": "Production claims webhook",
        "subjectScope": "Self",
    },
    cert=(cert_pem_path, key_pem_path),
)
print(response.status_code)
print(response.json())
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import axios from "axios";
import https from "https";
import fs from "fs";
import { randomUUID } from "crypto";

const agent = new https.Agent({
  pfx: fs.readFileSync("/path/to/cert.pfx"),
  passphrase: "<your-cert-password>",
});

const response = await axios.post(
  "https://apigw.arionbanki.is/webhooks/api/v1/subscriptions",
  {
    callbackUrl: "https://your-domain.example/webhooks/arion",
    eventTypes: ["Claim"],
    description: "Production claims webhook",
    subjectScope: "Self",
  },
  {
    httpsAgent: agent,
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      "X-Request-ID": randomUUID(),
      "Ocp-Apim-Subscription-Key": "<your-subscription-key>",
      Authorization: "Bearer <your-token>",
    },
  }
);
console.log(JSON.stringify(response.data, null, 2));
```

{% endtab %}

{% tab title="C#" %}

```csharp
using System.Net.Http.Json;
using System.Security.Cryptography.X509Certificates;

var certPath = "<Directory of cert>/cert.pfx";
var certPassword = "<cert password>";
var subscriptionKey = "<API key>";
var bearerToken = "<auth key from the auth service>";

var cert = new X509Certificate2(certPath, certPassword);

var handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);

using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("X-Request-ID", Guid.NewGuid().ToString());
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey);
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {bearerToken}");

var url = "https://apigw.arionbanki.is/webhooks/api/v1/subscriptions";

var response = await client.PostAsJsonAsync(url, new
{
    callbackUrl = "https://your-domain.example/webhooks/arion",
    eventTypes = new[] { "Claim" },
    description = "Production claims webhook",
    subjectScope = "Self",
});

var body = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Status: {response.StatusCode}");
Console.WriteLine(body);
```

{% endtab %}
{% endtabs %}

A `201 Created` response returns the subscription together with a one-time **`signingSecret`**:

```json
{
  "subscriptionId": "9c4c0392-2923-4a4f-93ed-fa3c80874044",
  "callbackUrl": "https://your-domain.example/webhooks/arion",
  "eventTypes": ["Claim"],
  "subjectScope": "Self",
  "deliveryEnabled": false,
  "disabledReason": "pending_activation",
  "signingSecret": "hKVDDTtFGRzAs****_****3N8zy4nGEsqXg4",
  "links": [ { "rel": "self", "href": "…", "method": "GET" } ]
}
```

{% hint style="warning" %}
**Store the `signingSecret` now.** It is returned only once, at creation. You need it to verify the signature and decrypt the body of every delivery.
{% endhint %}

{% hint style="info" %}
Use the `signingSecret` **exactly as returned**: the UTF-8 bytes of the string are the key material for both the signature and the decryption. Do not Base64-decode it, trim it or add a prefix.
{% endhint %}

{% hint style="info" %}
You can hold a limited number of active subscriptions at a time (5 by default). Creating one beyond your limit returns `409 Conflict`. Cancel a subscription you no longer need, or talk to us if you need a higher limit.
{% endhint %}

***

## Verifying & activating your endpoint

A new subscription has `deliveryEnabled: false` and `disabledReason: pending_activation`. Call **verify** to have us POST a signed test delivery to your callback URL. The payload is identical to a real event: there is no "test" marker in the body, and the distinction is the endpoint you call.

```bash
curl --cert <Directory of cert>/cert.pfx:<cert password> --cert-type P12 \
    -H "X-Request-ID: <uuid for request>" \
    -H "Ocp-Apim-Subscription-Key: <API key>" \
    -H "Authorization: Bearer <auth key from the auth service>" \
    -X POST "https://apigw.arionbanki.is/webhooks/api/v1/subscriptions/<subscriptionId>/verify?eventType=Claim"
```

The `eventType` query parameter takes the **category** (`Claim`) and must be one your subscription covers.

The response reflects what **your** endpoint returned:

| Status                | Meaning                                                                 |
| --------------------- | ----------------------------------------------------------------------- |
| `200 OK`              | Your endpoint returned `2xx`, so the subscription is now **activated**. |
| `502 Bad Gateway`     | Your endpoint returned a non-`2xx` status.                              |
| `504 Gateway Timeout` | Your endpoint did not respond within the timeout.                       |
| `400 Bad Request`     | The event type is not enabled on the subscription.                      |
| `404 Not Found`       | The subscription does not exist or is not yours.                        |

Verify is safe to call as often as you like. It is how you smoke-test a deployment of your receiver, and how you switch delivery back on if it was ever disabled.

***

## Receiving & validating deliveries

Every delivery is an HTTP `POST` to your callback URL with these headers:

| Header                | Value                                                                      |
| --------------------- | -------------------------------------------------------------------------- |
| `Content-Type`        | `application/jose`. The body is a **JWE compact token**, not plain JSON.   |
| `X-Webhook-Signature` | The HMAC signature you verify (below).                                     |
| `X-Request-ID`        | A correlation id for the delivery. Log it to make support requests easier. |

{% hint style="warning" %}
Because the body is `application/jose`, make sure your framework does **not** try to parse it as JSON and that your endpoint accepts that media type. Read the **raw** body, since you need the exact bytes to verify the signature. Cap the request body size at a sensible limit before reading it; the envelope is small.
{% endhint %}

Answer with any `2xx` as soon as you have accepted the delivery; do your processing afterwards. Anything else (or a slow response) counts as a failed delivery and is retried.

In short, for every delivery: **verify the signature over the raw body, then decrypt the same body.** The body is the JWE compact string in both steps. The signature covers the encrypted text, not the decrypted JSON.

### 1. Verify the signature

Each request carries an **`X-Webhook-Signature`** header:

```
X-Webhook-Signature: t=1755097570,v1=3f9a…<hex>
```

* `t` is the Unix timestamp (seconds) that was signed.
* `v1` is `HMAC-SHA256(signingSecret, "{t}.{rawBody}")`, lowercase hex. `rawBody` is the request body exactly as received, which is the JWE compact string.

To verify: recompute the HMAC over `"{t}.{rawBody}"` using your signing secret and compare in constant time. Reject the delivery if `v1` does not match, or if `t` is outside your tolerance window (we recommend **5 minutes**) to guard against replay.

### 2. Decrypt the body

The body is a JWE Compact token (`alg=dir`, `enc=A256GCM`), encrypted with a key derived from your signing secret. After verifying the signature:

1. Derive the key:

   ```
   key = HKDF-SHA256(ikm = UTF-8(signingSecret), salt = empty, info = "obevents:webhook-payload-enc:v1", length = 32 bytes)
   ```

   The salt is empty (zero-length), which RFC 5869 treats the same as a string of zero bytes. Libraries that require a value accept `None`, `null` or an empty buffer.
2. Decrypt the token with any JOSE library, using `key` as the direct (`dir`) key. Reject any token whose header is not `alg=dir`, `enc=A256GCM`.

The five dot-separated parts of the token are the protected header, an empty encrypted-key part (because `alg=dir`), the IV, the ciphertext and the authentication tag, each Base64url-encoded. The protected header also carries a `kid`, the id of the signing secret in use. If you decrypt by hand rather than with a JOSE library, the AES-256-GCM additional authenticated data is the ASCII text of the first part, exactly as received.

{% hint style="danger" %}
Never log, share or send your signing secret or the derived key. Anyone holding either can read and forge your deliveries.
{% endhint %}

### Complete receiver examples

These examples verify the signature and decrypt the body using only the standard cryptography library of each language (Python also needs the `cryptography` package). Pass them the raw request body as a string.

{% tabs %}
{% tab title="C#" %}

```csharp
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public static class WebhookReceiver
{
    private static readonly TimeSpan Tolerance = TimeSpan.FromMinutes(5);

    public static bool VerifySignature(string signatureHeader, string rawBody, string signingSecret, DateTimeOffset? now = null)
    {
        string? timestamp = null;
        var signatures = new List<string>();
        foreach (var part in signatureHeader.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
        {
            if (part.StartsWith("t=", StringComparison.Ordinal)) timestamp = part[2..];
            else if (part.StartsWith("v1=", StringComparison.Ordinal)) signatures.Add(part[3..]);
        }

        if (timestamp is null || signatures.Count == 0
            || !long.TryParse(timestamp, NumberStyles.None, CultureInfo.InvariantCulture, out var unixSeconds))
        {
            return false;
        }

        var age = (now ?? DateTimeOffset.UtcNow) - DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
        if (age.Duration() > Tolerance) return false;

        var expected = Convert.ToHexStringLower(HMACSHA256.HashData(
            Encoding.UTF8.GetBytes(signingSecret),
            Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}")));

        return signatures.Exists(candidate => CryptographicOperations.FixedTimeEquals(
            Encoding.ASCII.GetBytes(candidate), Encoding.ASCII.GetBytes(expected)));
    }

    public static string Decrypt(string compactJwe, string signingSecret)
    {
        var parts = compactJwe.Split('.');
        if (parts.Length != 5 || parts[1].Length != 0) throw new FormatException("Not a JWE compact token");
        var protectedHeader = parts[0];

        using var header = JsonDocument.Parse(Base64UrlDecode(protectedHeader));
        if (header.RootElement.GetProperty("alg").GetString() != "dir"
            || header.RootElement.GetProperty("enc").GetString() != "A256GCM")
        {
            throw new FormatException("Unexpected JWE algorithm");
        }

        var key = HKDF.DeriveKey(
            HashAlgorithmName.SHA256,
            ikm: Encoding.UTF8.GetBytes(signingSecret),
            outputLength: 32,
            salt: null, // empty salt
            info: Encoding.UTF8.GetBytes("obevents:webhook-payload-enc:v1"));

        var ciphertext = Base64UrlDecode(parts[3]);
        var plaintext = new byte[ciphertext.Length];
        using var aes = new AesGcm(key, tagSizeInBytes: 16);
        aes.Decrypt(Base64UrlDecode(parts[2]), ciphertext, Base64UrlDecode(parts[4]), plaintext,
            Encoding.ASCII.GetBytes(protectedHeader)); // AAD = ASCII of the protected header
        CryptographicOperations.ZeroMemory(key);
        var json = Encoding.UTF8.GetString(plaintext);
        CryptographicOperations.ZeroMemory(plaintext);
        return json;
    }

    // rawBody is the request body exactly as received (the JWE string).
    // On failure answer with a bare 401/400 and log the details on your side only; never echo exception messages.
    public static string HandleDelivery(string rawBody, string signatureHeader, string signingSecret) =>
        VerifySignature(signatureHeader, rawBody, signingSecret)
            ? Decrypt(rawBody, signingSecret)
            : throw new UnauthorizedAccessException("Invalid signature");

    private static byte[] Base64UrlDecode(string value)
    {
        var s = value.Replace('-', '+').Replace('_', '/');
        return Convert.FromBase64String(s.PadRight(s.Length + (4 - s.Length % 4) % 4, '='));
    }
}
```

{% endtab %}

{% tab title="Python" %}

```python
import base64
import hashlib
import hmac
import json
import time
from typing import Optional

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

TOLERANCE_SECONDS = 300


def b64url_decode(value: str) -> bytes:
    return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))


def verify_signature(signature_header: str, raw_body: str, signing_secret: str, now: Optional[float] = None) -> bool:
    timestamp, signatures = None, []
    for part in signature_header.split(","):
        key, _, value = part.strip().partition("=")
        if key == "t":
            timestamp = value
        elif key == "v1":
            signatures.append(value)
    if timestamp is None or not signatures or not timestamp.isdigit():
        return False
    if abs((now if now is not None else time.time()) - int(timestamp)) > TOLERANCE_SECONDS:
        return False
    expected = hmac.new(
        signing_secret.encode("utf-8"),
        f"{timestamp}.{raw_body}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return any(hmac.compare_digest(candidate, expected) for candidate in signatures)


def decrypt(compact_jwe: str, signing_secret: str) -> dict:
    parts = compact_jwe.split(".")
    if len(parts) != 5:
        raise ValueError("Not a JWE compact token")
    protected, _, iv, ciphertext, tag = parts

    header = json.loads(b64url_decode(protected))
    if header.get("alg") != "dir" or header.get("enc") != "A256GCM":
        raise ValueError("Unexpected JWE algorithm")

    key = HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=None,  # empty salt
        info=b"obevents:webhook-payload-enc:v1",
    ).derive(signing_secret.encode("utf-8"))

    # AES-GCM expects ciphertext || tag; the AAD is the ASCII protected header.
    plaintext = AESGCM(key).decrypt(
        b64url_decode(iv),
        b64url_decode(ciphertext) + b64url_decode(tag),
        protected.encode("ascii"),
    )
    return json.loads(plaintext)


def handle_delivery(raw_body: str, signature_header: str, signing_secret: str) -> dict:
    """raw_body is the request body exactly as received (the JWE string)."""
    if not verify_signature(signature_header, raw_body, signing_secret):
        raise PermissionError("Invalid signature")
    return decrypt(raw_body, signing_secret)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { createHmac, createDecipheriv, hkdfSync, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

export function verifySignature(signatureHeader, rawBody, signingSecret, nowSeconds = Date.now() / 1000) {
  let timestamp = null;
  const signatures = [];
  for (const part of signatureHeader.split(",")) {
    const [key, ...rest] = part.trim().split("=");
    const value = rest.join("=");
    if (key === "t") timestamp = value;
    else if (key === "v1") signatures.push(value);
  }
  if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;
  if (Math.abs(nowSeconds - Number(timestamp)) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", signingSecret).update(`${timestamp}.${rawBody}`).digest("hex");
  return signatures.some(
    (candidate) => candidate.length === expected.length && timingSafeEqual(Buffer.from(candidate), Buffer.from(expected)),
  );
}

export function decrypt(compactJwe, signingSecret) {
  const parts = compactJwe.split(".");
  if (parts.length !== 5) throw new Error("Not a JWE compact token");
  const [protectedHeader, , iv, ciphertext, tag] = parts;

  const header = JSON.parse(Buffer.from(protectedHeader, "base64url").toString("utf8"));
  if (header.alg !== "dir" || header.enc !== "A256GCM") throw new Error("Unexpected JWE algorithm");

  const key = Buffer.from(
    hkdfSync("sha256", Buffer.from(signingSecret, "utf8"), Buffer.alloc(0), "obevents:webhook-payload-enc:v1", 32),
  );

  const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64url"));
  decipher.setAAD(Buffer.from(protectedHeader, "ascii"));
  decipher.setAuthTag(Buffer.from(tag, "base64url"));
  const plaintext = Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64url")), decipher.final()]);
  return JSON.parse(plaintext.toString("utf8"));
}

// rawBody is the request body exactly as received (the JWE string).
export function handleDelivery(rawBody, signatureHeader, signingSecret) {
  if (!verifySignature(signatureHeader, rawBody, signingSecret)) throw new Error("Invalid signature");
  return decrypt(rawBody, signingSecret);
}
```

{% endtab %}
{% endtabs %}

***

## The delivered envelope

The decrypted payload is a thin envelope:

```json
{
  "eventId": "9c4c0392-2923-4a4f-93ed-fa3c80874044",
  "eventType": "claim.paid",
  "schemaVersion": "1.0",
  "occurredAt": "2025-08-13T15:06:10Z",
  "links": [
    { "rel": "self", "href": "https://apigw.arionbanki.is/claims/api/v1/claims/1234567890", "method": "GET" }
  ]
}
```

{% hint style="info" %}
The envelope deliberately contains **no business data or PII**, only identifiers and `links`. Follow the `self` link and call the relevant Business API (authenticated as usual) to fetch the full resource.
{% endhint %}

***

## Delivery guarantees

Build your receiver around these three rules and it will stay correct under retries, replays and bursts.

#### Delivery is at-least-once

You **may receive the same event more than once**: a retry after an ambiguous failure, or a replay. **`eventId` is the idempotency key**: the same logical event always carries the same `eventId`, across every retry and every replay. Deduplicate on it, and keep the ids you have seen for at least **30 days**.

#### Events are not ordered

Events **may arrive out of order**, even for the same resource. We do not order them for you. `occurredAt` is the **business-event time** (when the event actually happened, stable across retries), so sort or reconcile on that if order matters to you.

#### An event is a signal, not a fact

Treat every delivery as a trigger to **GET the current state** through the `self` link, never as the state itself. Because the envelope is thin, a duplicate or an out-of-order delivery simply causes another fetch of the current resource, so your view converges on the truth either way.

***

## Retries & automatic disabling

If your endpoint does not return a `2xx`, we retry the delivery: once immediately, then with a widening backoff after 1, 2, 5, 15, 30 and 60 minutes, then 3, 6, 12 and 24 hours. That gives your endpoint roughly two days to come back before the delivery is given up on.

**If 5 deliveries in a row are given up on, we automatically switch delivery off** for that subscription so we are not hammering an endpoint that is clearly down. Nothing is pushed to you when this happens. The subscription resource itself is the source of truth, so poll it (`GET /api/v1/subscriptions/{subscriptionId}`) as part of your own monitoring:

```json
{
  "deliveryEnabled": false,
  "disabledReason": "delivery_failures_exhausted",
  "disabledAt": "2025-08-15T09:12:44Z"
}
```

To start receiving again, fix your endpoint and **call verify**, the same call you used to activate it the first time. A `200 OK` switches delivery back on.

{% hint style="info" %}
Events that occur while a previously-active subscription is switched off are **not silently dropped**. They are recorded so they can be replayed once you are back. Get in touch when you re-enable if you need that window replayed.
{% endhint %}

***

## API documentation

Look at the whole documentation to view all endpoints and possibilities how to use this service.

{% content-ref url="/pages/KTfBBDEAe1cu681tsUKD" %}
[Webhooks API Reference](/webhooks/webhooks-api-reference.md)
{% endcontent-ref %}

***

## Use Our Sample Clients

Check out our sample clients if you want an example of usage:

{% columns %}
{% column %}
Here's a guide on how to get Postman set up against our services:

[Postman Examples](https://apidocs.arionbanki.is/business-apis/examples/postman-examples)
{% endcolumn %}

{% column %}
Here's an example of a .NET client if you prefer that;

[.NET Sample Clients](https://apidocs.arionbanki.is/business-apis/examples/.net-sample-clients)
{% endcolumn %}
{% endcolumns %}
