Skip to content

Webhooks ​

Quarzo sends a notification to your server when an event occurs for your Broker, for example when a payment is received or a policy is created.

Register a callback URL ​

Use an access token with the broker-admin role to register the URL for your Broker. The broker-advisor role does not permit registration.

http
POST /v1/broker-notifications/registrations
Authorization: Bearer <access_token>
X-Quarzo-Life-Idempotency-Key: <unique-key>
Content-Type: application/json

{ "callbackUrl": "https://example.com/webhooks/quarzo" }

The X-Quarzo-Life-Idempotency-Key header is required. See Idempotent requests.

Expected result: 201 Created with a Location header and this body:

http
Location: /v1/broker-notifications/registrations/0199b8f1-4c2e-7a10-9d3b-2f6c1e8a5b70
json
{
  "id": "0199b8f1-4c2e-7a10-9d3b-2f6c1e8a5b70",
  "brokerId": "<broker-id>",
  "callbackUrl": "https://example.com/webhooks/quarzo",
  "signingSecret": "whsec_q8V4mZtL2nR7xK1cB9dF3gH6wJ0sA5eYpU4iO2lT8rE"
}

Keep the signing secret

Quarzo returns signingSecret one time only, in this response. A replay with the same idempotency key returns the registration without signingSecret. Keep the secret in a secret store, and do not write it in logs. You need it to verify each notification.

Keep the id of the registration. You need it to read or revoke the registration. A replay with the same idempotency key returns the same id.

StatusCodeCause
400PARAMETER_INVALIDThe body is not valid, or the idempotency key is missing, empty or too long.
401The access token is missing, expired or not valid.
403The token does not have the permission to register a callback URL.
409CALLBACK_URL_ALREADY_REGISTERED_FOR_BROKERYour Broker already registered this URL.
409IDEMPOTENCY_KEY_CONFLICTThe same key was used with a different body.

You can register more than one URL. Each registered URL receives each notification.

Use an HTTPS URL. Some notifications contain personal data of Subscribers.

List, read or revoke a registration ​

Use an access token with the broker-admin role. The broker-advisor role does not permit these requests. You can list, read and revoke only the registrations of your Broker.

List your registrations ​

List your registrations to find the id of a registration.

http
GET /v1/broker-notifications/registrations?page=1&perPage=20
Authorization: Bearer <access_token>
Query parameterDescription
pageThe page number, from 1. The default value is 1.
perPageThe number of registrations in a page, 100 at most. The default is 20.
statusACTIVE or REVOKED. Without it, the list contains the two statuses.

Expected result: 200 OK with this body. The newest registration is first:

json
{
  "data": [
    {
      "id": "0199b8f1-4c2e-7a10-9d3b-2f6c1e8a5b70",
      "callbackUrl": "https://example.com/webhooks/quarzo",
      "status": "ACTIVE",
      "revokedAt": null
    }
  ],
  "meta": {
    "page": 1,
    "perPage": 20,
    "totalPages": 1,
    "totalCount": 1,
    "hasNext": false,
    "hasPrevious": false
  }
}

The response never contains a signing secret.

Read a registration ​

http
GET /v1/broker-notifications/registrations/<registration-id>
Authorization: Bearer <access_token>

Expected result: 200 OK with this body:

json
{
  "id": "0199b8f1-4c2e-7a10-9d3b-2f6c1e8a5b70",
  "callbackUrl": "https://example.com/webhooks/quarzo",
  "status": "ACTIVE",
  "revokedAt": null
}
FieldDescription
statusACTIVE: the URL receives the notifications. REVOKED: the URL receives no notification.
revokedAtThe time of the revocation. It is null for an ACTIVE registration.

The response never contains the signing secret.

Revoke a registration ​

Revoke a registration to stop the notifications to its callback URL. Do this immediately if the URL is wrong, or if another person possibly controls the URL or knows the signing secret.

http
DELETE /v1/broker-notifications/registrations/<registration-id>
Authorization: Bearer <access_token>

