OpenPhn docs

Webhooks

Register an HTTPS endpoint; we POST signed events on call completion.

Webhooks are OpenPhn's push-based alternative to polling /v1/calls/{id}. You register an HTTPS endpoint; we POST a signed JSON payload when a subscribed event fires. Retries, signing, and per-delivery history are all first-class.

Events that fire today

EventWhen
call.completedAny outbound or inbound call reaches a terminal state (completed or failed).

call.completed is the only event today. Future events (call.started, call.transferred, sms.delivered) will be opt-in via the same endpoint.

Register an endpoint

curl -X POST https://api.openphn.com/v1/webhooks \
  -H "Authorization: Bearer $OPENPHN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url":         "https://example.com/webhooks/openphn",
    "description": "Prod call-completion handler"
  }'

Response includes a secret (the HMAC signing key). Shown once — save it.

{
  "id":          "wh_01HV...",
  "url":         "https://example.com/webhooks/openphn",
  "secret":      "whsec_AbC123...",
  "description": "Prod call-completion handler",
  "created_at":  "2026-04-21T14:00:00Z",
  "active":      true
}

URLs must be HTTPS and resolve to a publicly routable IP. OpenPhn's SSRF guard rejects requests targeting RFC1918 space, loopback, link-local, or cloud metadata endpoints. Localhost testing needs ngrok / cloudflared.

Payload shape

{
  "id":         "evt_01HV...",
  "type":       "call.completed",
  "created_at": "2026-04-21T14:05:12Z",
  "data": {
    "call_id":         "cll_01HV...",
    "status":          "completed",
    "to":              "+14155551234",
    "from":            "+14155550000",
    "objective":       "Confirm order #A-14421 ships today",
    "outcome": {
      "will_ship_today": true,
      "tracking":        "1Z...",
      "eta":             "2026-04-22T18:00:00-07:00"
    },
    "transcript":      "…",
    "duration_seconds": 83,
    "cost_total":      "0.09"
  }
}

Signature verification

Every POST carries two headers:

X-OpenPhn-Signature: sha256=<hex>
X-OpenPhn-Timestamp: 1776791112

The signature is HMAC-SHA256 of timestamp + "." + raw_body using the webhook's secret. Verify before you process the body, and reject if the timestamp is more than ~5 minutes old (replay defense).

Node.js

import crypto from "node:crypto";

export function verify(rawBody, timestamp, signature, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    throw new Error("bad signature");
  }
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    throw new Error("timestamp too old");
  }
}

Python

import hmac, hashlib, time

def verify(raw_body: bytes, timestamp: str, signature: str, secret: str) -> None:
    expected = "sha256=" + hmac.new(
        secret.encode(),
        f"{timestamp}.{raw_body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expected, signature):
        raise ValueError("bad signature")
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("timestamp too old")

Retry policy

If your endpoint returns anything other than 2xx OR the request fails at the transport layer, OpenPhn retries with exponential backoff:

AttemptDelay
1immediate
2+5 seconds
3+30 seconds
4+2 minutes

After the 4th failure the delivery is marked permanently failed. You can replay it manually — see below.

Not retried: 4xx responses (except 429). A 4xx tells us your endpoint saw the request and rejected it; retrying just wastes attempts.

Idempotency on your side

Each delivery has a stable id (evt_01HV...) that's stable across retries. Deduplicate on that. Don't dedupe on call_id alone — one call can generate multiple deliveries if your endpoint 500s and we retry.

Delivery history API

Every attempt is persisted.

curl https://api.openphn.com/v1/webhooks/wh_01HV.../deliveries \
  -H "Authorization: Bearer $OPENPHN_KEY"

Each row includes the attempt number, response status, response body (truncated), and latency. Useful when chasing down "did my handler see this?" questions.

Manual retry

curl -X POST https://api.openphn.com/v1/webhooks/wh_01HV.../deliveries/dlv_01HV.../retry \
  -H "Authorization: Bearer $OPENPHN_KEY"

Forces OpenPhn to re-POST the original payload. Useful after you've fixed a bug on your endpoint and want to process missed deliveries without replaying the whole call.

Delete / deactivate

DELETE /v1/webhooks/{id} soft-deletes the endpoint. No more events will be dispatched. Existing delivery history remains queryable.

On this page