Public API
Webhooks
PactReach POSTs signed JSON to your endpoint when subscribed events fire. Verify HMAC before you trust the body.
Register
Write scope required. Use an HTTPS URL in production; http://localhost is accepted for local development. If you omit events, the default is deal.completed and conversion.recorded.
curl -sS -X POST 'https://api.pactreach.com/api/v1/brand/{brandId}/webhook/register-endpoint' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: pr_live_...' \
-d '{
"url": "https://api.example.com/pactreach/webhooks",
"events": ["deal.completed", "escrow.released", "conversion.recorded"]
}'data.signingSecret is returned once. It is not the API key. Rotate it with POST .../webhook/:endpointId/rotate-secret (the old secret stops working immediately).
Delivery payload
Each attempt POSTs JSON:
{
"event": "deal.completed",
"data": { },
"deliveryId": "...",
"sentAt": "2026-08-13T06:00:00.000Z"
}Headers on every attempt:
X-PactReach-Event- event nameX-PactReach-Delivery- delivery idX-PactReach-Timestamp- unix secondsX-PactReach-Signature-sha256=<hex>
Verify the signature
HMAC-SHA256 over {timestamp}.{rawBody} using the endpoint signing secret. Use the exact bytes you received, not re-serialised JSON. Reject if the timestamp is older than about five minutes (replay protection). Compare with a constant-time equals.
import crypto from 'node:crypto';
function verifyPactReachSignature(rawBody, timestamp, signatureHeader, signingSecret) {
const expected =
'sha256=' +
crypto.createHmac('sha256', signingSecret).update(`${timestamp}.${rawBody}`).digest('hex');
const a = Buffer.from(String(signatureHeader ?? ''));
const b = Buffer.from(expected);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Retries and timeouts
- Your endpoint has 10 seconds to return 2xx. Process asynchronously if work is slow.
- Non-2xx or network errors retry up to 6 attempts: ~30s, 2m, 10m, 1h, then 6h.
- 20 consecutive failures switch the endpoint off. PATCH isActive: true to re-enable (that also clears the failure counter).
- Inspect history with GET fetch-deliveries (status, eventName, endpointId, page, limit).
Send a test
POST .../webhook/:endpointId/send-test queues webhook.test to that endpoint only. The endpoint must be active; a disabled endpoint returns not found. Any valid key for the brand can send a test. Use it to prove signature handling before you subscribe to money events.
Subscribable events
Internal telemetry (for example ledger anomalies) is not offered. conversion.recorded is emitted when you post a conversion, not from the public campaign bus. webhook.test is only from send-test.
| Event | When it fires |
|---|---|
campaign.published | Campaign left review and is live for invites or briefs. |
campaign.completed | Every deal on the campaign is terminal and escrow is settled. |
campaign.cancelled | Campaign was cancelled before or during flight. |
campaign.budget_threshold | Spend crossed a configured budget threshold. |
deal.accepted | Creator accepted; escrow hold is in progress or complete. |
deal.declined | Creator declined an invite or offer. |
deal.expired | Offer TTL elapsed without accept. |
deal.status_changed | Deal moved to another status (content, live, verifying, ...). |
deal.completed | Deal finished after release or full refund. |
deal.cancelled | Deal was cancelled; escrow refunds per rules. |
deal.disputed | A dispute opened; settlement is paused. |
content.submitted | Creator submitted draft content for brand review. |
placement.submitted | Live post URL submitted for verification. |
placement.verified | Placement passed verification checks. |
placement.metrics_updated | Views, clicks, or engagements were updated. Payload may include measurementMode (api | link | hybrid); link platforms do not invent impressions. |
escrow.held | Funds moved from available into escrow for a deal. |
escrow.released | Earned funds released to the creator pending-unlock bucket. |
escrow.refunded | Unused escrow returned to the brand. |
verification.completed | Verification run finished for a placement or deal. |
dispute.resolved | Trust & Safety closed a dispute. |
conversion.recorded | Your postback created a new conversion. Replays do not re-fire this event. |
webhook.test | Ping from send-test. Use it to verify signature handling. |
Next: conversion postbacks or reference.