Expected result: 204 No Content. The request is idempotent: a registration that is already revoked returns 204 again.

After the revocation:

  • Quarzo sends no new notification to the callback URL.
  • Quarzo cancels the notifications that are not delivered, and does not try them again.
  • A notification that Quarzo sends at the time of the revocation can still arrive.
  • You can register the same callback URL again. The new registration has a new id and a new signing secret.

You cannot activate a revoked registration again.

Errors ​

StatusCodeCause
400PARAMETER_INVALIDThe registration identifier or a query parameter is not valid.
401The access token is missing, expired or not valid.
403The token does not have the permission.
404REGISTRATION_NOT_FOUNDThe registration does not exist, or belongs to a different Broker.

In this version, you cannot change the callback URL of a registration. Revoke the registration and register the new URL.

Notification format ​

Each notification is a POST request with two signature headers and a JSON body:

http
POST /webhooks/quarzo HTTP/1.1
Content-Type: application/json
X-Quarzo-Timestamp: 1791274542
X-Quarzo-Signature: sha256=5d41f9c0a7b3e2d8c6f14a9b0e7d3c2a1f8e6b4d9c0a7f3e2b1d8c6a5f4e3d2c
json
{
  "eventId": "0199b8f3-6c1a-7d42-9a0e-3f1b2c4d5e6f",
  "type": "VirtualIbanGenerated",
  "payload": {
    "policyId": "0199b7aa-2b3c-7d4e-8f90-1a2b3c4d5e6f",
    "virtualIban": "FR7630001007941234567890185"
  }
}
FieldDescription
eventIdThe identifier of the event. It is the same in each attempt of a delivery.
typeThe event type. See the list below.
payloadThe data of the event. Its fields change with the event type.
HeaderDescription
X-Quarzo-TimestampThe Unix time of the attempt, in seconds. Each attempt has a new timestamp.
X-Quarzo-Signaturesha256= and the signature of the attempt, in hexadecimal. See Security.

Event types ​

Quarzo sends these event types:

  • DirectDebitFailed
  • PayerCreated
  • PayerPersonalIbanAdded
  • PayerPersonalIbanDeleted
  • PaymentReceived
  • PaymentRequestCancelled
  • PaymentRequested
  • PolicyCreated
  • SubscriberCreated
  • TransferFailed
  • VirtualIbanGenerated

Your server receives only the events of your Broker. In this version, you cannot select event types.

Money values ​

Each Money value in a payload uses the Money format: an integer string in the minor unit of the currency, and the currency code. The payload has no scale field, and the amount has no n suffix.

This PaymentReceived notification is for a payment of €100.50:

json
{
  "eventId": "0199b8f3-6c1a-7d42-9a0e-3f1b2c4d5e70",
  "type": "PaymentReceived",
  "payload": {
    "paymentRequestId": "0199b7aa-2b3c-7d4e-8f90-1a2b3c4d5e70",
    "premiumRequestId": "0199b7aa-2b3c-7d4e-8f90-1a2b3c4d5e71",
    "premiumRequestType": "SINGLE_PREMIUM_REQUEST",
    "compartmentId": "0199b7aa-2b3c-7d4e-8f90-1a2b3c4d5e72",
    "amount": { "amount": "10050", "currency": "EUR" },
    "mode": "DIRECT_DEBIT",
    "status": "DIRECT_DEBIT_SUCCESS"
  }
}

Respond to a notification ​

Return a 2XX status code in less than 10 seconds. Quarzo records any other status, a timeout or a network error as a failure.

http
HTTP/1.1 200 OK

If your processing takes more time, send the response first. Then do the work in the background.

Duplicate notifications

Your server can receive the same notification more than once. Use eventId to find duplicates, and process each eventId only one time.

Retries ​

After a failure, Quarzo tries again. The delay doubles after each failure. Quarzo makes 5 attempts at most:

AttemptApproximate time after the first failure
1st (first delivery)—
2nd1 h 30 min
3rd4 h 30 min
4th10 h 30 min
5th (last attempt)22 h 30 min

