Webhooks API Reference
All webhook endpoints are mounted at /v1/webhooks.
Endpoints summary
| Method | Path | Description |
|---|---|---|
GET | /v1/webhooks | Get current webhook configuration |
PUT | /v1/webhooks | Create or update webhook configuration |
POST | /v1/webhooks/test | Send a test webhook to the configured URL |
GET | /v1/webhooks/secret | Reveal the current signing secret |
GET | /v1/webhooks/deliveries | List delivery jobs (paginated) |
GET | /v1/webhooks/deliveries/{job_id} | Get delivery job detail with all attempt logs |
POST | /v1/webhooks/deliveries/{job_id}/retry | Re-queue a failed delivery for immediate retry |
GET /v1/webhooks
Returns the current webhook configuration for the authenticated user.
Response (200 OK):
{
"success": true,
"data": {
"url": "https://yourserver.com/tcf-webhook",
"is_active": true,
"has_secret": true,
"events": ["payment.completed", "payment.expired", "withdrawal.completed"],
"updated_at": "2026-05-15T09:00:00Z"
}
}
has_secretis a boolean — the secret value is never returned via the API.
PUT /v1/webhooks
Create or update the webhook configuration. This is an upsert — if no config exists it is created; if one exists it is updated.
Request body:
| Field | Type | Required | Constraints |
|---|---|---|---|
url | string | Yes | Must start with https:// |
secret | string | No | If omitted, a secret is auto-generated. Provide to set your own. |
is_active | boolean | No | Default: true |
events | array of strings | No | Event type strings or ["*"] for all. Default: ["*"] |
Example request:
{
"url": "https://yourserver.com/tcf-webhook",
"secret": "my-strong-secret-at-least-32-chars",
"is_active": true,
"events": ["payment.completed", "payment.expired_selection", "withdrawal.completed"]
}Response (200 OK):
{
"success": true,
"data": {
"url": "https://yourserver.com/tcf-webhook",
"is_active": true,
"has_secret": true,
"events": ["payment.completed", "payment.expired_selection", "withdrawal.completed"],
"updated_at": "2026-05-16T12:00:00Z"
}
}Passing an empty string as
secretclears the signature requirement (deliveries are then sent unsigned — not recommended). Omittingsecretpreserves the existing secret, or auto-generates one on first setup.
Error codes:
| Code | Condition |
|---|---|
VALIDATION_ERROR | URL is not HTTPS / too long, events is empty, or the list contains an unknown event type |
GET /v1/webhooks/secret
Returns the raw webhook signing secret so you can configure HMAC verification on your server. The secret is stored encrypted and can be revealed at any time.
Response (200 OK):
{
"success": true,
"data": {
"secret": "whsec_..."
}
}You can also reveal the secret in the Client Portal → Dashboard → Webhooks → Reveal secret.
POST /v1/webhooks/test
Send a synthetic test webhook delivery to your configured URL. Useful for verifying your endpoint and signature verification code.
The test payload is shaped like a payment.completed event with synthetic (non-real) data.
Request body: none required
Response (200 OK):
{
"success": true,
"data": {
"delivered": true,
"status_code": 200,
"response_body": "OK",
"duration_ms": 145,
"payload_sent": {
"event": "payment.completed",
"timestamp": "2026-05-16T12:00:00.123456+00:00",
"data": { "payment_id": "test-00000000-0000-0000-0000-000000000000", "status": "COMPLETED", ... }
}
}
}The test delivery is signed with your configured secret (if any) and carries the same
X-TCF-Event/X-TCF-Delivery/X-TCF-Signatureheaders as real deliveries. No delivery job is recorded in your history.
Error codes:
| Code | Condition |
|---|---|
NOT_FOUND | No webhook configuration exists yet |
VALIDATION_ERROR | Webhook URL is not configured or inactive |
GET /v1/webhooks/deliveries
List webhook delivery jobs for the authenticated user, newest first.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter: pending, processing, failed, success, permanently_failed |
event | string | Filter by event type (e.g. payment.completed) |
page | integer | Default: 1 |
per_page | integer | Default: 20, max: 100 |
Response (200 OK):
{
"success": true,
"data": {
"total": 42,
"page": 1,
"page_size": 20,
"total_pages": 3,
"jobs": [
{
"job_id": "d1e2f3a4-...",
"event": "payment.completed",
"status": "success",
"attempt_count": 1,
"source_type": "payment",
"source_id": "9f1b2c3d-...",
"created_at": "2026-05-16T12:05:00Z",
"last_attempt_at": "2026-05-16T12:05:02Z",
"last_response_status": 200,
"next_attempt_at": null
}
]
}
}GET /v1/webhooks/deliveries/
Get full detail for a delivery job, including all attempt logs.
Response (200 OK):
{
"success": true,
"data": {
"job_id": "d1e2f3a4-...",
"event": "payment.completed",
"status": "success",
"attempt_count": 1,
"max_attempts": 10,
"source_type": "payment",
"source_id": "9f1b2c3d-...",
"payload": { "event": "payment.completed", ... },
"created_at": "2026-05-16T12:05:00Z",
"last_attempt_at": "2026-05-16T12:05:02Z",
"last_response_status": 200,
"next_attempt_at": null,
"logs": [
{
"id": "log-uuid",
"attempted_at": "2026-05-16T12:05:02Z",
"response_status": 200,
"response_body": "OK",
"error_message": null,
"success": true,
"duration_ms": 145
}
]
}
}POST /v1/webhooks/deliveries/{job_id}/retry
Manually re-queue a failed or permanently_failed delivery job for immediate retry. The attempt counter is reset so up to max_attempts fresh attempts are made. Jobs already success or currently processing are rejected.
Response (200 OK):
{
"success": true,
"data": {
"job_id": "d1e2f3a4-...",
"status": "pending"
}
}Error codes:
| Code | Condition |
|---|---|
NOT_FOUND | Job does not exist |
FORBIDDEN | Job belongs to another user |
CONFLICT | Job already succeeded or is currently processing |

