Docs

API reference / Webhooks

Create a webhook endpoint

POST/v1/webhook-endpoints

API key requiredSupports Idempotency-Key

Registers an HTTPS URL that receives signed events. The endpoint lives in the environment of the key (a sandbox key creates a sandbox endpoint that only receives sandbox events). The response contains the signing secret whsec_…: it is shown only once. Each delivery carries a Tilt-Signature: t=<timestamp>,v1=<hmac> header, where hmac = HMAC_SHA256(secret, "<t>.<raw body>"). Reply with a 2xx status to acknowledge; failed deliveries are retried up to 7 times (1 min, 5 min, 30 min, 2 h, 6 h, 24 h) and the endpoint is disabled after 20 failed attempts in a row. At most 20 endpoints per project. Accepts Idempotency-Key.

Authentication#

Send a secret API key as Authorization: Bearer <key>. Both tp_test_… and tp_live_… keys work; the key decides which environment you read and write. See Authentication.

Parameters#

Headers

Header parameters
NameTypeDescription
Idempotency-KeystringIdempotency key (1 to 255 characters, UUID v4 recommended). Responses are replayed for 24 h.

See Idempotency.

Request body#

  • urlstringrequired

    Public HTTPS URL. In sandbox only, http://localhost is accepted for local development.

    min length: 1 · max length: 2048

    Example "https://example.com/webhooks/tilt-pay"

  • descriptionstring | null

    Free-form label.

    max length: 200

    Example "Production backend"

  • event_typesarray of stringrequired

    Event types delivered to this endpoint, or ["*"] for all of them. Duplicates are removed.

    One ofuser.createduser.suspendeduser.unsuspendedkyc.approvedkyc.rejectedtransaction.receivedtransaction.completedwithdrawal.completedwithdrawal.failed*

  • enabledboolean

    Start enabled (default).

    default: true

Example request body
{
  "url": "https://example.com/webhooks/tilt-pay",
  "description": "Production backend",
  "event_types": [
    "transaction.completed",
    "kyc.approved"
  ]
}

Responses#

201Endpoint created; includes the signing secret (only time it is returned).

Headers

  • X-Request-Id Request identifier.
  • X-RateLimit-Limit Quota of the current window.
  • X-RateLimit-Remaining Requests remaining in the window.
  • X-RateLimit-Reset Seconds before the quota renews.

Body

  • idstringrequired

    Opaque identifier.

    Example "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"

  • objectstringrequired

    always "webhook_endpoint"

    Example "webhook_endpoint"

  • urlstringrequired

    HTTPS URL that receives the events.

    Example "https://example.com/webhooks/tilt-pay"

  • descriptionstring | nullrequired

    Free-form label.

    Example "Production backend"

  • event_typesarray of stringrequired

    Event types delivered to this endpoint, or ["*"] for all of them. Duplicates are removed.

    One ofuser.createduser.suspendeduser.unsuspendedkyc.approvedkyc.rejectedtransaction.receivedtransaction.completedwithdrawal.completedwithdrawal.failed*

  • enabledbooleanrequired

    The endpoint receives events.

    Example true

  • disabled_reasonstring | nullrequired

    manual when you disabled it, consecutive_failures when Tilt Pay disabled it after 20 failed attempts in a row.

    One ofmanualconsecutive_failures

  • consecutive_failuresintegerrequired

    Failed attempts in a row (reset by a success).

    Example 0

  • secret_last4stringrequired

    Last four characters of the current signing secret.

    Example "x9Yz"

  • previous_secret_expires_atstring | nullrequired

    During a rotation, until when the previous secret also signs events.

    format: date-time

    Example "2026-10-08T09:30:00.000Z"

  • livemodebooleanrequired

    true for a live endpoint, false for a sandbox endpoint.

    Example false

  • created_atstringrequired

    Creation date.

    format: date-time

    Example "2026-10-08T09:30:00.000Z"

  • updated_atstringrequired

    Last update.

    format: date-time

    Example "2026-10-08T09:30:00.000Z"

  • secretstringrequired

    Signing secret whsec_…. Shown only in this response: store it now, it cannot be retrieved later.

    Example "whsec_mQ4n7bV2xKc9LrT1uYe6AzHs0PdWfJgB"

