DebriefDeveloperAPI 2026-09-14
Manage keys in Settings
On this page

Developer API

Get a key, make your first request, and receive signed webhooks.

Quickstart

Requires a paid plan. Caps: up to 5 active keys and 10 webhooks per personal or organisation account. For Claude / ChatGPT / Copilot Connect (OAuth MCP, not API keys), see Connect docs.

  1. In Settings → Developer, create an API key. For a team CRM integration use the Organisation tab (team admin); for solo use Personal. The plaintext key is shown only once. See Individual vs Team.
  2. Call GET /v1/calls?limit=1 with your Bearer key:
curl
curl -sS \
  -H "Authorization: Bearer dbf_live_…" \
  "https://debrief.ajora.io/api/v1/calls?limit=1"
  1. Build your HTTPS handler so it verifies X-Debrief-Signature and returns 401 on failure — then register the URL (Settings or POST /v1/webhooks). Never return 2xx for an unverified body; see Signature.
  2. When a debrief completes, open Settings → Developer, expand your webhook, and check Delivery log. Empty usually means no event yet — complete a call first.

Authentication

Send Authorization: Bearer <api_key>. Keys are created once in Settings → Developer and shown in plaintext only at create time. Live keys use the prefix dbf_live_…. Store keys in your backend or a secret manager — never in client apps or public config. See the Quickstart curl for a full request.

Base URL

Prefix every path with this host. Example: GET https://debrief.ajora.io/api/v1/calls.

Base URL
https://debrief.ajora.io/api

Scopes

Default key on create: calls:read, debriefs:read, and webhooks:manage. Transcripts are opt-in via transcripts:read.

ScopeGrants
calls:readList and get calls your key can access
debriefs:readGET /v1/calls/{id}/debrief (and webhook data.debrief)
transcripts:readGET /v1/calls/{id}/transcript only (opt-in)
webhooks:manageRegister, list, and revoke webhooks via /v1

Webhooks

Register an HTTPS URL to receive events as signed POST bodies. Manage endpoints in Settings or with webhooks:manage. The signing secret is shown only once at create time — store it like an API key. URLs must be publicly reachable HTTPS; private or local network targets are rejected.

MethodPathScopeNotes
POST/v1/webhookswebhooks:manageBody: url, events[]; returns signing secret once
GET/v1/webhookswebhooks:manageMetadata only (no secrets)
DELETE/v1/webhooks/{id}webhooks:manageRevoke endpoint

Events

EventWhen
debrief.completedWhen a debrief is ready
debrief.updatedSomeone edited the debrief in Debrief

Payload

Every delivery uses this envelope. Recording URLs, share tokens, and transcripts are omitted by default.

application/json
{
  "id": "evt_…",
  "type": "debrief.completed",
  "created_at": "2026-09-14T10:00:00Z",
  "api_version": "2026-09-14",
  "data": {
    "call": {
      "id": "00000000-0000-4000-8000-000000000001",
      "channel": "phone",
      "status": "completed",
      "duration_seconds": 842,
      "started_at": "2026-09-14T09:45:00Z",
      "ended_at": null,
      "origin": { "phone_e164": "+31612345678", "display_name": null },
      "counterparty": { "phone_e164": "+31687654321", "display_name": "Jane" },
      "participants": []
    },
    "debrief": {
      "id": "00000000-0000-4000-8000-000000000002",
      "title": "Quarterly check-in",
      "summary": "Discussed timeline and next steps.",
      "sentiment": "positive",
      "sentiment_note": "Tone stayed constructive.",
      "action_points": [
        { "id": "ap_…", "text": "Send proposal draft", "done_at": null }
      ],
      "key_decisions": ["Ship by end of month"],
      "permalink": "https://debrief.ajora.io/dashboard/calls/00000000-0000-4000-8000-000000000001"
    }
  }
}

Signature

Your webhook URL is a public HTTPS endpoint. Without verification, anyone who discovers it can POST forged JSON and your integration may treat it as a real debrief. We sign every delivery with the endpoint secret shown once at create time (dbf_whsec_live_…). HMAC the full secret string — do not strip a prefix. Reject the request unless the signature matches; then process the body. Never return 2xx for an unverified delivery.

