Webhooks

Titus sends webhook events whenever an entity is created, updated, or deleted. These events are the primary way to track key moments like a successful checkout or a business onboarding. For a full list of events, see the webhooks reference.

You can configure webhook endpoints via your platform dashboard. Each platform can create multiple webhook endpoints for different types of events. All webhook endpoints must be configured with https URLs that accept POST requests with JSON payloads.

Event Model

All events are sent as POST requests with a consistent structure:

  • id id of the webhook event itself

  • createdAt when the webhook event was created

  • type is usually in the format {entity_type}.{action}, where action is created, updated, deleted. Entity types are authorization, checkout, business_connection, customer, and refund.

  • Four events do not follow that shape. authorization.loan_application_denied and checkout.loan_application_denied are sent when a customer's pay at close application is declined; which one you receive depends on whether the application started from an authorization or a checkout. checkout.loan_application_under_review and checkout.loan_application_approved are sent when an application that started from a checkout goes to manual review and when it is approved. The payload on all four carries only the entity's id and a timestamp, and none of them changes the checkout's status, which stays requires-payment until the customer completes payment. Handle the denial events if you need to react to a decline, for example by falling back to another payment method.

  • checkout.payment_failed keeps the standard shape and carries the full checkout. It is sent, alongside checkout.updated, when a payment fails after it was accepted for processing — an ACH debit that is canceled or returned, for example — and the checkout returns to requires-payment so the customer can retry. It is not sent for a charge rejected at submission, nor for pay at close failures. The reason is in the checkout's lastPaymentError, which is cleared once a payment succeeds.

  • payload contains the full current state of the entity at the time of the event

{
  "type": "checkout.updated",
  "payload": {
     ...entity data
  }

}

Authenticating webhooks

Titus signs all webhook requests so you can verify their authenticity. Each request includes two headers:

  • x-webhook-signature: HMAC SHA-256 of ${timestamp}.${body}

  • x-webhook-timestamp: Timestamp of when the event was signed

Never use a webhook request body directly without first verifying the signature.

Example webhook endpoint

function generateWebhookSignature(secret: string, bodyString: string, timestamp: number) {
  return crypto.createHmac('sha256', secret).update(`${timestamp}.${bodyString}`).digest('hex');
}

export async function POST(request) {
  const body = await request.json();
  const signature = request.headers.get('x-webhook-signature');
  const timestamp = request.headers.get('x-webhook-timestamp');

  if (!signature || !timestamp) {
    throw new Error('Missing signature or timestamp');
  }

  const timestampNum = parseInt(timestamp);
  if (isNaN(timestampNum)) {
    throw new Error('Invalid timestamp');
  }

  // Check if timestamp is within 5 minutes
  const now = Date.now();
  if (now - timestampNum > 300_000) {
    throw new Error('Timestamp too old');
  }

  const expectedSignature = generateWebhookSignature('YOUR_WEBHOOK_SECRET', JSON.stringify(body), timestampNum);

  if (signature !== expectedSignature) {
    throw new Error('Invalid signature');
  }

  // webhook verified! You can now safely use it to update your systems

}

Replay Protection

We recommend rejecting webhook requests if the timestamp is older than 5 minutes to prevent replay attacks.

Retry Behavior & Idempotency

Titus will automatically retry webhook deliveries if:

  • Your server responds with a non-2xx HTTP status code

  • The request times out or fails to connect

Retry Logic

  • Retries use exponential backoff with a maximum number of attempts

  • There is no guarantee of delivery order

  • Your endpoint must be idempotent — handle duplicates gracefully

Best Practices

  • Log webhook attempts and failures for debugging and support

  • Always return a 2xx status code (e.g. 200 OK) if processing succeeds

Titus considers a webhook delivered once your server responds with a 2xx. All other responses will be retried.

Summary

  • Always validate webhook signatures

  • Handle retries and duplicate events safely

  • Use webhooks or API requests as the source of truth — not redirects