Agentic Workforce ME Developer PortalDocs 1.1 · Widget 0.1.0

Backend integration

Webhooks: close the loop

Subscribe to run and approval events, verify Hive-Signature, handle retries idempotently, read the delivery ledger, test locally.

Webhooks are how Agentic Workforce ME tells your backend what happened without you polling: a run finished (with its output and cost), a run failed, a run is waiting for a person. Each delivery is a signed POST to a URL you register; you verify the signature over the raw body, de-duplicate on the event id, acknowledge quickly and do the real work asynchronously. This closes the loop the trigger opened.

1. Subscribe

POST /v1/webhooks with the URL and the event types you want. The response contains the signing secret once; store it as your HIVE_WEBHOOK_SECRET. An endpoint subscribes to up to 16 entries from the catalog below, or to "*" for everything. POST /v1/webhooks/:id/test sends a signed webhook.test so you can prove the receiver before wiring anything else.

TypeScript
const endpoint = await hive.webhooks.create({
  url: 'https://api.acme.example/hive/webhook',
  events: ['run.completed', 'run.failed', 'approval.requested'],
  description: 'orders service',
});
// endpoint.secret is shown ONCE → store it as HIVE_WEBHOOK_SECRET

2. What a delivery looks like

one deliveryHTTP
POST /hive/webhook HTTP/1.1
Content-Type: application/json
User-Agent: hive-webhooks/1.0
Hive-Event: run.completed
Hive-Delivery-Id: 019a…            ← per endpoint per event; stable across retries
Hive-Signature: t=1757000000,v1=5f2a…64 hex…

{
  "id": "019a1b2c-…",               event id (uuid v7): de-duplicate on this
  "type": "run.completed",
  "created_at": "2026-09-03T08:00:00.000Z",
  "data": {
    "run_id": "…", "agent_id": "…", "workflow_id": "…", "thread_id": null,
    "trigger": "webhook", "status": "succeeded",
    "output": { "text": "Refunded 129.00 AED on ord_1001 (rf_…)." },
    "error": null, "tokens_in": 1820, "tokens_out": 240, "cost_usd": 0.0064,
    "finished_at": "2026-09-03T08:00:41.000Z"
  }
}
HeaderValue
Content-Typeapplication/json
User-Agenthive-webhooks/1.0
Hive-EventThe event type, same as the body’s `type`.
Hive-Delivery-IdThe delivery id: one per endpoint per event, identical on every retry of that delivery.
Hive-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>

Envelope

FieldTypeDescription
iduuid v7Event id. De-duplicate on it — retries resend the same id.
typeWebhookEventTypeAlso sent as the Hive-Event header.
created_atISO date-timeWhen the event was emitted.
dataobjectEvent-specific payload (tables below).

3. Event catalog

The types below are the ones the runtime emits today; the API rejects a subscription to anything else. run.completed and run.failed fire for the top-level run only — the agent run you started, or the workflow run a trigger started. The child runs a workflow spawns for its agent nodes execute inline and emit no lifecycle webhooks of their own; read their outcome from the parent’s output (GET /v1/runs/:idoutputs.<node>.child_run_id). Filter on workflow_id / agent_id or on the run_id you stored when you started the work.

run.completed

A run (agent or workflow) reached succeeded. Dedupe key: run.completed:<run_id>.

data fieldTypeDescription
run_iduuidThe run.
agent_iduuid | nullAgent (null for a parent workflow run).
workflow_iduuid | nullWorkflow (parent runs).
thread_iduuid | nullThread when started from chat.
triggerstringchat, webhook, manual, cron, delegate, …
status`succeeded` | `failed`Terminal status.
outputunknownAgent runs: { text, json? } (plus citations / refused when present). Workflow runs: { outputs: { <node_id>: { text, child_run_id, json? } }, variables } — read the node you care about.
error{ code, message } | nullSet on run.failed.
tokens_in / tokens_outnumberAggregated model usage.
cost_usdnumberAggregated cost.
finished_atISO date-time | nullWhen the run ended.

run.failed

A run reached failed (model error, tool failure surfaced by the graph, cancellation while waiting). Dedupe key: run.failed:<run_id>.

data fieldTypeDescription
run_iduuidThe run.
agent_iduuid | nullAgent (null for a parent workflow run).
workflow_iduuid | nullWorkflow (parent runs).
thread_iduuid | nullThread when started from chat.
triggerstringchat, webhook, manual, cron, delegate, …
status`succeeded` | `failed`Terminal status.
outputunknownAgent runs: { text, json? } (plus citations / refused when present). Workflow runs: { outputs: { <node_id>: { text, child_run_id, json? } }, variables } — read the node you care about.
error{ code, message } | nullSet on run.failed.
tokens_in / tokens_outnumberAggregated model usage.
cost_usdnumberAggregated cost.
finished_atISO date-time | nullWhen the run ended.

approval.requested

A run paused on a human-in-the-loop gate; one event per newly pending approval. Dedupe key: approval.requested:<approval_id>.

