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
webhookUrlon a media request receives that job'sgeneration.succeeded/generation.failed.
Workspace endpoints
| Endpoint | Body / query → result |
|---|---|
GET / | WebhookEndpointDto[] (members) |
POST / | { workspaceId, url, kind?: "GENERIC" | "SLACK", events?: string[] } → dto (admin). Empty events = everything |
PATCH / DELETE / | admin |
| Event | When | data |
|---|---|---|
approval.requested | An approval was created (by the API, the AI marketer or an automation) | ApprovalDto |
approval.decided · approval.executed | An admin approved / rejected; the executor finished (or failed) | ApprovalDto |
proposal.created | The AI left a suggestion for review | ProposalDto |
report.created | A report was generated (manual, schedule or automation) | ReportDto (summary section only — fetch GET / for all sections) |
content.published | A content item reached PUBLISHED | ContentItemDto |
metrics.alert | A metric rule tripped | { productId, channelId, metric, value, rule } |
channel.synced · connection.needs_reauth | Daily / manual connector sync finished; a connection needs the user to sign in again | ChannelDto / ChannelConnectionDto |
product.analyzed | The product analyser finished | { productId, runId } |
hook.received | An inbound hook with action EVENT was called | { hookId, event, data } |
automation.notify | An automation's action.webhook node ran | { automationId, runId, message } |
generation.succeeded · generation.failed | A 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
{
"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
| Header | Value |
|---|---|
X-Poremu-Timestamp | Unix seconds when the request was signed |
X-Poremu-Signature | v1=<hex HMAC-SHA256> of "{timestamp}.{rawBody}" using your webhook secret |
User-Agent | Poremu-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)
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
2xxwithin 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
webhookUrlis 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 / (workspace admins). The token (ph_…) is shown once, on creation and on rotation.
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:
| Action | Body | Result |
|---|---|---|
EVENT (default) | event, data | A product note 외부 이벤트 · <event> + outbound webhook hook.received + a realtime notice |
METRICS | rows[] — { date, metrics, scopeType?, scopeId?, scopeName? } | Metric snapshots upserted on the hook's channel (source: hook) |
AGENT | event, 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 /issues a new one.v1/ hooks/ :id/ rotate - Manage hooks with
GET /,v1/ hooks? workspaceId=… PATCH /,v1/ hooks/ :id DELETE /.v1/ hooks/ :id