Webhooks
Overview
Subscribe to events (accounts, payments, cards) and receive POST notifications on your own URL. Managing subscriptions is a CRUD over a Webhook object.
Authentication
These endpoints are authenticated with partner-level headers, not with a client bearer token.
| Header | Type | Required | Description |
|---|---|---|---|
x-client-id | UUID | Required | Your client id. |
x-api-key | UUID | Required | Your API key. |
Both headers are required on every request in this chapter.
A missing or malformed header returns 403.
x-client-id: <your-client-id>
x-api-key: <your-api-key>
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/webhooks | Create a subscription. |
| GET | /v1/webhooks | List subscriptions (paginated). |
| GET | /v1/webhooks/:webhookId | Read one subscription. |
| PATCH | /v1/webhooks/:webhookId | Update a subscription. |
| DELETE | /v1/webhooks/:webhookId | Delete a subscription (irreversible). |
Create a subscription
/v1/webhooksAuth: Partner client ID and API key headers — how to send the partner headers.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| url | string (URL) | Required | Where the POST notifications are delivered. | |
| secret | string | Required | Secret used to sign outgoing notifications; never returned back. | |
| events | enum[] | Required | At least one event from the catalogue. | |
| isActive | boolean | Required | Whether the subscription is enabled. | |
| description | string | Optional | Free-form description. |
/v1/webhookscurl -X POST 'https://sandbox-bank.tedr.com/open-banking/v1/webhooks' \
-H 'x-client-id: <your-client-id>' \
-H 'x-api-key: <your-api-key>' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://partner-app.com/webhooks/payments",
"secret": "<your-signing-secret>",
"events": ["INNER_PAYMENT_SUCCEEDED", "ACCOUNTS_CREATED"],
"isActive": true,
"description": "Webhook to notify when payments are updated."
}'
const response = await fetch('https://sandbox-bank.tedr.com/open-banking/v1/webhooks', {
method: 'POST',
headers: {
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://partner-app.com/webhooks/payments',
secret: '<your-signing-secret>',
events: ['INNER_PAYMENT_SUCCEEDED', 'ACCOUNTS_CREATED'],
isActive: true,
description: 'Webhook to notify when payments are updated.',
}),
});
const { data: webhook } = await response.json();
import requests
response = requests.post(
'https://sandbox-bank.tedr.com/open-banking/v1/webhooks',
headers={
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
},
json={
'url': 'https://partner-app.com/webhooks/payments',
'secret': '<your-signing-secret>',
'events': ['INNER_PAYMENT_SUCCEEDED', 'ACCOUNTS_CREATED'],
'isActive': True,
'description': 'Webhook to notify when payments are updated.',
},
)
webhook = response.json()['data']
secret is not returned.
The response is the subscription object — id, createdAt, description, isActive, url, events. Store the secret on your side.
List and read subscriptions
Only subscriptions belonging to your x-client-id are returned. events come back sorted alphabetically.
GET /v1/webhooks
/v1/webhooksAuth: Partner client ID and API key headers — how to send the partner headers.
Query: ids[], pagination[limit], pagination[skip].
/v1/webhookscurl -G 'https://sandbox-bank.tedr.com/open-banking/v1/webhooks' \
-H 'x-client-id: <your-client-id>' \
-H 'x-api-key: <your-api-key>' \
-d 'pagination[skip]=0' \
-d 'pagination[limit]=10'
const query = new URLSearchParams({
'pagination[skip]': '0',
'pagination[limit]': '10',
});
const response = await fetch(`https://sandbox-bank.tedr.com/open-banking/v1/webhooks?${query}`, {
headers: {
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
},
});
const { data } = await response.json();
import requests
response = requests.get(
'https://sandbox-bank.tedr.com/open-banking/v1/webhooks',
headers={
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
},
params={'pagination[skip]': 0, 'pagination[limit]': 10},
)
data = response.json()['data']
GET /v1/webhooks/:webhookId
/v1/webhooks/:webhookIdAuth: Partner client ID and API key headers — how to send the partner headers.
/v1/webhooks/:webhookIdcurl 'https://sandbox-bank.tedr.com/open-banking/v1/webhooks/<webhookId>' \
-H 'x-client-id: <your-client-id>' \
-H 'x-api-key: <your-api-key>'
const response = await fetch('https://sandbox-bank.tedr.com/open-banking/v1/webhooks/<webhookId>', {
headers: {
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
},
});
const { data: webhook } = await response.json();
import requests
response = requests.get(
'https://sandbox-bank.tedr.com/open-banking/v1/webhooks/<webhookId>',
headers={
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
},
)
webhook = response.json()['data']
Update and delete
PATCH /v1/webhooks/:webhookId
/v1/webhooks/:webhookIdAuth: Partner client ID and API key headers — how to send the partner headers.
All fields are optional; only what is sent is changed.
/v1/webhooks/:webhookIdcurl -X PATCH 'https://sandbox-bank.tedr.com/open-banking/v1/webhooks/<webhookId>' \
-H 'x-client-id: <your-client-id>' \
-H 'x-api-key: <your-api-key>' \
-H 'Content-Type: application/json' \
-d '{
"isActive": false,
"url": "https://partner-app.com/webhooks/payments-v2",
"description": "Updated webhook.",
"events": {
"attach": ["SWIFT_PAYMENT_SUCCEEDED"],
"detach": ["ACCOUNTS_CREATED"]
}
}'
const response = await fetch('https://sandbox-bank.tedr.com/open-banking/v1/webhooks/<webhookId>', {
method: 'PATCH',
headers: {
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
'Content-Type': 'application/json',
},
body: JSON.stringify({
isActive: false,
url: 'https://partner-app.com/webhooks/payments-v2',
description: 'Updated webhook.',
events: {
attach: ['SWIFT_PAYMENT_SUCCEEDED'],
detach: ['ACCOUNTS_CREATED'],
},
}),
});
const { data: webhook } = await response.json();
import requests
response = requests.patch(
'https://sandbox-bank.tedr.com/open-banking/v1/webhooks/<webhookId>',
headers={
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
},
json={
'isActive': False,
'url': 'https://partner-app.com/webhooks/payments-v2',
'description': 'Updated webhook.',
'events': {
'attach': ['SWIFT_PAYMENT_SUCCEEDED'],
'detach': ['ACCOUNTS_CREATED'],
},
},
)
webhook = response.json()['data']
The event list is modified incrementally.
Use attach / detach — the list is not overwritten with a new array, and at least one event must remain attached.
DELETE /v1/webhooks/:webhookId
/v1/webhooks/:webhookIdAuth: Partner client ID and API key headers — how to send the partner headers.
DELETE /v1/webhooks/:webhookId removes the subscription permanently and returns the deleted object.
/v1/webhooks/:webhookIdcurl -X DELETE 'https://sandbox-bank.tedr.com/open-banking/v1/webhooks/<webhookId>' \
-H 'x-client-id: <your-client-id>' \
-H 'x-api-key: <your-api-key>'
const response = await fetch('https://sandbox-bank.tedr.com/open-banking/v1/webhooks/<webhookId>', {
method: 'DELETE',
headers: {
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
},
});
const { data: deletedWebhook } = await response.json();
import requests
response = requests.delete(
'https://sandbox-bank.tedr.com/open-banking/v1/webhooks/<webhookId>',
headers={
'x-client-id': '<your-client-id>',
'x-api-key': '<your-api-key>',
},
)
deleted_webhook = response.json()['data']
Event catalogue
A notification is delivered only to partners that have both an active subscription and the matching scope granted by the client.
| Group | Events | Scope required |
|---|---|---|
| Accounts | ACCOUNTS_ORDERED, ACCOUNTS_CREATED | tb:accounts:read |
| INNER payments | INNER_PAYMENT_CREATED / _UPDATED / _SUCCEEDED / _DECLINED / _REFUNDED | tb:payments:read |
| SWIFT payments | SWIFT_PAYMENT_CREATED / _UPDATED / _SUCCEEDED / _DECLINED / _REFUNDED | tb:payments:read |
| INTERNAL_CARDS payments | INTERNAL_CARDS_PAYMENT_CREATED / _UPDATED / _SUCCEEDED / _DECLINED / _REFUNDED | tb:payments:read |
| Cards | INTERNAL_CARD_ORDERED, INTERNAL_CARD_UPDATED, INTERNAL_CARD_EXPIRES, INTERNAL_CARD_EXPIRED | tb:internal-cards:read |
| Stripe cards | STRIPE_CARD_CREATED, STRIPE_CARD_EXPIRES, STRIPE_CARD_EXPIRED | tb:external-cards:read |
| Stripe payments | STRIPE_PAYMENT_CREATED, STRIPE_PAYMENT_UPDATED | tb:external-cards:read |
What arrives at your URL
| Field | Type | Description |
|---|---|---|
id | UUID | Unique id of this delivery — use it for idempotency. |
webhookId | UUID | The subscription the notification belongs to. |
event | string | Event name from the catalogue. |
timestamp | number | Unix time in seconds. |
data | object | The entity snapshot: the payment, the account list or the card. |
{
"id": "9b1c8f0e-7d3a-4e2b-8a11-6c5d4e3f2a10",
"webhookId": "5f4d42e5-60cb-42bc-b0d4-2b73d063560f",
"event": "INNER_PAYMENT_SUCCEEDED",
"timestamp": 1748689100,
"data": { }
}
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
X-SIGNATURE | HMAC-SHA1(secret, raw body) in hex, where secret is the one you set on the subscription. |
Compute the HMAC over the raw body.
Sign the bytes exactly as received, before any JSON parsing.
const crypto = require('crypto');
const SECRET = '<your-signing-secret>';
// rawBody: the request body bytes exactly as received, before any JSON parsing.
function isValidSignature(rawBody, signature, secret) {
// X-SIGNATURE is HMAC-SHA1 in hex: exactly 40 hex characters.
if (typeof signature !== 'string' || !/^[0-9a-f]{40}$/i.test(signature)) {
return false;
}
const expected = Buffer.from(
crypto.createHmac('sha1', secret).update(rawBody).digest('hex'),
'hex',
);
const received = Buffer.from(signature, 'hex');
return (
received.length === expected.length &&
crypto.timingSafeEqual(received, expected)
);
}
const ok = isValidSignature(rawBody, req.headers['x-signature'], SECRET);
What your endpoint must return
Any 2xx status counts as a successful delivery. The response body is ignored — 200 with an empty body, 204, or 200 with any JSON all work equally.
Delivery guarantees
Deliveries are at-least-once: the same event can arrive more than once, so deduplicate by id. The order of deliveries is not guaranteed.
Retries
The response timeout is 10 seconds; a timeout counts as a failed delivery. Failed deliveries are retried on a fixed schedule — 20 attempts in total: three after 15 s, one after 30 s, three after 60 s, two after 320 s, five after ~2.8 h, five after 12 h and a final one after 24 h.
15, 15, 15, 30, 60, 60, 60, 320, 320, 10240 × 5, 43200 × 5, 86400
An exhausted schedule disables the subscription.
It is switched to isActive: false with the reason RETRY_TIMEOUT — fix the receiver and re-enable it with PATCH.