data fieldTypeDescription
approval_iduuidDecide it with POST /v1/approvals/:id/decision.
run_iduuidThe paused run.
agent_iduuid | nullThe agent that asked.
toolstringRequested tool key: MCP tools are server-qualified (acme-orders.orders.refund); workflow pauses use workflow:gate / workflow:human_task.
riskstringFrom the matching policy (medium when none).
assignee_rolestringRole that may decide.
expires_atISO date-timeSLA expiry.

experiment.rolled_back

An A/B experiment was rolled back (manual or automatic). Dedupe key: experiment.rolled_back:<experiment_id>.

data fieldTypeDescription
experiment_iduuidThe experiment.
agent_iduuidIts agent.
pinned_version_iduuidVersion now pinned.
reasonstringmanual or the metric that tripped.

payment.executed

An agent payment (charge or refund) executed under a mandate. Dedupe key: payment.executed:<payment_id>.

data fieldTypeDescription
payment_iduuidLedger row.
run_id / agent_iduuidWho paid.
mandate_iduuidAuthorising mandate.
toolstringpayments.charge or payments.refund.
amount_minor / currencynumber / stringAmount in minor units.
rail / external_refstringPayment rail and its reference.

payment.denied

A payment attempt was refused by policy or mandate. Dedupe key: payment.denied:<payment_id>.

data fieldTypeDescription
payment_iduuidLedger row.
run_id / agent_iduuidWho tried.
tool / amount_minor / currencyWhat was attempted.
code / reasonstringWhy it was denied.

webhook.test

You called POST /v1/webhooks/:id/test. Dedupe key: webhook.test:<event_id> (never de-duplicated).

data fieldTypeDescription
endpoint_iduuidThe endpoint under test.
triggered_bystringPrincipal that requested the test.
approval.requestedJSON
{
  "id": "019a1b2d-…",
  "type": "approval.requested",
  "created_at": "2026-09-03T08:00:12.000Z",
  "data": {
    "approval_id": "019a1b2d-…",
    "run_id": "…",
    "agent_id": "…",
    "tool": "acme-orders.orders.refund",    MCP tools arrive server-qualified (<server slug>.<tool>);
    "risk": "high",                          a policy written as "orders.refund" still matches
    "assignee_role": "admin",
    "expires_at": "2026-09-04T08:00:12.000Z"
  }
}

4. Verify the signature

ElementValue
HeaderHive-Signature
Formatt=<unix seconds>,v1=<hex HMAC-SHA256>
AlgorithmHMAC-SHA256 keyed with the endpoint secret (UTF-8), lowercase hex
Signed material"<t>.<raw body>"
Replay window300 s either side of your clock (default in the SDK helpers)
CompareConstant time, after the timestamp check.
webhook.tsTypeScript
import express from 'express';
import { verifyWebhookSignature } from '@hive/sdk'; // or copy hive-signing.mjs from the example

const app = express();

// Raw body FIRST — the signature covers the exact bytes the platform sent.
app.post('/hive/webhook', express.raw({ type: '*/*' }), async (req, res) => {
  const raw = req.body.toString('utf8');
  const check = verifyWebhookSignature(process.env.HIVE_WEBHOOK_SECRET!, req.get('hive-signature') ?? '', raw);
  if (!check.ok) return res.status(401).json({ error: check.reason }); // MALFORMED_HEADER | STALE_TIMESTAMP | SIGNATURE_MISMATCH

  const event = JSON.parse(raw) as { id: string; type: string; data: Record<string, unknown> };
  if (await alreadyProcessed(event.id)) return res.status(200).json({ duplicate: true }); // retries resend the same id

  await enqueue(event); // ACK fast, apply asynchronously
  res.status(200).json({ received: true });
});
  • Raw body. Register the raw-body parser before any JSON parser on this route (Express express.raw, Flask request.get_data(), ASP.NET the request stream). Re-serialising the parsed JSON changes bytes and fails the check.
  • Helpers. verifyWebhookSignature(secret, header, rawBody) in @hive/sdk and verify_webhook_signature in the Python SDK return { ok, reason } with MALFORMED_HEADER, STALE_TIMESTAMP or SIGNATURE_MISMATCH. The Python sample on this page and the example’s hive-signing.mjs are executed by the portal’s tests against the platform’s own signer; the .NET and Java samples are checked for the same header, canonical string and algorithm.
  • Clock. The timestamp is the platform’s clock in Unix seconds; keep your receiver’s clock NTP-synced or widen the tolerance deliberately, never by disabling the check.

