Skip to main content

Webhooks overview

Webhooks let your server receive real-time notifications when events happen in your PaymentsAI account. PaymentsAI sends an HTTP POST request to your registered endpoint every time a relevant event occurs. No polling is required.

How it works

  1. You register a webhook destination (a URL on your server).
  2. You subscribe that destination to one or more event types.
  3. When an event fires, PaymentsAI sends a JSON payload to your URL.
  4. Your server must respond with HTTP 200 within the timeout window.
  5. If your server does not respond with 200, PaymentsAI retries delivery.

See How to set up a webhook for step-by-step registration instructions.

Event structure

Every webhook payload shares the same envelope:

type Event = {
deduplicationId: string;
type: string;
organizationId: string;
metadata: Transaction | Subscription | Dispute;
};
FieldTypeDescription
deduplicationIdstringUnique identifier for this delivery. Use it to deduplicate retries — PAI may deliver the same event more than once.
typestringThe event type. See Event catalog for all values.
organizationIdstringThe organization this event belongs to.
metadataobjectEvent-specific payload. Shape varies by event type. See Schemas.

Signature verification

PaymentsAI signs every payload so you can verify it actually came from PAI and was not tampered with.

How it works: PAI computes HMAC-SHA256 of the raw request body using your webhook secret and sends the result in a signature header.

To verify:

  1. Read the raw request body before any JSON parsing.
  2. Compute HMAC-SHA256 of that raw body using your webhook secret.
  3. Compare your result to the value in the signature header using a timing-safe comparison.

Always use the raw request body. Parsing and re-serializing JSON may change whitespace or key order and will produce a different HMAC.

Code examples

Node.js
const crypto = require('crypto');

function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Python
import hmac
import hashlib

def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
PHP
function verifySignature(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
Ruby
require 'openssl'

def verify_signature(raw_body, signature, secret)
expected = OpenSSL::HMAC.hexdigest('sha256', secret, raw_body)
ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end

Delivery guarantees

  • At-least-once delivery: PAI may deliver the same event more than once. Use deduplicationId to skip duplicates.
  • Out-of-order delivery: Events are not guaranteed to arrive in the order they were generated.
  • Retry on non-200: Any response other than HTTP 200 triggers a retry with exponential backoff (see below).

To avoid timeouts, respond with 200 immediately and process the event asynchronously.

Retry mechanism

If your endpoint does not return HTTP 200 within 20 seconds, PaymentsAI retries delivery automatically using exponential backoff.

AttemptDelay after previous
1st retry30 minutes
2nd retry1 hour
3rd retry2 hours
4th retry4 hours
5th retry8 hours
6th retry16 hours

After all 6 retries are exhausted the event is marked as failed and no further attempts are made. Use deduplicationId to handle retries safely — your handler should be idempotent.