API reference
Events
Every state change as an immutable, ordered event. The same envelope is delivered by webhook and served here.
GET /v1/event-types
Event types an endpoint can subscribe to
Every webhook event type, with when it is sent and whether it is final. Requires events:read or webhooks:manage.
Responses
- 200
{data: [{type, when, terminal}], total}.
Request
curl -X GET "$KULPAY_URL/v1/event-types" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"terminal": true,
"type": "payment.succeeded",
"when": "The payment settled and the money is captured."
},
{
"terminal": true,
"type": "payment.failed",
"when": "The payment was declined or could not be completed."
}
],
"total": 2
}
GET /v1/events
List events, or catch up after an event
Two modes.
Browse (no since): newest first, filtered by type, aggregate_id and created[gte|lte], paged with starting_after.
Catch up (since=<event id>, or since= for the beginning): every later event, oldest first. Use it to recover anything a webhook endpoint missed. The newest 2 seconds are held back so no event is ever skipped by a transaction still committing.
Events of another account are never returned. Requires events:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
since | query | string | Event id to continue after (catch-up mode). |
type | query | string | Event type; repeat or comma-separate for several. |
aggregate_id | query | string | |
created[gte] | query | string | RFC 3339 or Unix seconds. |
created[lte] | query | string | RFC 3339 or Unix seconds. |
starting_after | query | string | |
limit | query | integer | |
direction | query | string | inbound, outbound or internal (PSP-7.1). One of: inbound, outbound, internal |
pattern | query | string | The interaction pattern (PSP-7.1). One of: outbound_sync, outbound_async, outbound_notification, inbound_request, inbound_callback, inbound_file, outbound_file, status_query |
Responses
- 200 A page of events.
- 400 Invalid parameter (
invalid_request). - 403 Missing
events:read(insufficient_scope) or a key not bound to an account (account_required). - 404 Unknown cursor (
not_found). - 503 No event store in this deployment (
service_unavailable).
Request
curl -X GET "$KULPAY_URL/v1/events" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"actor": {
"id": "system",
"type": "system"
},
"aggregate_id": "3KWPm4BeofWfvUSB1Cnv95jEq01",
"aggregate_type": "payment",
"created_at": "2026-10-10T22:15:06.718012Z",
"data": {
"attempt": 1,
"detail": {
"endpoints": 0,
"event_id": "3KWPm3lMXjY9xox1gUUd8AoRJNc",
"event_type": "payment.succeeded"
},
"duration_ms": 0,
"ended_at": "2026-10-10T22:15:06.71700216Z",
"id": "3KWPm9Zsu3GgFBtCrrl83wZo7ML",
"outcome": "skipped",
"payment_id": "3KWPm4BeofWfvUSB1Cnv95jEq01",
"reason_code": "no_endpoints",
"stage": "notify",
"started_at": "2026-10-10T22:15:06.71700216Z",
"transaction_type": "SALE"
},
"direction": "outbound",
"id": "3KWPm6bJc7T4d0U43IOgK0pT0vS",
"mode": "test",
"occurred_at": "2026-10-10T22:15:06.718012Z",
"pattern": "outbound_sync",
"recorded_at": "2026-10-10T22:15:06.718012Z",
"schema_version": 1,
"sequence": 10,
"tenant_id": "3KWPm886QpMrxawEJqc4BES8NmT",
"type": "transaction.stage.notify.skipped"
}
],
"has_more": true,
"next_cursor": "3KWPm6bJc7T4d0U43IOgK0pT0vS"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "use RFC 3339 (2026-10-08T10:00:00Z) or Unix seconds",
"error": {
"code": "invalid_request",
"message": "use RFC 3339 (2026-10-08T10:00:00Z) or Unix seconds"
},
"status": 400,
"title": "Invalid created[gte]",
"type": "about:blank"
}
403 Error · insufficient_scope application/problem+json
{
"code": "insufficient_scope",
"detail": "missing required scope: events:read",
"error": {
"code": "insufficient_scope",
"message": "missing required scope: events:read",
"missing_scope": "events:read"
},
"missing_scope": "events:read",
"status": 403,
"title": "Forbidden",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "No event with that id exists for this account.",
"error": {
"code": "not_found",
"message": "No event with that id exists for this account."
},
"status": 404,
"title": "Unknown cursor",
"type": "about:blank"
}
503 Error · service_unavailable application/problem+json
{
"code": "service_unavailable",
"detail": "No event store in this deployment (service_unavailable).",
"error": {
"code": "service_unavailable",
"message": "No event store in this deployment (service_unavailable)."
},
"status": 503,
"title": "Service Unavailable",
"type": "about:blank"
}
GET /v1/events/{id}
Get one event
An event of another account or mode is reported as not found. Requires events:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The event.
- 404 No such event (
not_found).
Request
curl -X GET "$KULPAY_URL/v1/events/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"actor": {
"id": "535163a20e6ce0cae10b379d29134a7f",
"ip": "203.0.113.10",
"type": "api_key",
"user_agent": "node"
},
"aggregate_id": "3KWPcChMXO2voAp3Vdqu6ZLEMQv",
"aggregate_type": "payment",
"chain_seq": 320,
"correlation_id": "b1768c81bbe474585ca1474a",
"created_at": "2026-10-10T22:13:47.674035Z",
"data": {
"attempt": 1,
"duration_ms": 3,
"ended_at": "2026-10-10T22:13:47.643753809Z",
"id": "3KWPc7hVNdQdhtS532Wttp6sORT",
"outcome": "passed",
"payment_id": "3KWPcChMXO2voAp3Vdqu6ZLEMQv",
"stage": "enrich_validate",
"started_at": "2026-10-10T22:13:47.640372197Z",
"transaction_type": "SALE"
},
"direction": "inbound",
"hash": "504e00e8a3ab657f3dbbbbdc52542302f9e54b7f03142cb24757999cff2a10bc",
"id": "3KWPcDFQR4Iy59TZIVI6Hf51z3k",
"idempotency_key": "ux-1791670427346",
"mode": "test",
"occurred_at": "2026-10-10T22:13:47.674035Z",
"pattern": "inbound_request",
"prev_hash": "d88f24bf1fbd0b40ce5736661f2e46f09aee5faf699c836c1322b34cc46a324d",
"recorded_at": "2026-10-10T22:13:47.674035Z",
"region": "mz",
"schema_version": 1,
"sequence": 3,
"tenant_id": "OcThktmhLjl2JAewdDBNXh2u0lT",
"type": "transaction.stage.enrich_validate.passed"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"error": {
"code": "not_found",
"message": ""
},
"status": 404,
"title": "No such event",
"type": "about:blank"
}
POST /v1/webhook-deliveries/{id}/replay
Replay one delivery
Queues the delivery again with its frozen signed body. Requires webhooks:replay.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 202
{"queued": n}: fresh deliveries on the same schedule, each event once. - 404 Not this account's delivery.
not_found - 409
endpoint_disabled.
Request
curl -X POST "$KULPAY_URL/v1/webhook-deliveries/{id}/replay" \
-H "Authorization: Bearer $KULPAY_KEY"
202 Response application/json
{
"queued": 1
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "webhook endpoint or delivery not found",
"error": {
"code": "not_found",
"message": "webhook endpoint or delivery not found"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · endpoint_disabled application/problem+json
{
"code": "endpoint_disabled",
"detail": "the endpoint is disabled: enable it before replaying",
"error": {
"code": "endpoint_disabled",
"message": "the endpoint is disabled: enable it before replaying"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
GET /v1/webhook-endpoints
The account's webhook endpoints
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/webhook-endpoints" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"contacts": [
"dev@example.co.mz"
],
"created_at": "2026-07-02T09:15:00Z",
"ed25519": true,
"enabled": true,
"event_types": [
"payment.succeeded",
"payment.failed",
"refund.succeeded"
],
"id": "3K9fVrOSGHIvfS0z8BcX9qUgBf3",
"mode": "live",
"status": "active",
"url": "https://loja.example.co.mz/webhooks/kulpay"
},
{
"created_at": "2026-06-20T16:40:12Z",
"disabled_at": "2026-10-01T08:00:00Z",
"disabled_reason": "paused",
"ed25519": false,
"enabled": false,
"event_types": [
"*"
],
"id": "3K4nekciqFMaDnnsmlT6qBJ8alZ",
"mode": "test",
"status": "paused",
"url": "https://staging.loja.example.co.mz/webhooks/kulpay"
}
],
"total": 2
}
POST /v1/webhook-endpoints
Subscribe an endpoint to events
Registers an endpoint for the key's mode, subscribed to event types: exact names, families (payout.*) or everything (omit, or *) (PSP-7.5). Deliveries are signed and follow the 24-hour schedule and dead-letter queue of all webhooks. The signing secret is returned once. Records webhook_endpoint.subscribed. Requires webhooks:manage. Subscribe to partner.alert (or receive everything) to get KulPay's operational alerts on this endpoint too, besides the email to your contacts (P-2): an API key or client certificate expiring, a key found in public, a statement not received.
Request body application/json required
| Field | Type | Description |
|---|---|---|
contacts | string (email)[] | |
event_types | string[] | |
url required | string |
Responses
- 201 The endpoint.
- 400 An unknown event type or a bad address.
invalid_request
Request
curl -X POST "$KULPAY_URL/v1/webhook-endpoints" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"contacts": [
"ops@example.co.mz"
],
"event_types": [
"payment.succeeded"
],
"url": "https://merchant.example.co.mz/kulpay/return"
}'
201 Response application/json
{
"contacts": [
"string"
],
"created_at": "2026-10-10T09:30:00Z",
"disabled_reason": "paused",
"enabled": true,
"event_types": [
"payment.succeeded"
],
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"mode": "test",
"secret": "string",
"status": "active",
"url": "https://merchant.example.co.mz/kulpay/return"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "\"payment.completed\" is not a webhook event type (GET /v1/event-types lists them)",
"error": {
"code": "invalid_request",
"message": "\"payment.completed\" is not a webhook event type (GET /v1/event-types lists them)"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/webhook-endpoints/{id}
A webhook endpoint
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The endpoint.
- 404 Not found.
not_found
Request
curl -X GET "$KULPAY_URL/v1/webhook-endpoints/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"contacts": [
"ops@example.co.mz"
],
"created_at": "2026-10-10T09:30:00Z",
"disabled_reason": "paused",
"enabled": true,
"event_types": [
"payment.succeeded",
"bill_payment.succeeded"
],
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"mode": "test",
"secret": "string",
"status": "active",
"url": "https://loja.example.co.mz/webhooks/kulpay"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "webhook endpoint or delivery not found",
"error": {
"code": "not_found",
"message": "webhook endpoint or delivery not found"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
PATCH /v1/webhook-endpoints/{id}
Change an endpoint's subscription
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
event_types | string[] |
Responses
- 200 The endpoint.
- 400 An unknown event type.
invalid_request
Request
curl -X PATCH "$KULPAY_URL/v1/webhook-endpoints/{id}" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_types": [
"payment.succeeded"
]
}'
200 Response application/json
{
"contacts": [
"string"
],
"created_at": "2026-10-10T09:30:00Z",
"disabled_reason": "paused",
"enabled": true,
"event_types": [
"payment.succeeded"
],
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"mode": "test",
"secret": "string",
"status": "active",
"url": "https://merchant.example.co.mz/kulpay/return"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "\"payment.completed\" is not a webhook event type (GET /v1/event-types lists them)",
"error": {
"code": "invalid_request",
"message": "\"payment.completed\" is not a webhook event type (GET /v1/event-types lists them)"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
DELETE /v1/webhook-endpoints/{id}
Remove a webhook endpoint
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 204 Removed.
Request
curl -X DELETE "$KULPAY_URL/v1/webhook-endpoints/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
PUT /v1/webhook-endpoints/{id}/contacts
Who is alerted about an endpoint
Emails told when a delivery is dead-lettered or the endpoint disabled; without any, a PSP's technical contact. Requires webhooks:manage.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
emails | string (email)[] |
Responses
- 204 Saved.
- 400 Not an email address.
invalid_request
Request
curl -X PUT "$KULPAY_URL/v1/webhook-endpoints/{id}/contacts" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": [
"ops@example.co.mz"
]
}'
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "\"suporte.example.co.mz\" is not an email address",
"error": {
"code": "invalid_request",
"message": "\"suporte.example.co.mz\" is not an email address"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/webhook-endpoints/{id}/deliveries
An endpoint's delivery history
The last 30 days, newest first: status, attempts, response code, latency, next attempt.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | |
limit | query | integer |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/webhook-endpoints/{id}/deliveries" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"attempts": 2,
"created_at": "2026-10-11T09:12:03Z",
"duration_ms": 10004,
"event_id": "3KP9ygtnmJTauCTmOray77nyB4C",
"event_type": "payment.failed",
"id": "3KwftWaywR8E0sQgebUy55IidZY",
"last_error": "HTTP 503",
"next_attempt_at": "2026-10-11T09:17:03Z",
"response_code": 503,
"status": "pending"
},
{
"attempts": 1,
"created_at": "2026-10-11T08:47:21Z",
"delivered_at": "2026-10-11T08:47:21Z",
"duration_ms": 184,
"event_id": "3K15E3LnuKxuG278PTGllbABGwy",
"event_type": "payment.succeeded",
"id": "3K7dZCinKGJgHK9WSjj79t6IWaX",
"response_code": 200,
"status": "delivered"
}
],
"total": 2
}
POST /v1/webhook-endpoints/{id}/enable
Enable a disabled endpoint
Clears the disable reason (gone, dead_letters); records webhook_endpoint.enabled. Requires webhooks:manage.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The endpoint, enabled: the same representation as
GET /v1/webhook-endpoints/{id}. - 404 Not this account's endpoint.
not_found
Request
curl -X POST "$KULPAY_URL/v1/webhook-endpoints/{id}/enable" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"contacts": [
"ops@example.co.mz"
],
"created_at": "2026-10-10T09:30:00Z",
"enabled": true,
"event_types": [
"payment.succeeded",
"bill_payment.succeeded"
],
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"mode": "test",
"status": "active",
"url": "https://loja.example.co.mz/webhooks/kulpay"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "webhook endpoint or delivery not found",
"error": {
"code": "not_found",
"message": "webhook endpoint or delivery not found"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/webhook-endpoints/{id}/pause
Pause an endpoint
Nothing is sent while paused; resume with /enable, then replay what it missed from an event. Records webhook_endpoint.paused.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The endpoint.
Request
curl -X POST "$KULPAY_URL/v1/webhook-endpoints/{id}/pause" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"contacts": [
"string"
],
"created_at": "2026-10-10T09:30:00Z",
"disabled_reason": "paused",
"enabled": true,
"event_types": [
"payment.succeeded"
],
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"mode": "test",
"secret": "string",
"status": "active",
"url": "https://merchant.example.co.mz/kulpay/return"
}
POST /v1/webhook-endpoints/{id}/replay
Replay an endpoint's deliveries
Webhooks are tried immediately, then 1 min, 5 min, 15 min, 1 h, 3 h, 8 h and 24 h after the first attempt (10 s timeout each; any 2xx is success). The last failure dead-letters the delivery and alerts the endpoint's contacts; a 410 disables the endpoint, as do three days running with dead-lettered deliveries (PSP-9.5). This queues again every event from since on (default the last 30 days), or the dead-lettered ones (status=exhausted). Records delivery.replayed. Requires webhooks:replay.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | |
since | query | string | An event id: replay it and everything after. |
status | query | string | One of: exhausted, delivered |
Responses
- 202
{"queued": n}: fresh deliveries on the same schedule, each event once. - 404 Not this account's endpoint.
not_found - 409
endpoint_disabled: enable it first.
Request
curl -X POST "$KULPAY_URL/v1/webhook-endpoints/{id}/replay" \
-H "Authorization: Bearer $KULPAY_KEY"
202 Response application/json
{
"queued": 14
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "webhook endpoint or delivery not found",
"error": {
"code": "not_found",
"message": "webhook endpoint or delivery not found"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · endpoint_disabled application/problem+json
{
"code": "endpoint_disabled",
"detail": "the endpoint is disabled: enable it before replaying",
"error": {
"code": "endpoint_disabled",
"message": "the endpoint is disabled: enable it before replaying"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
POST /v1/webhook-endpoints/{id}/roll-secret
Roll an endpoint's signing secret
A new secret (shown once); the old one keeps signing until previous_secret_expires_at (default 24 h, at most 7 days), so each delivery meanwhile carries a v1= per secret in KulPay-Signature (PSP-8.8). Records credential.webhook_secret.rotated. See the "Rotate a webhook signing secret" runbook.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json
| Field | Type | Description |
|---|---|---|
grace | string |
Responses
- 200
{endpoint (with secret), previous_secret_expires_at}. - 400 A grace under 1 minute or over 7 days.
invalid_request
Request
curl -X POST "$KULPAY_URL/v1/webhook-endpoints/{id}/roll-secret" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"grace": "24h"
}'
200 Response application/json
{
"endpoint": {
"contacts": [
"dev@example.co.mz"
],
"created_at": "2026-05-04T14:22:10Z",
"ed25519": false,
"enabled": true,
"event_types": [
"payment.succeeded",
"payment.failed"
],
"id": "3K4lgJuN0uZKZkh8qikNmN22sbx",
"mode": "live",
"previous_secret_expires_at": "2026-10-12T10:30:00Z",
"secret": "whsec_…",
"status": "active",
"url": "https://loja.example.co.mz/webhooks/kulpay"
},
"previous_secret_expires_at": "2026-10-12T10:30:00Z"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the grace period is between 1 minute and 7 days",
"error": {
"code": "invalid_request",
"message": "the grace period is between 1 minute and 7 days"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
PUT /v1/webhook-endpoints/{id}/schema-versions
Pin event types to schema versions
Events are recorded at the latest schema version of their type (each envelope carries schema_version). Pin an event type to a version and this endpoint receives it in that version, converted by KulPay, for as long as the version is supported; pinning the current version shields you from the next one. An event the pinned version cannot express (a status it did not have) arrives in the current version, its envelope saying which. When a version is scheduled to retire, the endpoint's contacts are emailed 90 days before (webhook_endpoint.schema_retiring, stage notice) and from the date the type arrives in its current version (stage retired). GET /v1/webhook-schema-versions lists the versions, which can be pinned and when they retire. Replaces the endpoint's pins; {} unpins every type. Records webhook_endpoint.schema_pinned. Requires webhooks:manage.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
schema_versions required | object | Event types pinned to a schema version (F3-18). |
Responses
- 200 The endpoint.
- 400 The request body or query failed validation. The response body lists each field-level issue.
invalid_request - 404 The named resource was not found.
not_found
Request
curl -X PUT "$KULPAY_URL/v1/webhook-endpoints/{id}/schema-versions" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"schema_versions": {
"bill_invoice.paid": 1,
"payment.succeeded": 2
}
}'
200 Response application/json
{
"contacts": [
"string"
],
"created_at": "2026-10-10T09:30:00Z",
"disabled_reason": "paused",
"enabled": true,
"event_types": [
"payment.succeeded"
],
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"mode": "test",
"status": "active",
"url": "https://merchant.example.co.mz/kulpay/return"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "The request body failed validation.",
"error": {
"code": "invalid_request",
"errors": [
{
"field": "money.amount",
"message": "must be a positive decimal with at most 2 places"
}
],
"message": "The request body failed validation."
},
"errors": [
{
"field": "money.amount",
"message": "must be a positive decimal with at most 2 places"
}
],
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "No payment with this id.",
"error": {
"code": "not_found",
"message": "No payment with this id."
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
PUT /v1/webhook-endpoints/{id}/signing
Ed25519 signatures
With ed25519: true deliveries also carry v1e=<base64 Ed25519 signature of "<t>.<body>">, verifiable with the region's key from /.well-known/kulpay-webhook-keys.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
ed25519 | boolean |
Responses
- 200 The endpoint.
Request
curl -X PUT "$KULPAY_URL/v1/webhook-endpoints/{id}/signing" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"ed25519": true
}'
200 Response application/json
{
"contacts": [
"dev@example.co.mz"
],
"created_at": "2026-05-04T14:22:10Z",
"ed25519": true,
"enabled": true,
"event_types": [
"payment.succeeded",
"payment.failed"
],
"id": "3K4lgJuN0uZKZkh8qikNmN22sbx",
"mode": "live",
"status": "active",
"url": "https://loja.example.co.mz/webhooks/kulpay"
}
POST /v1/webhook-endpoints/{id}/test
Send a test event
A signed webhook_endpoint.tested event to this endpoint only, through the normal delivery path.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 202
{delivery_id, event_id, event_type}. - 409
endpoint_disabled.
Request
curl -X POST "$KULPAY_URL/v1/webhook-endpoints/{id}/test" \
-H "Authorization: Bearer $KULPAY_KEY"
202 Response application/json
{
"delivery_id": "3K7dZCinKGJgHK9WSjj79t6IWaX",
"event_id": "3K15E3LnuKxuG278PTGllbABGwy",
"event_type": "webhook_endpoint.tested"
}
409 Error · endpoint_disabled application/problem+json
{
"code": "endpoint_disabled",
"detail": "the endpoint is disabled: enable it before replaying",
"error": {
"code": "endpoint_disabled",
"message": "the endpoint is disabled: enable it before replaying"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
GET /v1/webhook-schema-versions
Webhook schema versions
Events are recorded at the latest schema version of their type (each envelope carries schema_version). Pin an event type to a version and this endpoint receives it in that version, converted by KulPay, for as long as the version is supported; pinning the current version shields you from the next one. An event the pinned version cannot express (a status it did not have) arrives in the current version, its envelope saying which. When a version is scheduled to retire, the endpoint's contacts are emailed 90 days before (webhook_endpoint.schema_retiring, stage notice) and from the date the type arrives in its current version (stage retired). GET /v1/webhook-schema-versions lists the versions, which can be pinned and when they retire. Requires webhooks:manage or admin:read.
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/webhook-schema-versions" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"current": true,
"event_type": "payment.succeeded",
"pinnable": true,
"retires_at": "2026-10-10T09:30:00Z",
"version": 1
}
],
"total": 12
}
KulPay