Offer conversion callback API
Self-service offers configured with Custom S2S send server-verified events to a campaign-specific endpoint. This is the external advertiser integration. The campaign management endpoints use a dashboard bearer session; credentials are not interchangeable.
Endpoint and authentication
POST /performance-marketing/v2/postbacks/custom-s2s/{campaignId}
Content-Type: application/json
X-RapidoReach-Key-Id: YOUR_CAMPAIGN_KEY_ID
X-RapidoReach-Timestamp: CURRENT_UNIX_SECONDS
X-RapidoReach-Signature: LOWERCASE_HEX_HMAC_SHA256
{
"click_id": "clk_example",
"transaction_id": "order_12345",
"goal_id": "qualified_purchase",
"status": "approved",
"occurred_at": "2026-05-31T00:00:00.000Z"
}
Use the endpoint and key ID returned by the campaign's Tracking setup. Create its credential in Campaign Studio; the secret is returned once. Store it on your server. The route accepts GET and POST, but use POST so event fields do not enter URL logs. Offer delivery must be enabled; a disabled route returns 404 not_found.
| JSON field | Requirement |
|---|---|
click_id | Exact ID received through the {click_id} destination macro. |
transaction_id | Stable ID for this conversion or order. Keep it unchanged on retry and later state changes. |
goal_id | Exact externalGoalId of the campaign milestone. |
status | pending, approved, rejected, or reversed. |
occurred_at | Parseable event date/time; send ISO 8601 UTC. |
Construct the signature
Convert the current Unix time in seconds to a decimal string. Join six strings with actual newline bytes, in this order: timestamp, click_id, transaction_id, goal_id, status, occurred_at. Compute HMAC-SHA256 using the campaign secret as the UTF-8 key and encode the digest as hexadecimal. Send the timestamp string and digest in the headers. The server rejects timestamps more than five minutes from its clock.
const crypto = require('node:crypto');
const timestamp = String(Math.floor(Date.now() / 1000));
const event = {
click_id: 'clk_example', transaction_id: 'order_12345',
goal_id: 'qualified_purchase', status: 'approved',
occurred_at: new Date().toISOString(),
};
const message = [timestamp, event.click_id, event.transaction_id,
event.goal_id, event.status, event.occurred_at].join('\n');
const signature = crypto.createHmac('sha256', process.env.RAPIDOREACH_CAMPAIGN_SECRET)
.update(message).digest('hex');
// POST JSON.stringify(event) with the timestamp, key ID, and signature headers.
Sign the values actually transmitted. Do not sign the whole JSON object or use a publisher callback secret. A campaign IP allowlist, when configured, is checked in addition to the signature.
Delivery and retries
HTTP 200 means the callback passed authentication and was durably placed in the processing inbox. It does not mean the conversion or reward has settled. The response is { "accepted": true, "duplicate": false, "receiptId": "INBOX_ID" }; an exact retry returns duplicate: true and the same logical receipt. Inspect campaign reporting and postback processing for the final state. On timeout or transient failure, retry the same transaction, goal, and state. A reused transaction with conflicting click or goal data is rejected. Send reversed when a credited outcome must be reversed; reconcile the ledger adjustment.
Test custom S2S checks the stored key locally and returns deliveredExternally: false and accountingWrites: 0. Then run a development launch, capture a real click ID, send a callback, inspect the conversion, retry the event, and test an invalid signature. The in-memory test alone does not verify attribution.
Older DIRECT campaigns may return /performance-marketing/v2/postbacks/direct and a direct token. Use that flow only when Tracking setup selects it. Managed MMP campaigns use their provider-specific callback. See tracking and key rotation and errors.