Example 201 response
{
  "id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21",
  "object": "webhook_endpoint",
  "url": "https://example.com/webhooks/tilt-pay",
  "description": "Production backend",
  "event_types": [
    "*"
  ],
  "enabled": true,
  "disabled_reason": null,
  "consecutive_failures": 0,
  "secret_last4": "x9Yz",
  "previous_secret_expires_at": null,
  "livemode": false,
  "created_at": "2026-10-08T09:30:00.000Z",
  "updated_at": "2026-10-08T09:30:00.000Z",
  "secret": "whsec_mQ4n7bV2xKc9LrT1uYe6AzHs0PdWfJgB"
}
400Invalid request (malformed JSON, unknown cursor, invalid Idempotency-Key header).

Example: invalid_cursor

Example 400 response
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_cursor",
    "message": "starting_after: unknown cursor",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}
401Missing, invalid or revoked API key.

Example: missing_api_key

Example 401 response
{
  "error": {
    "type": "authentication_error",
    "code": "missing_api_key",
    "message": "Provide your API key as \"Authorization: Bearer <key>\"",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}

Example: invalid_api_key

Example 401 response
{
  "error": {
    "type": "authentication_error",
    "code": "invalid_api_key",
    "message": "The API key is invalid",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}

Example: api_key_revoked

Example 401 response
{
  "error": {
    "type": "authentication_error",
    "code": "api_key_revoked",
    "message": "This API key has been revoked",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}
403Organization suspended, live access not enabled, or sandbox-only route.

Example: organization_suspended

Example 403 response
{
  "error": {
    "type": "permission_error",
    "code": "organization_suspended",
    "message": "Your organization is suspended. Contact Tilt Pay support.",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}

Example: live_not_enabled

Example 403 response
{
  "error": {
    "type": "permission_error",
    "code": "live_not_enabled",
    "message": "Live access has not been enabled for your organization yet",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}

Example: sandbox_only

Example 403 response
{
  "error": {
    "type": "permission_error",
    "code": "sandbox_only",
    "message": "This route is only available with a sandbox API key",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}
409Conflict with the state of the resource, or the original idempotent request is still in progress.

Example: user_already_exists

Example 409 response
{
  "error": {
    "type": "invalid_request_error",
    "code": "user_already_exists",
    "message": "A user with this phone number already exists",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}
422Invalid parameter, or Idempotency-Key reused with a different body.

Example: validation_error

Example 422 response
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_error",
    "message": "phone: has an invalid format",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}

Example: idempotency_key_reuse

Example 422 response
{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_key_reuse",
    "message": "This Idempotency-Key was already used with a different request",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}
429Rate limit exceeded. Retry after the Retry-After delay (seconds).

Example: rate_limit_exceeded

Example 429 response
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Slow down and retry after the delay in Retry-After.",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}
500Internal error. Retry; give request_id to support if it persists.

Example: internal_error

Example 500 response
{
  "error": {
    "type": "api_error",
    "code": "internal_error",
    "message": "Internal server error",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}

Headers on every response

  • X-Request-Id Identifier of the request, also present in error bodies. Give it to support.
  • X-API-Version Version of the API that answered.
  • X-RateLimit-Limit / -Remaining / -Reset Quota of your key for the current window. See Rate limits.

Errors#

Errors share one format, described in Errors. Branch your code on error.code.

Errors this route can return
StatusCodeMeaning
400invalid_cursorInvalid request (malformed JSON, unknown cursor, invalid Idempotency-Key header).
401missing_api_keyMissing, invalid or revoked API key.
401invalid_api_keyMissing, invalid or revoked API key.
401api_key_revokedMissing, invalid or revoked API key.
403organization_suspendedOrganization suspended, live access not enabled, or sandbox-only route.
403live_not_enabledOrganization suspended, live access not enabled, or sandbox-only route.
403sandbox_onlyOrganization suspended, live access not enabled, or sandbox-only route.
409user_already_existsConflict with the state of the resource, or the original idempotent request is still in progress.
409webhook_endpoint_existsAn endpoint with this URL already exists in this environment.
409webhook_endpoint_limitThe project already has 20 webhook endpoints.
422validation_errorInvalid parameter, or Idempotency-Key reused with a different body.
422idempotency_key_reuseInvalid parameter, or Idempotency-Key reused with a different body.
422invalid_webhook_urlThe URL is not acceptable: https is required (http://localhost is accepted in sandbox, outside production), private or internal addresses, credentials and sensitive ports are refused.
429rate_limit_exceededRate limit exceeded. Retry after the Retry-After delay (seconds).
500internal_errorInternal error. Retry; give request_id to support if it persists.