Webhooks
ROI.me emits a standard event for every significant change. Subscribe an HTTPS endpoint and your CRM, workflow tools or internal systems stay in step.
In development
The ROI.me API is in development. Resource names and shapes below reflect the v1 design and may change before general availability.
Events
| Event | Sent when |
|---|---|
| visitor.identified | An anonymous visitor became known through a legitimate source. |
| visitor.intent_changed | A visitor's intent level changed. |
| audience.created | An audience was created. |
| audience.member_added | Members joined an audience. |
| audience.synced | An audience finished syncing to a destination. |
| creative.generated | A creative generation job completed. |
| creative.scored | A creative received a quality score. |
| campaign.created | A campaign was created. |
| campaign.launched | A campaign went live on one or more networks. |
| campaign.paused | A campaign was paused. |
| conversion.created | A conversion was recorded. |
| agent.recommendation_created | ROI Agent produced a recommendation. |
| agent.action_pending | An agent action is waiting for approval. |
| agent.action_completed | An approved agent action finished executing. |
| integration.error | A connected integration failed and needs attention. |
Payload
POST https://your-app.example/roi-webhook
roi-signature: t=1790467451,v1=5f2b0c…
roi-event-id: evt_4Tq2…
{
"id": "evt_4Tq2…",
"type": "agent.action_pending",
"created_at": "2026-09-26T18:04:11Z",
"organization": "org_1Hs7…",
"data": {
"action": "act_9Lm3…",
"kind": "change_budget",
"campaign": "cmp_2hQ9…",
"impact": { "daily_budget": { "from": 14000, "to": 21000, "currency": "USD" } },
"approval_url": "https://app.roi.me/approvals/apv_7Kc1…"
}
}Verify signatures
Each delivery is signed with your endpoint's secret using HMAC-SHA256 over timestamp.body. Reject deliveries whose signature doesn't match or whose timestamp is more than five minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Delivery and retries
- Respond with any 2xx status within 10 seconds to acknowledge.
- Failed deliveries retry with exponential backoff for up to 24 hours.
- Use
roi-event-idto de-duplicate: a delivery may arrive more than once. - The portal shows every delivery with its request, response and timing, and lets you replay any of them.