After the fifth failure, Quarzo stops. It does not send that notification again. In this version, you cannot see the delivery history or send a notification again.

Security ​

Quarzo signs each notification with the signing secret of the registration. Verify the signature before you process a notification. A request without a valid signature does not come from Quarzo.

The signing secret ​

Quarzo creates one signing secret for each registration, and returns it one time only, when you register the callback URL.

The secret has this format: whsec_ and the base64url encoding of 32 random bytes. The HMAC key is the 32 decoded bytes, not the text of the secret. To get the key:

  1. Remove the whsec_ prefix.
  2. Decode the remaining text as base64url without padding.

The signature ​

Quarzo calculates the signature of each attempt as follows:

text
signature = HMAC_SHA256(key, timestamp + "." + rawBody)
  • key is the 32 decoded bytes of the signing secret.
  • timestamp is the value of the X-Quarzo-Timestamp header, as text.
  • rawBody is the exact bytes of the request body.

The X-Quarzo-Signature header contains sha256= and the signature in lowercase hexadecimal. A retry has a new timestamp, and thus a new signature.

Verify a notification ​

Do these steps for each request, before you process the notification:

  1. Read the raw body of the request. Do not parse the JSON and serialize it again: the bytes can change.
  2. Read the X-Quarzo-Timestamp and X-Quarzo-Signature headers. If one header is missing, refuse the request.
  3. Compare the timestamp with the time of your server. Refuse a timestamp that is older than 5 minutes. Refuse a timestamp that is more than 30 seconds in the future.
  4. Calculate the signature with your signing secret, the timestamp and the raw body.
  5. Compare your signature with the header in constant time. If they are different, refuse the request.

To refuse a request, return a 401 status code and do not process the body.

This Node.js example does these steps:

js
import { createHmac, timingSafeEqual } from "node:crypto";

const MAX_AGE_SECONDS = 5 * 60;
const MAX_FUTURE_SECONDS = 30;

// signingSecret: the `signingSecret` value of the registration.
// rawBody: the bytes of the request body (a Buffer), before JSON parsing.
export function isQuarzoNotification({ signingSecret, headers, rawBody }) {
  const timestamp = headers["x-quarzo-timestamp"] ?? "";
  const signature = headers["x-quarzo-signature"] ?? "";
  if (!/^\d+$/.test(timestamp)) return false;

  const age = Math.floor(Date.now() / 1000) - Number(timestamp);
  if (age > MAX_AGE_SECONDS || age < -MAX_FUTURE_SECONDS) return false;

  const key = Buffer.from(signingSecret.slice("whsec_".length), "base64url");
  const expected =
    "sha256=" +
    createHmac("sha256", key)
      .update(`${timestamp}.`)
      .update(rawBody)
      .digest("hex");

  const received = Buffer.from(signature);
  const computed = Buffer.from(expected);
  return received.length === computed.length && timingSafeEqual(received, computed);
}

If you do not have the signing secret ​

Quarzo cannot show a signing secret again. You do not have the secret in these cases:

  • you registered the callback URL before Quarzo signed notifications;
  • you lost the response of the registration, or you lost the secret.

To get a signing secret, replace the registration:

  1. If you do not have the id of the registration, list your registrations.
  2. Revoke the registration.
  3. Register the callback URL again.
  4. Keep the new signing secret.

A server that does not verify the signature continues to receive the notifications: the two headers do not change the body. Until your server verifies the signature, read the current state with the Quarzo API before you change data in your system.

Other rules ​

  • Use an HTTPS URL. Some notifications contain personal data of Subscribers.
  • Use a different signing secret for each environment. A sandbox secret does not verify production notifications.
  • If a signing secret is possibly known to another person, revoke the registration and register the callback URL again.

Best practices ​

  • Verify the signature first. Refuse a request that has no valid signature or has an old timestamp.
  • Respond quickly. Return 200 immediately, and process the notification in a background job.
  • Find duplicates with eventId. Keep the eventId values that you processed.
  • Monitor your endpoint. After the fifth failure, Quarzo does not send the notification again.

Website