> 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/business-apis/webhooks-api/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.

{% content-ref url="/pages/0RJ5SYumtOLMmoli2EdC" %}
[Production](/business-apis/authentication/production.md)
{% endcontent-ref %}
{% endcolumn %}

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

{% content-ref url="/pages/0lWYnBPAgrudG7Q5Sw76" %}
[Sandbox](/business-apis/authentication/sandbox.md)
{% endcontent-ref %}
{% 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, 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 URL for production is `https://apigw.arionbanki.is/events` (sandbox: `https://apigwsandbox.arionbanki.is/events`).

***

## Available methods

Here is what the Webhooks API offers.

{% columns %}
{% column %}

### Subscriptions

`POST /api/v1/subscriptions` — Create a subscription

`GET /api/v1/subscriptions` — List your subscriptions

`GET /api/v1/subscriptions/{subscriptionId}` — Get a subscription
{% endcolumn %}

{% column %}

### Manage & verify

`PUT /api/v1/subscriptions/{subscriptionId}` — Update a subscription

`DELETE /api/v1/subscriptions/{subscriptionId}` — Cancel a subscription

`POST /api/v1/subscriptions/{subscriptionId}/verify` — Verify endpoint & activate
{% 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 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 claim actions defined today are:

| Action          | Meaning                      | Status                   |
| --------------- | ---------------------------- | ------------------------ |
| `claim.paid`    | A claim was settled.         | **Delivered today**      |
| `claim.created` | A new claim was issued.      | Defined, not yet enabled |
| `claim.overdue` | A claim passed its due date. | Defined, not yet enabled |

{% 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](/business-apis/authentication/production.md#certificate).
* **API key** from the [developer portal](/business-apis/authentication/production.md#get-your-api-key).
* **Auth key** from the [authentication service](/business-apis/authentication/production.md#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/events/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/events/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/events/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/events/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": "whsec_…",
  "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" %}
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; 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/events/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` — 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 — it makes support requests far 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 — you need the exact bytes to verify the signature.
{% 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.

### 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. More than one `v1=` value may be present during secret rotation — a match against **any** of them is valid.

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

### 2. Decrypt the body

The request body is a **JWE Compact** token (AES-256-GCM). The content-encryption key is **HKDF-derived from your signing secret**, so the same secret both verifies and decrypts. Verify the signature over the raw JWE string **first**, then decrypt.

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/1rx2uJXPwAvQoPrSNcpP" %}
[Webhooks API Reference](/business-apis/webhooks-api/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:

{% content-ref url="/pages/Qf4oYxCJ7tayE0YVbEMa" %}
[Postman Examples](/business-apis/examples/postman-examples.md)
{% endcontent-ref %}
{% endcolumn %}

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

{% content-ref url="/pages/R5GwZIHeTwiKK8nehodk" %}
[.NET Sample Clients](/business-apis/examples/.net-sample-clients.md)
{% endcontent-ref %}
{% endcolumn %}
{% endcolumns %}
