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 name
  • X-PactReach-Delivery - delivery id
  • X-PactReach-Timestamp - unix seconds
  • X-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.

EventWhen it fires
campaign.publishedCampaign left review and is live for invites or briefs.
campaign.completedEvery deal on the campaign is terminal and escrow is settled.
campaign.cancelledCampaign was cancelled before or during flight.
campaign.budget_thresholdSpend crossed a configured budget threshold.
deal.acceptedCreator accepted; escrow hold is in progress or complete.
deal.declinedCreator declined an invite or offer.
deal.expiredOffer TTL elapsed without accept.
deal.status_changedDeal moved to another status (content, live, verifying, ...).
deal.completedDeal finished after release or full refund.
deal.cancelledDeal was cancelled; escrow refunds per rules.
deal.disputedA dispute opened; settlement is paused.
content.submittedCreator submitted draft content for brand review.
placement.submittedLive post URL submitted for verification.
placement.verifiedPlacement passed verification checks.
placement.metrics_updatedViews, clicks, or engagements were updated. Payload may include measurementMode (api | link | hybrid); link platforms do not invent impressions.
escrow.heldFunds moved from available into escrow for a deal.
escrow.releasedEarned funds released to the creator pending-unlock bucket.
escrow.refundedUnused escrow returned to the brand.
verification.completedVerification run finished for a placement or deal.
dispute.resolvedTrust & Safety closed a dispute.
conversion.recordedYour postback created a new conversion. Replays do not re-fire this event.
webhook.testPing from send-test. Use it to verify signature handling.

Next: conversion postbacks or reference.