Skip to content

Developer docs

Webhooks

Marketing events (approvals, reports, proposals, channel syncs, automations) and generation results, pushed to your URL or Slack — plus inbound hooks that push your events in.

Two kinds of outbound webhooks share one payload shape and one signature:

  • Workspace endpoints (/v1/webhooks) receive marketing events — approvals, proposals, reports, channel syncs, product analysis, inbound hooks, automations — for a whole workspace. Slack incoming-webhook URLs are supported natively.
  • Per-generation webhookUrl on a media request receives that job's generation.succeeded / generation.failed.

Workspace endpoints

EndpointBody / query → result
GET /v1/webhooks?workspaceId=WebhookEndpointDto[] (members)
POST /v1/webhooks{ workspaceId, url, kind?: "GENERIC" | "SLACK", events?: string[] } → dto (admin). Empty events = everything
PATCH / DELETE /v1/webhooks/{id}admin
EventWhendata
approval.requestedAn approval was created (by the API, the AI marketer or an automation)ApprovalDto
approval.decided · approval.executedAn admin approved / rejected; the executor finished (or failed)ApprovalDto
proposal.createdThe AI left a suggestion for reviewProposalDto
report.createdA report was generated (manual, schedule or automation)ReportDto (summary section only — fetch GET /v1/reports/{id} for all sections)
content.publishedA content item reached PUBLISHEDContentItemDto
metrics.alertA metric rule tripped{ productId, channelId, metric, value, rule }
channel.synced · connection.needs_reauthDaily / manual connector sync finished; a connection needs the user to sign in againChannelDto / ChannelConnectionDto
product.analyzedThe product analyser finished{ productId, runId }
hook.receivedAn inbound hook with action EVENT was called{ hookId, event, data }
automation.notifyAn automation's action.webhook node ran{ automationId, runId, message }
generation.succeeded · generation.failedA media generation finished (per-generation webhookUrl only)the generation object

Endpoints with kind: "SLACK" get { text, blocks } in Slack's format instead of the signed JSON below. After 10 consecutive failures an endpoint is disabled (enabled: false) — fix the URL and PATCH it back on.

Per-generation webhooks

Pass webhookUrl when creating a generation. When the job reaches SUCCEEDED or FAILED we POST the result to that URL.

Payload

json
{
  "id": "evt_6f1d6a2e-…",
  "type": "generation.succeeded",
  "createdAt": "2026-03-02T04:12:40.000Z",
  "data": { "id": "6f1d6a2e-…", "status": "SUCCEEDED", "outputs": [ … ] }
}

type is one of the events above; data is the object named in the table.

Headers

HeaderValue
X-Poremu-TimestampUnix seconds when the request was signed
X-Poremu-Signaturev1=<hex HMAC-SHA256> of "{timestamp}.{rawBody}" using your webhook secret
User-AgentPoremu-Webhook/1.0

Your webhook secret (whsec_…) is shown in Settings → Developer → Webhooks. It is the same for all your keys; workspace endpoints are signed with the secret of the member who created them.

Verify (Node.js)

js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, headers, secret) {
  const ts = headers['x-poremu-timestamp'];
  const sig = (headers['x-poremu-signature'] || '').replace(/^v1=/, '');
  if (!ts || !sig || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  return expected.length === sig.length && timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

Delivery

  • We expect a 2xx within 10 seconds. Otherwise we retry 2 more times (2 s, 4 s apart).
  • Deliveries can arrive more than once in rare cases — make handlers idempotent using data.id.
  • Per-generation webhookUrl is only honoured on API-key requests (not browser sessions). Workspace endpoints fire for everything.

Inbound hooks

The other direction exists too: a URL that pushes external events — orders, signups, metric rows — into Poremu. Create one under Settings → Developer → Inbound hooks or with POST /v1/hooks (workspace admins). The token (ph_…) is shown once, on creation and on rotation.

bash
curl -X POST https://api.poremu.ai/v1/hooks/ph_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
  -H "Content-Type: application/json" \
  -d '{"event":"order.created","data":{"orderId":"A-1041","amount":59000}}'

The response is { "ok": true, "action": "EVENT", "id": "…" }. What the body does depends on the hook's action:

ActionBodyResult
EVENT (default)event, dataA product note 외부 이벤트 · <event> + outbound webhook hook.received + a realtime notice
METRICSrows[] — { date, metrics, scopeType?, scopeId?, scopeName? }Metric snapshots upserted on the hook's channel (source: hook)
AGENTevent, data (or an explicit goal)Runs the AI marketer with the hook's goal template, {{event}} / {{data}} substituted (trigger: EVENT, background engine)
  • 60 requests per minute per token, body ≤ 64 KB; beyond that you get 429 RATE_LIMITED / 400 VALIDATION_ERROR.
  • No Authorization header — the token is the secret. If it leaks, POST /v1/hooks/:id/rotate issues a new one.
  • Manage hooks with GET /v1/hooks?workspaceId=…, PATCH /v1/hooks/:id, DELETE /v1/hooks/:id.