Set up webhooks
Receive real-time notifications for payout lifecycle events.
Register a webhook to stay informed about payout status changes instead of polling.
Manage subscriptions
POST /webhook— subscribe a URL to events.GET /webhook— list your subscriptions.DELETE /webhook/{id}— remove one.
Webhooks live in the environment they were created in: a webhook registered via the sandbox API only fires for sandbox events, and one registered via the production API only fires for production events.
Event payload
When events occur, webhooks receive HTTP POST requests with a JSON payload that matches the payout schema returned by GET /payout/{id}.
Optional fields are omitted when absent, including paidOutAt and retry.
Event triggers
Webhooks fire for the payout lifecycle events:
- Payout creation (status
created) - Payout approval (status
approved) - Payout request (status
requested) - Payout return by the provider (status
requested), and again when the recipient's retry opens (with the retry link inretry.url) - Payout completion (status
completed, only after delivery completes) - Payout deletion (status
deleted) - Payout expiration (status
expired)
A retry does not emit a completion notification. When the retry completes, the webhook uses the original public payout ID and the delivery details of the successful attempt.
Delivery behavior
Method: POST, Content-Type: application/json.
Request headers:
X-Talentir-Signature: HMAC-SHA256 signature for payload verificationX-Talentir-Timestamp: Unix timestamp (seconds) when the webhook was sent
Retry behavior:
- Server errors (5xx): retried up to 10 times with exponential backoff (10s, 20s, 40s, 80s, 160s, 320s, 640s, 1280s, 2560s, 5120s). Total retry window is approximately 2.8 hours.
- Rate limiting (429): retried after a 60 second delay.
- Gone (410): the webhook subscription is automatically removed. No retry.
- Other client errors (4xx): not retried. These indicate a permanent failure (e.g. invalid endpoint configuration).
- Success (2xx): delivery confirmed.
Removal: when 25 events in a row are not delivered after all retries, and these failures span at least 5 days, the webhook is removed, as it is for a 410. A removed webhook receives no events, and GET /webhook no longer lists it. A successful delivery resets the count. Every member of your team gets an email when we remove a webhook. To receive events again, fix the endpoint and create the webhook again.
Verify signatures
Every webhook request includes a cryptographic signature. Always verify webhook signatures to ensure requests are genuinely from Talentir.
- Extract the signature and timestamp from the request headers.
- Construct the signed payload:
{timestamp}.{raw_request_body}. - Compute HMAC-SHA256 using your webhook's signing secret.
- Compare the computed signature with
X-Talentir-Signatureusing timing-safe comparison. - Optionally, reject requests with timestamps older than 5 minutes to prevent replay attacks.
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
timestamp: string,
signingSecret: string
): boolean {
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = createHmac('sha256', signingSecret)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison to prevent timing attacks
const sigBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expectedSignature);
if (sigBuffer.length !== expectedBuffer.length) return false;
return timingSafeEqual(sigBuffer, expectedBuffer);
}
// In your webhook handler:
app.post('/webhook', (req, res) => {
const signature = req.headers['x-talentir-signature'];
const timestamp = req.headers['x-talentir-timestamp'];
const payload = JSON.stringify(req.body);
// Reject requests older than 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return res.status(400).send('Timestamp too old');
}
if (!verifyWebhookSignature(payload, signature, timestamp, YOUR_SIGNING_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook...
});