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

EventSent when
visitor.identifiedAn anonymous visitor became known through a legitimate source.
visitor.intent_changedA visitor's intent level changed.
audience.createdAn audience was created.
audience.member_addedMembers joined an audience.
audience.syncedAn audience finished syncing to a destination.
creative.generatedA creative generation job completed.
creative.scoredA creative received a quality score.
campaign.createdA campaign was created.
campaign.launchedA campaign went live on one or more networks.
campaign.pausedA campaign was paused.
conversion.createdA conversion was recorded.
agent.recommendation_createdROI Agent produced a recommendation.
agent.action_pendingAn agent action is waiting for approval.
agent.action_completedAn approved agent action finished executing.
integration.errorA 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-id to 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.