Header X-Debrief-Signature: t=<unix>,v1=<hex_hmac_sha256>. Compute HMAC-SHA256 of {t}.{raw_body} using the raw request body bytes (read the body before your framework parses JSON — e.g. Express express.raw(), or await request.arrayBuffer()). Also reject if |now − t| > 300 seconds — that blocks replay of an old captured request.

Python
import hashlib
import hmac
import time

def verify(secret: str, header: str, raw_body: bytes, skew: int = 300) -> bool:
    if not header:
        return False
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
        v1 = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(int(time.time()) - t) > skew:
        return False
    signed = f"{t}.".encode("ascii") + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

# Header: X-Debrief-Signature: t=<unix>,v1=<hex_hmac_sha256>
# Use the full secret string (e.g. dbf_whsec_live_…) — do not strip any prefix.
Node.js
import crypto from "node:crypto";

export function verify(secret, header, rawBody, skew = 300) {
  if (!header) return false;
  const parts = Object.fromEntries(
    header.split(",").filter((p) => p.includes("=")).map((p) => p.split("=", 2)),
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!Number.isFinite(t) || typeof v1 !== "string") return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > skew) return false;
  const signed = Buffer.concat([Buffer.from(`${t}.`, "ascii"), rawBody]);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signed)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(v1);
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

// Header: X-Debrief-Signature: t=<unix>,v1=<hex_hmac_sha256>
// Use the full secret string (e.g. dbf_whsec_live_…) — do not strip any prefix.

Retries & idempotency

Timeouts, connection errors, 429, and 5xx retry with backoff 5 / 30 / 120 seconds (max 3 retries, 4 attempts total). Other HTTP 4xx responses are final — we do not retry them. After exhausted retries or repeated terminal failures the endpoint is marked unhealthy and deliveries stop until you revoke and create a new webhook. The same event may arrive more than once — treat envelope id as an idempotency key.

Calls API

You only see calls your key can access. Unknown or inaccessible call ids return 404 not_found (not 403).

MethodPathScopeNotes
GET/v1/callscalls:readQuery: since, status, origin_e164, counterparty_e164, cursor, limit
GET/v1/calls/{id}calls:readCall object only; unknown or inaccessible → 404 not_found (not 403)
GET/v1/calls/{id}/debriefdebriefs:readSame debrief object as webhook data.debrief
GET/v1/calls/by-phone/{e164}calls:readMatches counterparty E.164 (not origin)
GET/v1/calls/{id}/transcripttranscripts:readOpt-in; not included in default webhook payload

List

GET /v1/calls returns a cursor-paginated { items, next_cursor } envelope. Combine filters as needed.

Get

GET /v1/calls/{id} returns the call object only. Fetch the debrief via GET /v1/calls/{id}/debrief (debriefs:read).

Filters

ParameterDescription
origin_e164Your side of the call (normalized E.164)
counterparty_e164The other party’s phone (normalized E.164)
sinceISO-8601 datetime — calls created at or after this time
statusprocessing | completed | failed
cursor / limitCursor pagination for list endpoints

By phone

GET /v1/calls/by-phone/{e164} matches the counterparty E.164 — not the origin / host dialer.

Debrief

GET /v1/calls/{id}/debrief returns the same debrief object as webhook data.debrief.

Transcript

GET /v1/calls/{id}/transcript requires transcripts:read. Transcripts are omitted from the default webhook payload — re-fetch via this endpoint when needed.

Individual vs Team

A key is either personal or organisation-owned — never both. A team admin manages organisation keys and webhooks in Settings; members can still create personal resources.

Key typeVisible calls
Personal keyCalls you own
Organisation keyAll team members' calls

Errors & rate limits

Errors use { error: { code, message } }— no stack traces or record contents. Rate limit: 120/minute per API key. Paid plans only; otherwise you may see feature_disabled.

error
{
  "error": {
    "code": "not_found",
    "message": "Call not found"
  }
}
CodeMeaning
unauthorizedMissing or invalid API key
forbiddenAuthenticated but not allowed
not_foundUnknown or inaccessible resource (returns 404, not 403)
rate_limitedExceeded 120/minute per key
validation_errorInvalid query or body
feature_disabledDeveloper API not available on this plan

Delivery log

Settings → Developer shows delivery attempts for the last 14 days: timestamps, event type, event id, call id, HTTP status, latency, attempt number, outcome, and a short error class when failed. Request and response bodies are not stored — re-fetch call or debrief content via /v1 when you need the payload again.