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.
- 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.
- Call
GET /v1/calls?limit=1with your Bearer key:
curl -sS \
-H "Authorization: Bearer dbf_live_…" \
"https://debrief.ajora.io/api/v1/calls?limit=1"- Build your HTTPS handler so it verifies
X-Debrief-Signatureand returns401on failure — then register the URL (Settings orPOST /v1/webhooks). Never return 2xx for an unverified body; see Signature. - 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.
| Scope | Grants |
|---|---|
| calls:read | List and get calls your key can access |
| debriefs:read | GET /v1/calls/{id}/debrief (and webhook data.debrief) |
| transcripts:read | GET /v1/calls/{id}/transcript only (opt-in) |
| webhooks:manage | Register, 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.
| Method | Path | Scope | Notes |
|---|---|---|---|
| POST | /v1/webhooks | webhooks:manage | Body: url, events[]; returns signing secret once |
| GET | /v1/webhooks | webhooks:manage | Metadata only (no secrets) |
| DELETE | /v1/webhooks/{id} | webhooks:manage | Revoke endpoint |
Events
| Event | When |
|---|---|
| debrief.completed | When a debrief is ready |
| debrief.updated | Someone edited the debrief in Debrief |
Payload
Every delivery uses this envelope. Recording URLs, share tokens, and transcripts are omitted by default.
{
"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.
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.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).
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/calls | calls:read | Query: since, status, origin_e164, counterparty_e164, cursor, limit |
| GET | /v1/calls/{id} | calls:read | Call object only; unknown or inaccessible → 404 not_found (not 403) |
| GET | /v1/calls/{id}/debrief | debriefs:read | Same debrief object as webhook data.debrief |
| GET | /v1/calls/by-phone/{e164} | calls:read | Matches counterparty E.164 (not origin) |
| GET | /v1/calls/{id}/transcript | transcripts:read | Opt-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
| Parameter | Description |
|---|---|
| origin_e164 | Your side of the call (normalized E.164) |
| counterparty_e164 | The other party’s phone (normalized E.164) |
| since | ISO-8601 datetime — calls created at or after this time |
| status | processing | completed | failed |
| cursor / limit | Cursor 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 type | Visible calls |
|---|---|
| Personal key | Calls you own |
| Organisation key | All 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": {
"code": "not_found",
"message": "Call not found"
}
}| Code | Meaning |
|---|---|
| unauthorized | Missing or invalid API key |
| forbidden | Authenticated but not allowed |
| not_found | Unknown or inaccessible resource (returns 404, not 403) |
| rate_limited | Exceeded 120/minute per key |
| validation_error | Invalid query or body |
| feature_disabled | Developer 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.