Appearance
Webhook endpoints
A webhook endpoint is a URL Zay sends events to. You can register up to 10. Managing them uses the same signed requests as the rest of the API; the deliveries themselves are signed differently — see Receive and verify webhooks.
New to webhooks? See How webhook delivery works. For what Zay sends, see the Event catalog.
| Endpoint | Operation |
|---|---|
POST /merchants/v1/webhook_endpoints | Register webhook endpoint |
GET /merchants/v1/webhook_endpoints | List webhook endpoints |
GET /merchants/v1/webhook_endpoints/{id} | Retrieve webhook endpoint |
PATCH /merchants/v1/webhook_endpoints/{id} | Update webhook endpoint |
DELETE /merchants/v1/webhook_endpoints/{id} | Delete webhook endpoint |
POST /merchants/v1/webhook_endpoints/{id}/test_delivery | Trigger test delivery |
The WebhookEndpoint object
json
{
"webhook_endpoint": {
"id": "11111111-1111-7000-8000-000000000001",
"url": "https://example.com/zay-webhooks",
"description": "Production endpoint",
"status": "enabled",
"created_at": "2025-12-05T12:05:49Z",
"updated_at": "2025-12-05T12:05:49Z"
}
}| Field | Type | Description |
|---|---|---|
id | string (uuid) | |
url | string (uri) | HTTPS URL Zay will POST webhook deliveries to. |
description | string | null | Optional free-form label. |
status | string | Endpoint status. disabled is set either manually by the merchant or automatically when sustained delivery failures trip Zay's autodisable policy. One of enabled, disabled. |
created_at | string (date-time) | Creation date and time (ISO-8601) |
updated_at | string (date-time) | Creation date and time (ISO-8601) |
New endpoints start enabled. Zay sets an endpoint to disabled itself when deliveries keep failing — see How webhook delivery works.
The signing secret
A signing_secret is generated when you register the endpoint. It is returned exactly once — in the response to Register webhook endpoint. Zay cannot show it to you again. Store it in your secret manager right away. If you lose it, delete the endpoint and register it again.
Secrets are prefixed with whsec_, matching the Standard Webhooks convention, so they are easy to identify and grep for in logs.
Register webhook endpoint
Register a new HTTPS URL to receive webhook deliveries. Returns the endpoint with its signing_secret — the only response that ever carries it.
POST /merchants/v1/webhook_endpoints
Request body
| Field | Type | Description |
|---|---|---|
webhook_endpointrequired | object | |
webhook_endpoint.urlrequired | string (uri) | |
webhook_endpoint.description | string | null |
The url must use https and resolve to a public address — loopback, private (RFC 1918) and link-local addresses are rejected with 422.
Returns 201 Created with a wrapped webhook_endpoint that includes signing_secret.
Errors:
401— invalid signature.422— the URL fails validation, or you already have 10 endpoints.429— rate limit exceeded.
Examples
cURL
bash
curl -X POST "https://api-connect.zay.me/merchants/v1/webhook_endpoints" \
-H "Content-Type: application/json" \
-H "zay-api-key: $ZAY_API_KEY" \
-H "zay-timestamp: $TS" \
-H "zay-signature: $SIG" \
-H 'Idempotency-Key: "d7c59ab2-3f12-4ad8-9b03-2a7b4f9c7e10"' \
-d '{
"webhook_endpoint": {
"url": "https://merchant.example.com/zay-webhooks",
"description": "Production endpoint"
}
}'Python
python
body = json.dumps({
"webhook_endpoint": {
"url": "https://merchant.example.com/zay-webhooks",
"description": "Production endpoint",
}
})
url = "https://api-connect.zay.me/merchants/v1/webhook_endpoints"
headers = {
"Content-Type": "application/json",
"zay-api-key": ZAY_API_KEY,
"Idempotency-Key": f'"{uuid.uuid4()}"',
**merchant_headers("POST", url, ZAY_PRIVATE_KEY, body),
}
r = requests.post(url, headers=headers, data=body)
endpoint = r.json()["webhook_endpoint"]
signing_secret = endpoint["signing_secret"] # store this immediatelyNode.js
javascript
const url = "https://api-connect.zay.me/merchants/v1/webhook_endpoints";
const body = JSON.stringify({
webhook_endpoint: {
url: "https://merchant.example.com/zay-webhooks",
description: "Production endpoint",
},
});
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"zay-api-key": ZAY_API_KEY,
"Idempotency-Key": `"${crypto.randomUUID()}"`,
...merchantHeaders("POST", url, ZAY_PRIVATE_KEY, body),
},
body,
});
const { webhook_endpoint: endpoint } = await res.json();
// endpoint.signing_secret — store immediately, you won't see it againResponse
json
{
"webhook_endpoint": {
"id": "11111111-1111-7000-8000-000000000001",
"url": "https://example.com/zay-webhooks",
"description": "Production endpoint",
"status": "enabled",
"created_at": "2025-12-05T12:05:49Z",
"updated_at": "2025-12-05T12:05:49Z",
"signing_secret": "whsec_DGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHRlc3Q="
}
}List webhook endpoints
Returns all webhook endpoints registered under your merchant account. There is no pagination — the cap is 10. signing_secret is never included.
GET /merchants/v1/webhook_endpoints
Errors:
401— invalid signature.429— rate limit exceeded.
Example
bash
curl "https://api-connect.zay.me/merchants/v1/webhook_endpoints" \
-H "zay-api-key: $ZAY_API_KEY" \
-H "zay-timestamp: $TS" \
-H "zay-signature: $SIG"Response
json
{
"webhook_endpoints": [
{
"id": "11111111-1111-7000-8000-000000000001",
"url": "https://example.com/zay-webhooks",
"description": "Production endpoint",
"status": "enabled",
"created_at": "2025-12-05T12:05:49Z",
"updated_at": "2025-12-05T12:05:49Z"
},
{
"id": "11111111-1111-7000-8000-000000000002",
"url": "https://example.com/zay-webhooks/backup",
"description": "Backup endpoint",
"status": "enabled",
"created_at": "2025-12-05T12:05:49Z",
"updated_at": "2025-12-05T12:05:49Z"
}
]
}Retrieve webhook endpoint
Returns a single webhook endpoint by id. signing_secret is not included.
GET /merchants/v1/webhook_endpoints/{id}
Parameters
| Parameter | Type | Description |
|---|---|---|
idrequired | string (uuid), path | Webhook endpoint ID |
Errors:
401— invalid signature.404— endpoint does not exist under this merchant.429— rate limit exceeded.
Example
bash
curl "https://api-connect.zay.me/merchants/v1/webhook_endpoints/0192f5a4-7c48-4fe9-b957-738bd2e04adc" \
-H "zay-api-key: $ZAY_API_KEY" \
-H "zay-timestamp: $TS" \
-H "zay-signature: $SIG"Update webhook endpoint
Update one or more of url, description and status. signing_secret cannot be changed — to rotate a secret, delete the endpoint and register a new one.
PATCH /merchants/v1/webhook_endpoints/{id}
Parameters
| Parameter | Type | Description |
|---|---|---|
idrequired | string (uuid), path | Webhook endpoint ID |
Request body
| Field | Type | Description |
|---|---|---|
webhook_endpointrequired | object | |
webhook_endpoint.url | string (uri) | |
webhook_endpoint.description | string | null | |
webhook_endpoint.status | string | Endpoint status. disabled is set either manually by the merchant or automatically when sustained delivery failures trip Zay's autodisable policy. One of enabled, disabled. |
Errors:
401— invalid signature.404— endpoint not found.422— the URL fails validation.429— rate limit exceeded.
Example — re-enable a disabled endpoint
bash
curl -X PATCH "https://api-connect.zay.me/merchants/v1/webhook_endpoints/0192f5a4-7c48-4fe9-b957-738bd2e04adc" \
-H "Content-Type: application/json" \
-H "zay-api-key: $ZAY_API_KEY" \
-H "zay-timestamp: $TS" \
-H "zay-signature: $SIG" \
-d '{ "webhook_endpoint": { "status": "enabled" } }'Response
json
{
"webhook_endpoint": {
"id": "11111111-1111-7000-8000-000000000001",
"url": "https://example.com/zay-webhooks",
"description": "Migrated to v2 receiver",
"status": "disabled",
"created_at": "2025-12-05T12:05:49Z",
"updated_at": "2025-12-05T12:05:49Z"
}
}Delete webhook endpoint
Delete a webhook endpoint. Deliveries still scheduled for it are dropped — there is no graceful drain.
DELETE /merchants/v1/webhook_endpoints/{id}
Parameters
| Parameter | Type | Description |
|---|---|---|
idrequired | string (uuid), path | Webhook endpoint ID |
Returns 204 No Content.
Errors:
401— invalid signature.404— endpoint not found.429— rate limit exceeded.
Example
bash
curl -X DELETE "https://api-connect.zay.me/merchants/v1/webhook_endpoints/0192f5a4-7c48-4fe9-b957-738bd2e04adc" \
-H "zay-api-key: $ZAY_API_KEY" \
-H "zay-timestamp: $TS" \
-H "zay-signature: $SIG"Trigger test delivery
Send a synthetic webhook_endpoint.test event to this endpoint through the full signed-delivery pipeline. Useful right after registering or re-enabling an endpoint, to confirm your receiver works before real events arrive.
POST /merchants/v1/webhook_endpoints/{id}/test_delivery
Parameters
| Parameter | Type | Description |
|---|---|---|
idrequired | string (uuid), path | Webhook endpoint ID |
- The endpoint must be
enabled. A test delivery to adisabledendpoint is accepted with202but never sent. - The
202response carries the delivery'sid. The same value arrives as thewebhook-idheader of the POST your receiver gets, so you can match the two in your logs.
Returns 202 Accepted with the queued webhook_delivery. A few seconds later your endpoint receives the event itself, in the format described in the Event catalog.
Errors:
401— invalid signature.404— endpoint not found.429— rate limit exceeded.
Example
bash
curl -X POST "https://api-connect.zay.me/merchants/v1/webhook_endpoints/0192f5a4-7c48-4fe9-b957-738bd2e04adc/test_delivery" \
-H "zay-api-key: $ZAY_API_KEY" \
-H "zay-timestamp: $TS" \
-H "zay-signature: $SIG"Response
json
{
"webhook_delivery": {
"id": "22222222-2222-7000-8000-000000000001",
"type": "webhook_endpoint.test",
"occurred_at": "2025-12-05T12:05:49Z",
"data": {
"webhook_endpoint_id": "11111111-1111-7000-8000-000000000001",
"message": "This is a test event from Zay. If you received this, your endpoint is configured correctly."
}
}
}