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.
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:
Location: /v1/broker-notifications/registrations/0199b8f1-4c2e-7a10-9d3b-2f6c1e8a5b70{
"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.
| Status | Code | Cause |
|---|---|---|
400 | PARAMETER_INVALID | The body is not valid, or the idempotency key is missing, empty or too long. |
401 | The access token is missing, expired or not valid. | |
403 | The token does not have the permission to register a callback URL. | |
409 | CALLBACK_URL_ALREADY_REGISTERED_FOR_BROKER | Your Broker already registered this URL. |
409 | IDEMPOTENCY_KEY_CONFLICT | The 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.
GET /v1/broker-notifications/registrations?page=1&perPage=20
Authorization: Bearer <access_token>| Query parameter | Description |
|---|---|
page | The page number, from 1. The default value is 1. |
perPage | The number of registrations in a page, 100 at most. The default is 20. |
status | ACTIVE or REVOKED. Without it, the list contains the two statuses. |
Expected result: 200 OK with this body. The newest registration is first:
{
"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
GET /v1/broker-notifications/registrations/<registration-id>
Authorization: Bearer <access_token>Expected result: 200 OK with this body:
{
"id": "0199b8f1-4c2e-7a10-9d3b-2f6c1e8a5b70",
"callbackUrl": "https://example.com/webhooks/quarzo",
"status": "ACTIVE",
"revokedAt": null
}| Field | Description |
|---|---|
status | ACTIVE: the URL receives the notifications. REVOKED: the URL receives no notification. |
revokedAt | The 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.
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
idand a new signing secret.
You cannot activate a revoked registration again.
Errors
| Status | Code | Cause |
|---|---|---|
400 | PARAMETER_INVALID | The registration identifier or a query parameter is not valid. |
401 | The access token is missing, expired or not valid. | |
403 | The token does not have the permission. | |
404 | REGISTRATION_NOT_FOUND | The 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:
POST /webhooks/quarzo HTTP/1.1
Content-Type: application/json
X-Quarzo-Timestamp: 1791274542
X-Quarzo-Signature: sha256=5d41f9c0a7b3e2d8c6f14a9b0e7d3c2a1f8e6b4d9c0a7f3e2b1d8c6a5f4e3d2c{
"eventId": "0199b8f3-6c1a-7d42-9a0e-3f1b2c4d5e6f",
"type": "VirtualIbanGenerated",
"payload": {
"policyId": "0199b7aa-2b3c-7d4e-8f90-1a2b3c4d5e6f",
"virtualIban": "FR7630001007941234567890185"
}
}| Field | Description |
|---|---|
eventId | The identifier of the event. It is the same in each attempt of a delivery. |
type | The event type. See the list below. |
payload | The data of the event. Its fields change with the event type. |
| Header | Description |
|---|---|
X-Quarzo-Timestamp | The Unix time of the attempt, in seconds. Each attempt has a new timestamp. |
X-Quarzo-Signature | sha256= and the signature of the attempt, in hexadecimal. See Security. |
Event types
Quarzo sends these event types:
DirectDebitFailedPayerCreatedPayerPersonalIbanAddedPayerPersonalIbanDeletedPaymentReceivedPaymentRequestCancelledPaymentRequestedPolicyCreatedSubscriberCreatedTransferFailedVirtualIbanGenerated
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:
{
"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/1.1 200 OKIf 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:
| Attempt | Approximate time after the first failure |
|---|---|
| 1st (first delivery) | — |
| 2nd | 1 h 30 min |
| 3rd | 4 h 30 min |
| 4th | 10 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:
- Remove the
whsec_prefix. - Decode the remaining text as base64url without padding.
The signature
Quarzo calculates the signature of each attempt as follows:
signature = HMAC_SHA256(key, timestamp + "." + rawBody)keyis the 32 decoded bytes of the signing secret.timestampis the value of theX-Quarzo-Timestampheader, as text.rawBodyis 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:
- Read the raw body of the request. Do not parse the JSON and serialize it again: the bytes can change.
- Read the
X-Quarzo-TimestampandX-Quarzo-Signatureheaders. If one header is missing, refuse the request. - 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.
- Calculate the signature with your signing secret, the timestamp and the raw body.
- 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:
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:
- If you do not have the
idof the registration, list your registrations. - Revoke the registration.
- Register the callback URL again.
- 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
200immediately, and process the notification in a background job. - Find duplicates with
eventId. Keep theeventIdvalues that you processed. - Monitor your endpoint. After the fifth failure, Quarzo does not send the notification again.