5. Retries, idempotency and ordering

  • A delivery is successful on any 2xx. Anything else — a 4xx, a 5xx, a timeout, a TLS failure — is retried: 5 attempts with exponential backoff starting at 5 s (≈ 5 s, 10 s, 20 s, 40 s between attempts). After the last failure the delivery is marked failed; the ledger below shows the status code or transport error you returned.
  • Exactly the same bytes are sent on every retry (the envelope is stored once), with the same id, the same Hive-Delivery-Id and a fresh signature — computed at delivery time with the endpoint’s current secret, so after a rotation retries of earlier events arrive signed with the new one. De-duplicate on the envelope id (or the delivery id) in a store that survives restarts; answer 200 for a duplicate. Check the store before applying and mark the id after the apply succeeded (or after persisting the raw event): marking first turns a crash mid-apply into a silently lost event.
  • Acknowledge fast. Verify, record, respond, then process on a queue. A slow handler risks the delivery timeout and a redundant retry.
  • Ordering is not guaranteed. Retries and parallel workers can deliver approval.requested after run.completed for the same run. Treat each event as a fact with a created_at, and read GET /v1/runs/:id when you need the current state rather than inferring it from arrival order.
  • Disabled endpoints (PATCH enabled: false) skip delivery entirely; nothing is queued for later.

The delivery ledger

Shell
# The ledger: what was sent, when, with which HTTP status from your side
curl https://console.agenticworkforce.me/v1/webhooks/$ENDPOINT_ID/deliveries \
  -H "Authorization: Bearer $HIVE_API_KEY" -H "X-Tenant-Id: $HIVE_TENANT_ID"
# → { "items": [ { "event_id": "…", "event_type": "run.completed", "status": "succeeded",
#                  "attempts": 1, "last_status_code": 200, "last_error": null, … } ] }

6. Local testing

Shell
# Expose your local receiver with a public https URL
cloudflared tunnel --url http://localhost:4100      # or: ngrok http 4100
# Register https://<random>.trycloudflare.com/hive/webhook as the endpoint URL.

# Self-hosted stack on your machine instead? Allow plain-http/private hosts for the worker:
A2A_ALLOW_INSECURE_HOSTS=localhost,host.docker.internal
  • Tunnels give you a public HTTPS URL for a laptop; register it and re-run your setup script when it changes. POST /v1/webhooks/:id/test is the fastest round trip.
  • If you run the stack yourself, the worker’s SSRF policy can allow-list private hosts through the deployment’s environment (A2A_ALLOW_INSECURE_HOSTS). Never do this in production.
  • Generate signed test payloads offline: buildSignatureHeader(secret, body) from @hive/core / @hive/sdk produces a valid header for any body, which is how the portal tests drive the example receiver.

Endpoints

GET/v1/webhooksList webhook endpoints (never the secret).
Auth
API key + X-Tenant-Id (tenant admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN
POST/v1/webhooks201Subscribe a URL to platform events. The signing secret is returned once.
Auth
API key + X-Tenant-Id (tenant admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN VALIDATION INVALID_URL

Request

FieldTypeDescription
urlstringhttps and a public host in production (SSRF-guarded at registration and at delivery).
eventsWebhookEventType[] | ["*"]1–16 event types, or ["*"] for everything.
descriptionstring?Up to 500 chars.

Response

FieldTypeDescription
iduuidEndpoint id.
secretstringShown once. Verify every delivery with it.
GET/v1/webhooks/:idOne endpoint.
Auth
API key + X-Tenant-Id (tenant admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN NOT_FOUND
PATCH/v1/webhooks/:idChange url, events, description or enabled.
Auth
API key + X-Tenant-Id (tenant admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN VALIDATION INVALID_URL NOT_FOUND
DELETE/v1/webhooks/:id204Remove the subscription (204).
Auth
API key + X-Tenant-Id (tenant admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN NOT_FOUND
POST/v1/webhooks/:id/rotate-secretMint a new signing secret (shown once). Every delivery after rotation — including retries of earlier events — is signed with the new secret: the worker reads the endpoint’s current secret at delivery time.
Auth
API key + X-Tenant-Id (tenant admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN NOT_FOUND

Response

FieldTypeDescription
secretstringThe new secret.
  • No overlap window. Deploy the new secret before the next retry of any failing delivery (attempts are 5 s, 10 s, 20 s and 40 s apart, about 75 s in total) or accept both secrets during the transition. For zero downtime, register a second endpoint, cut over, then delete the old one.
GET/v1/webhooks/:id/deliveriesThe delivery ledger — status, attempts, last HTTP status and error per event.
Auth
API key + X-Tenant-Id (tenant admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN NOT_FOUND

Response

FieldTypeDescription
items[].event_iduuidThe envelope id your receiver de-duplicates on.
items[].event_typeWebhookEventTypeWhat was sent.
items[].statusstringpending, succeeded or failed (after the last attempt).
items[].attemptsnumberAttempts so far (max 5).
items[].last_status_codenumber | nullYour endpoint’s last HTTP status.
items[].last_errorstring | nullTransport or HTTP error text.
POST/v1/webhooks/:id/test202Enqueue a signed webhook.test delivery to the endpoint — the first thing to run after subscribing.
Auth
API key + X-Tenant-Id (tenant admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN NOT_FOUND