{
  "version": 1,
  "documents": [
    {
      "id": "agents/use-callonline-through-mcp",
      "title": "Use CallOnline through MCP",
      "description": "Connect an MCP client to CallOnline, search public documentation without a key, and authorize call or billing actions with scoped credentials.",
      "summary": "Point an MCP client at /mcp, discover the stateless server or use legacy initialization, use the three public documentation tools for grounded answers, and attach a scoped bearer key only for protected account actions.",
      "contentType": "tutorial",
      "category": "agents",
      "canonicalUrl": "https://callonline.app/resources/agents/use-callonline-through-mcp/",
      "markdownUrl": "https://callonline.app/resources/agents/use-callonline-through-mcp.md",
      "headings": [
        "Use CallOnline through MCP",
        "Discover the server",
        "Public documentation tools",
        "Protected action tools",
        "Approve call placement and cancellation",
        "Agent safety policy"
      ],
      "content": "# Use CallOnline through MCP\n\nCallOnline exposes a stateless MCP endpoint at `https://callonline.app/mcp`. It supports MCP `2026-07-28` discovery and requests plus legacy `2025-11-25`, `2025-06-18`, and `2025-03-26` initialization. The tool catalog covers public documentation, calls, voices, billing readiness, and purchase-session workflows.\n\n<picture>\n  <source srcset=\"/images/optimized/kb-images/agent-integration-1280.avif\" type=\"image/avif\" />\n  <source srcset=\"/images/optimized/kb-images/agent-integration-1280.webp\" type=\"image/webp\" />\n  <img src=\"/kb-images/agent-integration.png\" width=\"1672\" height=\"941\" loading=\"eager\" alt=\"AI agent using MCP public documentation and OpenAPI authorized actions with CallOnline\" />\n</picture>\n\n## Discover the server\n\nModern clients should call `server/discover` first. Every modern POST includes matching `MCP-Protocol-Version` and `Mcp-Method` headers, plus protocol version and client capabilities in `params._meta`. A `tools/call` request also includes an `Mcp-Name` header matching `params.name`.\n\n```json\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"server/discover\",\n  \"params\": {\n    \"_meta\": {\n      \"io.modelcontextprotocol/protocolVersion\": \"2026-07-28\",\n      \"io.modelcontextprotocol/clientCapabilities\": {},\n      \"io.modelcontextprotocol/clientInfo\": {\n        \"name\": \"example-client\",\n        \"version\": \"1.0.0\"\n      }\n    }\n  }\n}\n```\n\nLegacy clients initialize with a supported 2025 protocol version, capabilities, and client information, then send the id-less `notifications/initialized` notification. The endpoint returns `202 Accepted` with an empty body for that legacy notification. In either era, call `tools/list` and use the returned schemas instead of relying on a hard-coded tool list.\n\n```json\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"initialize\",\n  \"params\": {\n    \"protocolVersion\": \"2025-11-25\",\n    \"capabilities\": {},\n    \"clientInfo\": {\n      \"name\": \"example-legacy-client\",\n      \"version\": \"1.0.0\"\n    }\n  }\n}\n```\n\n## Public documentation tools\n\nThese tools read the same published corpus as the website and require no API key:\n\n- `search_documentation(query, category?, limit?)` searches seven public collections. Query length is 2–160 characters; limit defaults to 5 and cannot exceed 10.\n- `get_documentation(pathOrId)` accepts a document ID, canonical resource path, or `.md` companion path and returns the complete Markdown with a canonical citation.\n- `get_api_operation(operationId)` returns the exact method, path, summary, OpenAPI URL, and a related guide when one is mapped.\n\nExample tool call:\n\n```json\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": \"docs-1\",\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"search_documentation\",\n    \"arguments\": {\n      \"query\": \"verify webhook signature\",\n      \"category\": \"webhooks\",\n      \"limit\": 3\n    }\n  }\n}\n```\n\nAn agent should follow search with `get_documentation` before giving detailed implementation advice. That second call supplies the complete current source rather than only a ranking snippet.\n\n## Protected action tools\n\nTools such as `place_call`, `get_call_status`, `cancel_call`, billing status, usage balance, and purchase-session creation call protected account APIs. Send an `Authorization: Bearer ...` header to the MCP request; CallOnline forwards it to the relevant handler.\n\nUse `callonline.calls` for call actions, `callonline.billing` for billing and commerce actions, and the appropriate webhook scope for webhook REST operations. Keep the key in the MCP host's secret store, not in a prompt.\n\n### Approve call placement and cancellation\n\n`place_call` and `cancel_call` use a two-step server approval and durable idempotency flow. Supply an `idempotencyKey` containing 8–120 letters, numbers, periods, underscores, colons, or hyphens. The first call omits `approvalToken` and returns an MCP result whose `status` is `202`, with `approvalRequired`, an opaque token, and its expiry. Show the exact action to the user, obtain approval, and repeat the same tool call with the returned token.\n\n```json\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": \"cancel-1\",\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"cancel_call\",\n    \"arguments\": {\n      \"callId\": \"call_123\",\n      \"idempotencyKey\": \"cancel-call_123-20260803\"\n    }\n  }\n}\n```\n\nThe approval lasts five minutes, can be consumed once, and is bound to the authenticated account, API key, tool, idempotency key, and exact action arguments. Changing any bound value invalidates it. Cancellation also verifies that the call belongs to the authenticated account before issuing approval.\n\nAfter execution, an exact retry with the same idempotency key and arguments replays the stored result without dialing or hanging up again. Reusing a key with different arguments returns a conflict. A pending or indeterminate provider outcome is locked and marked non-retryable; inspect `get_call_status` before deciding whether a genuinely new action is safe.\n\n## Agent safety policy\n\nTool availability is not permission to act. Configure the agent host to:\n\n1. require human confirmation before a live call or purchase;\n2. collect the exact compliance fields from an approved system of record;\n3. never invent consent evidence, local-time context, or caller identity;\n4. stop on a compliance block instead of editing fields to obtain approval;\n5. reuse the original idempotency key for an exact retry and never switch to a new key after a pending or indeterminate outcome;\n6. cite the canonical documentation URL returned by the tool.\n\nDocumentation retrieval can be autonomous because the corpus is public and read-only. Live calls and purchases should remain inside your explicit authorization boundary.\n"
    },
    {
      "id": "agents/use-openapi-with-ai-agent",
      "title": "Use the OpenAPI document with an AI agent",
      "description": "Ground an AI coding or operations agent in the CallOnline OpenAPI contract while keeping approval, compliance, and retry policy outside generated requests.",
      "summary": "Load the production OpenAPI URL as the operation contract, address operations by their stable operationId, validate generated arguments, and pair each action with the relevant human guide and application-side safety policy.",
      "contentType": "guide",
      "category": "agents",
      "canonicalUrl": "https://callonline.app/resources/agents/use-openapi-with-ai-agent/",
      "markdownUrl": "https://callonline.app/resources/agents/use-openapi-with-ai-agent.md",
      "headings": [
        "Use the OpenAPI document with an AI agent",
        "Stable operation IDs",
        "Pair schema with policy",
        "Recommended agent sequence",
        "Keep secrets and raw results bounded",
        "Detect contract drift"
      ],
      "content": "# Use the OpenAPI document with an AI agent\n\nThe canonical contract is [https://callonline.app/openapi.yaml](https://callonline.app/openapi.yaml). Import that URL rather than copying a stale schema into a prompt or repository. The API and documentation MCP operation lookup share the same structured operation registry.\n\n## Stable operation IDs\n\nImportant v1 IDs include:\n\n| Operation ID | Method and path | Purpose |\n| --- | --- | --- |\n| `placeCall` | `POST /v1/calls` | Create a policy-controlled outbound call |\n| `getCall` | `GET /v1/calls/{callId}` | Read current call and audit events |\n| `cancelCall` | `POST /v1/calls/{callId}/cancel` | Stop an active call |\n| `listVoices` | `GET /v1/voices` | Read voice-tier choices |\n| `getCallOnlinePricing` | `GET /v1/pricing` | Read dynamic pricing and packages |\n| `listWebhooks` | `GET /v1/webhooks` | List subscriptions |\n| `createWebhook` | `POST /v1/webhooks` | Create a subscription |\n| `deleteWebhook` | `DELETE /v1/webhooks/{webhookId}` | Delete a subscription |\n\nAgent-commerce operations are also present. Read their current capability response before assuming that a direct payment mode is available.\n\n## Pair schema with policy\n\nOpenAPI describes callable operations, but it cannot prove that a call is appropriate. Add host-level instructions that require:\n\n- validated arguments from trusted application state;\n- explicit human confirmation before the first live call;\n- a server-side bearer key with minimal scopes;\n- refusal to invent or weaken compliance evidence;\n- a stop on `compliance_blocked` or `configuration_error`;\n- reconciliation before any retry of `placeCall`.\n\n## Recommended agent sequence\n\n1. Retrieve `placeCall` through `get_api_operation` or the imported OpenAPI document.\n2. Retrieve the [compliant request guide](/resources/calls/compliant-call-request-fields/).\n3. Ask the application—not the model—for consent and caller records.\n4. Validate the object against the operation schema.\n5. Present destination, objective, purpose, represented organization, recording choice, and estimated billing context for confirmation.\n6. Call the operation once and persist the returned identifier.\n7. Use `getCall` or verified webhooks for later state.\n\n## Keep secrets and raw results bounded\n\nDo not paste API keys, signing secrets, unredacted transcripts, or sensitive metadata into a general-purpose agent conversation. Let the tool host attach credentials and redact results according to your data policy.\n\n## Detect contract drift\n\nFetch the OpenAPI document during CI and compare the operation IDs your integration relies on. If an expected operation disappears or its method/path changes, block deployment for review. Use the documentation guide for semantics and the live OpenAPI document for the current machine contract.\n"
    },
    {
      "id": "authentication/api-key-security",
      "title": "Create and protect a CallOnline API key",
      "description": "Use scoped CallOnline bearer keys safely, keep secrets out of clients and logs, and distinguish unauthorized from insufficient-scope errors.",
      "summary": "Create a key in App9 Account, grant only the scopes the integration needs, store the value in a server-side secret manager, and rotate it immediately if it appears in a client bundle, log, ticket, or repository.",
      "contentType": "guide",
      "category": "authentication",
      "canonicalUrl": "https://callonline.app/resources/authentication/api-key-security/",
      "markdownUrl": "https://callonline.app/resources/authentication/api-key-security.md",
      "headings": [
        "Create and protect a CallOnline API key",
        "Create the narrowest useful key",
        "Send the key from a trusted server",
        "Handle authentication errors",
        "Rotation checklist",
        "Keys and MCP"
      ],
      "content": "# Create and protect a CallOnline API key\n\nCallOnline authenticates protected API operations with an HTTP bearer token. The service hashes the presented token before comparing it with an active key record; revoked keys do not authenticate.\n\n## Create the narrowest useful key\n\nOpen the [App9 Account API key dashboard](https://account.app9.co/dashboard/ai-keys) and create a key for one environment and one integration. Grant only what it uses:\n\n| Scope | Use |\n| --- | --- |\n| `callonline.calls` | Create calls and use call actions |\n| `callonline.webhooks` | Create, list, and delete webhook subscriptions |\n| `callonline.billing` | Read billing readiness and use agent-commerce endpoints |\n| `*` | Full access; reserve for controlled administration |\n\nDo not reuse a production key in local development or staging. Separate keys make rotation and incident review much simpler.\n\n## Send the key from a trusted server\n\n```bash\nexport CALLONLINE_API_KEY=\"replace-with-secret-from-your-vault\"\n\ncurl https://callonline.app/v1/voices \\\n  --header \"Authorization: Bearer $CALLONLINE_API_KEY\"\n```\n\nNever embed the bearer value in browser JavaScript, mobile application resources, screenshots, public agent prompts, source-control history, or query strings. Avoid printing the full value in CI output. If a support workflow needs to identify a key, log a safe internal ID or a short non-secret label instead.\n\n## Handle authentication errors\n\n- HTTP `401` means no active key authenticated. Check the `Bearer` prefix, whitespace, environment, revocation state, and whether the exact secret was truncated.\n- HTTP `403` with a scope message means the key authenticated but lacks the required permission. Create or update a deliberately scoped key instead of switching every integration to `*`.\n- A billing or compliance denial is not fixed by broader key scope. Read the response error and correct the relevant prerequisite.\n\n## Rotation checklist\n\n1. Create a replacement key with the same minimal scopes.\n2. Update the server-side secret in one environment.\n3. Run a non-billable check such as a permitted read endpoint.\n4. Deploy the consumer and confirm the new key is in use.\n5. Revoke the old key.\n6. Review logs and repository history if exposure was possible.\n\nRotate immediately after accidental disclosure. Deleting a message or rewriting git history is not a substitute for revocation.\n\n## Keys and MCP\n\nThe MCP documentation tools search only public material and do not require a key. Action tools such as `place_call` forward the request authorization to the protected API, so an MCP client still needs an appropriately scoped credential for real account actions.\n"
    },
    {
      "id": "billing/credits-billing-and-pricing",
      "title": "Credits, billing readiness, and dynamic pricing",
      "description": "Check CallOnline credit balance, billing readiness, spend controls, connected-minute charges, and live pricing before an outbound call.",
      "summary": "Read /v1/pricing for current rates and packages, use the billing-status endpoint with a billing-scoped key, and confirm available credits, commerce state, spend cap, and compliance readiness before calling.",
      "contentType": "reference",
      "category": "billing",
      "canonicalUrl": "https://callonline.app/resources/billing/credits-billing-and-pricing/",
      "markdownUrl": "https://callonline.app/resources/billing/credits-billing-and-pricing.md",
      "headings": [
        "Credits, billing readiness, and dynamic pricing",
        "Read public pricing",
        "Check account readiness",
        "Connected-minute charging",
        "Credit holds and reservations",
        "Funding flows",
        "Pre-call checklist"
      ],
      "content": "# Credits, billing readiness, and dynamic pricing\n\nCallOnline pricing is dynamic. Do not copy a displayed rate into application logic or documentation. Read the pricing endpoint when presenting current packages or estimating a workflow.\n\n<picture>\n  <source srcset=\"/images/optimized/kb-images/billing-readiness-1280.avif\" type=\"image/avif\" />\n  <source srcset=\"/images/optimized/kb-images/billing-readiness-1280.webp\" type=\"image/webp\" />\n  <img src=\"/kb-images/billing-readiness.png\" width=\"1672\" height=\"941\" loading=\"eager\" alt=\"Dynamic pricing, credit balance, spend cap, and connected usage sequence\" />\n</picture>\n\n## Read public pricing\n\n```bash\ncurl https://callonline.app/v1/pricing\n```\n\nThe response is derived from current App9 credit packages and CallOnline rate-card items. Standard and premium voice tiers can have different credits-per-connected-minute values. If the upstream pricing source is unavailable, the endpoint returns an error rather than inventing a current public price.\n\n## Check account readiness\n\n```bash\ncurl https://callonline.app/v1/agent-commerce/billing-status \\\n  --header \"Authorization: Bearer $CALLONLINE_API_KEY\"\n```\n\nThe key needs `callonline.billing` or `*`. The result includes:\n\n- `billingReady`, based on a numeric available balance greater than zero;\n- the current `creditBalance` response;\n- whether agent commerce is enabled;\n- `monthlySpendCapCents`;\n- whether human approval is required;\n- the commerce kill-switch state;\n- a `requiredBeforeCalling` checklist.\n\nUse `/v1/agent-commerce/usage-balance` when you only need the current balance object.\n\n## Connected-minute charging\n\nCustomer credits are recorded for completed connected usage events, not for a call that never connected. The calculation uses the current tier rate and a 30-second minimum, then rounds the resulting credit amount upward.\n\nTreat this as a billing rule, not a duration guarantee. Carrier events and the pricing source determine the final recorded usage.\n\n## Credit holds and reservations\n\nCallOnline v1 does not expose a public per-call `hold` resource or a hold identifier. Do not write an integration that waits for an undocumented hold object. The supported public readiness surface is the billing-status response; the supported final usage surface is the account balance and recorded connected-minute charge.\n\nIf your own application reserves budget before a call, label it as your application-level reservation and reconcile it after the terminal call state.\n\n## Funding flows\n\n`GET /v1/agent-commerce/plans` lists current fundable plans. `POST /v1/agent-commerce/purchase-session` creates a human-approved checkout session and requires an idempotency key. Agent-commerce capability endpoints indicate which current or preview payment modes are enabled; never assume a preview mode is available from its name alone.\n\nPurchasing credits does not bypass API scope, calling policy, disclosure, DNC, or quiet-hour checks.\n\n## Pre-call checklist\n\n1. Fetch current pricing if showing an estimate.\n2. Confirm `billingReady` and a positive available balance.\n3. Check your own approved budget and the returned monthly spend cap.\n4. Confirm the key has `callonline.calls` for the actual call.\n5. Validate all compliance fields.\n6. Require human approval for the first live workflow and any purchase session.\n\nAvoid automatic funding or calling loops. A billing failure, compliance block, or ambiguous create response should stop the workflow for inspection.\n"
    },
    {
      "id": "calls/api-errors-and-safe-retries",
      "title": "API errors and safe retry limits",
      "description": "Decide which CallOnline API failures can be retried, which require correction, and how to avoid duplicate call creation.",
      "summary": "Retry read operations conservatively after transient failures, but do not blindly retry POST /v1/calls because v1 has no idempotency contract; first reconcile any saved call ID, webhook, or application record.",
      "contentType": "reference",
      "category": "calls",
      "canonicalUrl": "https://callonline.app/resources/calls/api-errors-and-safe-retries/",
      "markdownUrl": "https://callonline.app/resources/calls/api-errors-and-safe-retries.md",
      "headings": [
        "API errors and safe retry limits",
        "Error guide",
        "The call-creation boundary",
        "Bounded read retries",
        "Webhook receiver retries",
        "Purchase-session exception"
      ],
      "content": "# API errors and safe retry limits\n\nUse the HTTP status, `error` value, and operation type together. A retry policy that is safe for a read can be unsafe for a call-creation command.\n\n## Error guide\n\n| Result | Meaning | Default action |\n| --- | --- | --- |\n| `400 invalid_request` | JSON or a field failed schema validation. | Correct the request; do not retry unchanged. |\n| `401 unauthorized` | No active bearer key authenticated. | Fix or rotate the key. |\n| `403 forbidden` | The key lacks a required scope. | Grant the narrow scope or use the correct key. |\n| `403 compliance_blocked` | Product preflight rejected the call. | Inspect every block and correct evidence or context; never auto-bypass. |\n| `404 not_found` | The requested call or resource does not exist. | Check the stored identifier and environment. |\n| `502 dial_failed` | The dial operation failed. | Treat call creation as ambiguous until reconciled. |\n| `502` pricing/source error | A dependent read source is unavailable. | Retry the read with bounded backoff. |\n| `503 configuration_error` | Required calling, usage, or audit configuration is unavailable. | Alert an operator; do not loop call creation. |\n\n## The call-creation boundary\n\n`POST /v1/calls` currently has **no idempotency key or idempotency contract**. The service may create a call record before a later carrier or network failure becomes visible to your client. Repeating the same POST can therefore create another call.\n\nBefore any manual retry:\n\n1. check whether the first response included `id` or `callId`;\n2. inspect your own request ledger for a stored CallOnline ID;\n3. check accepted webhook deliveries for the business operation;\n4. query the known call ID if one exists;\n5. require human review when the outcome remains ambiguous.\n\nCreate a client-side operation ID in `metadata` for correlation, but do not mistake that value for server-enforced idempotency.\n\n## Bounded read retries\n\nFor `GET` operations, use exponential backoff with jitter and a hard limit. One reasonable application policy is 250 ms, 750 ms, and 2 seconds for three additional attempts, followed by a visible degraded state. That example is a client policy, not a CallOnline service-level promise.\n\nDo not retry `400`, `401`, `403`, or a stable `404` unless something relevant changed. Honor any future `Retry-After` header if the API adds one.\n\n## Webhook receiver retries\n\nYour receiver should safely accept duplicate delivery IDs. Return success for a previously accepted delivery instead of reapplying effects. If a webhook is delayed, reconcile with `GET /v1/calls/{callId}` rather than creating another call.\n\n## Purchase-session exception\n\nAgent-commerce purchase session creation has its own idempotency key. That contract applies to purchase sessions only; it does not make `POST /v1/calls` idempotent.\n"
    },
    {
      "id": "calls/call-lifecycle-statuses",
      "title": "Call lifecycle and status values",
      "description": "Understand the ten CallOnline call states, which ones are terminal, and how carrier events update the v1 call record.",
      "summary": "A call can move through dialing, dialed, initiated, ringing, and answered before ending; canceled, dial_failed, compliance_blocked, configuration_error, and ended represent outcomes that require no normal forward transition.",
      "contentType": "reference",
      "category": "calls",
      "canonicalUrl": "https://callonline.app/resources/calls/call-lifecycle-statuses/",
      "markdownUrl": "https://callonline.app/resources/calls/call-lifecycle-statuses.md",
      "headings": [
        "Call lifecycle and status values",
        "Status reference",
        "Status versus events",
        "Cancellation behavior",
        "Recommended application mapping"
      ],
      "content": "# Call lifecycle and status values\n\nTreat the stored status as the current state of the call and the ordered `events` array as its audit trail. Carrier events can arrive asynchronously, so consumers must tolerate repeated reads and should not assume that every intermediate state will be observed.\n\n<picture>\n  <source srcset=\"/images/optimized/kb-images/call-lifecycle-1280.avif\" type=\"image/avif\" />\n  <source srcset=\"/images/optimized/kb-images/call-lifecycle-1280.webp\" type=\"image/webp\" />\n  <img src=\"/kb-images/call-lifecycle.png\" width=\"1672\" height=\"941\" loading=\"eager\" alt=\"Call lifecycle from dialing to ended with blocked, failed, and canceled outcomes\" />\n</picture>\n\n## Status reference\n\n| Status | Meaning | Terminal? |\n| --- | --- | --- |\n| `dialing` | The request passed preflight and CallOnline is starting the dial operation. | No |\n| `dialed` | The carrier accepted the dial request and returned call identifiers. | No |\n| `initiated` | The carrier reported that the call was initiated. | No |\n| `ringing` | The destination is being alerted. | No |\n| `answered` | The destination answered and the connected call can proceed. | No |\n| `ended` | The connected or attempted call has ended. | Yes |\n| `canceled` | A cancel request ended the active CallOnline call. | Yes |\n| `dial_failed` | The dial operation failed before a normal connected lifecycle. | Yes |\n| `compliance_blocked` | Product preflight rejected the call before dialing. | Yes |\n| `configuration_error` | Required calling or usage-tracking configuration was unavailable before dialing. | Yes |\n\nThe normal path is not guaranteed to visit every non-terminal status. For example, a fast carrier update may make polling observe `dialed` and then `answered` without seeing `initiated` or `ringing`.\n\n## Status versus events\n\n`GET /v1/calls/{callId}` returns:\n\n```json\n{\n  \"call\": {\n    \"id\": \"call_opaque_identifier\",\n    \"status\": \"answered\",\n    \"createdAt\": \"2026-07-14T14:30:00.000Z\",\n    \"answeredAt\": \"2026-07-14T14:30:08.000Z\",\n    \"endedAt\": null,\n    \"errorCode\": null,\n    \"errorMessage\": null\n  },\n  \"events\": [\n    { \"eventType\": \"dial\", \"payload\": {} },\n    { \"eventType\": \"dialed\", \"payload\": {} },\n    { \"eventType\": \"call.answered\", \"payload\": {} }\n  ]\n}\n```\n\nUse `call.status` for the current decision and preserve event IDs or delivery IDs for audit and deduplication. Event names can be carrier-oriented (`call.answered`) or product-oriented (`dialed`, `configuration_error`). Do not convert event strings into the call-status enum without an explicit mapping.\n\n## Cancellation behavior\n\nCanceling an active call returns `canceled`. If the stored call is already `ended`, `canceled`, or `compliance_blocked`, the cancel endpoint returns the existing state with `alreadyFinal: true`. A missing call returns `404`.\n\n## Recommended application mapping\n\n- Show `dialing`, `dialed`, `initiated`, and `ringing` as **in progress**.\n- Show `answered` as **connected**.\n- Show `ended` as **finished**.\n- Show `canceled` as **stopped by request**.\n- Send `dial_failed`, `compliance_blocked`, and `configuration_error` to an exception path that displays the supplied error or block details.\n\nDo not label `compliance_blocked` as a transient carrier failure. It requires a policy or evidence correction, not an automatic retry.\n"
    },
    {
      "id": "calls/compliant-call-request-fields",
      "title": "Required fields for a compliant call request",
      "description": "Map every CallOnline v1 call-creation field to its purpose, format, and preflight behavior before sending an outbound request.",
      "summary": "A call request needs a destination, objective, and compliance object containing the true purpose, consent evidence, called-party context, caller identity, disclosure acknowledgements, and recording choice.",
      "contentType": "compliance",
      "category": "calls",
      "canonicalUrl": "https://callonline.app/resources/calls/compliant-call-request-fields/",
      "markdownUrl": "https://callonline.app/resources/calls/compliant-call-request-fields.md",
      "headings": [
        "Required fields for a compliant call request",
        "Top-level fields",
        "Purpose and consent",
        "National DNC context",
        "Called-party context",
        "Caller identity and disclosure",
        "Recording"
      ],
      "content": "# Required fields for a compliant call request\n\nThe schema validates shape and the preflight evaluates product policy. Passing both does not establish that a campaign is lawful in every jurisdiction. Keep the underlying evidence and obtain review appropriate to the call.\n\n<picture>\n  <source srcset=\"/images/optimized/kb-images/compliance-preflight-1280.avif\" type=\"image/avif\" />\n  <source srcset=\"/images/optimized/kb-images/compliance-preflight-1280.webp\" type=\"image/webp\" />\n  <img src=\"/kb-images/compliance-preflight.png\" width=\"1672\" height=\"941\" loading=\"eager\" alt=\"Consent, DNC, local time, identity, disclosure, and recording inputs converging on approval\" />\n</picture>\n\n## Top-level fields\n\n| Field | Required | Rules |\n| --- | --- | --- |\n| `to` | Yes | Destination in E.164 form; preflight rejects invalid numbers. |\n| `objective` | Yes | A non-empty, truthful description used to guide the call and disclosure. |\n| `onBehalfOf` | No | Overrides `compliance.caller.onBehalfOf` when supplied. |\n| `voiceTier` | No | `standard` or `premium`; defaults to `standard`. |\n| `webhookUrl` | No | A valid URL associated with the request. Account webhook subscriptions are managed separately. |\n| `metadata` | No | String-keyed JSON values for your correlation data; do not put secrets here. |\n| `compliance` | Yes | The complete policy context described below. |\n\n## Purpose and consent\n\n`compliance.purpose` must be one of `telemarketing`, `transactional`, `survey`, `political`, `charity`, or `internal_test`. Choose the real purpose; the value changes preflight requirements.\n\nThe optional `consent` object becomes functionally required for approved AI voice calls:\n\n```json\n{\n  \"basis\": \"prior_express_consent\",\n  \"capturedAt\": \"2026-07-10T16:20:00Z\",\n  \"evidenceRef\": \"crm-consent-record-4831\"\n}\n```\n\nSupported basis values are `prior_express_written_consent`, `prior_express_consent`, `established_business_relationship`, `personal_relationship`, `nonprofit_exemption`, and `manual_internal_test`. Current product preflight approves telemarketing only with `prior_express_written_consent`; non-telemarketing purposes require prior express or prior express written consent; internal tests accept `manual_internal_test` or prior express written consent.\n\nThe timestamp must parse, include an offset, and not be in the future. `evidenceRef` must identify an auditable record.\n\n## National DNC context\n\nFor telemarketing, include:\n\n```json\n{\n  \"scrubbedAt\": \"2026-07-12T10:00:00Z\",\n  \"subscriptionAccountNumber\": \"optional-account-reference\"\n}\n```\n\nThe current product check requires a valid scrub timestamp no more than 31 days old. CallOnline also checks its account-specific do-not-call store for non-internal-test calls.\n\n## Called-party context\n\n`calledParty.timeZone` is required and should be an IANA time-zone identifier. Product quiet-hour checks block non-internal-test calls before 8:00 a.m. and at or after 9:00 p.m. in that time zone. `state` is optional and exactly two characters. `type` can be `wireless`, `residential`, `business`, or `unknown` and defaults to `unknown`.\n\n## Caller identity and disclosure\n\nThe `caller` object requires:\n\n- `legalName`: the responsible caller's legal name;\n- `onBehalfOf`: the organization represented in the call;\n- `callbackNumber`: an E.164 callback number;\n- `aiDisclosureAcknowledged: true`;\n- `dncPolicyAcknowledged: true`.\n\nThese acknowledgements are literal `true` values, not strings. The product generates an AI disclosure script from the organization, objective, purpose, and recording choice.\n\n## Recording\n\n`recording.enabled` defaults to `false`. `twoPartyConsentRequired` is optional context. Turning recording on can add obligations that vary by location and call circumstances; do not enable it by default without an approved recording policy.\n\n> **Not legal advice:** CallOnline validates documented product requirements and fails closed for specific conditions. Your organization remains responsible for legal basis, suppression sources, disclosure content, recording consent, retention, and campaign review.\n"
    },
    {
      "id": "getting-started/first-outbound-call",
      "title": "Place your first outbound AI phone call",
      "description": "Create a policy-controlled CallOnline request, understand the 201 response, and inspect its state without placing duplicate calls.",
      "summary": "Send one authenticated POST request with a destination, objective, consent evidence, caller identity, local-time context, and recording choice; save the returned call ID before polling or waiting for a webhook.",
      "contentType": "tutorial",
      "category": "getting-started",
      "canonicalUrl": "https://callonline.app/resources/getting-started/first-outbound-call/",
      "markdownUrl": "https://callonline.app/resources/getting-started/first-outbound-call.md",
      "headings": [
        "Place your first outbound AI phone call",
        "1. Check prerequisites",
        "2. Send one request",
        "3. Save the accepted result",
        "4. Inspect the call",
        "If the request is blocked"
      ],
      "content": "# Place your first outbound AI phone call\n\nThis tutorial uses a placeholder destination. Do not paste a real phone number into an example until your organization has approved the workflow, the intended recipient has the required consent relationship, and billing is ready.\n\n<picture>\n  <source srcset=\"/images/optimized/kb-images/call-flow-1280.avif\" type=\"image/avif\" />\n  <source srcset=\"/images/optimized/kb-images/call-flow-1280.webp\" type=\"image/webp\" />\n  <img src=\"/kb-images/call-flow.png\" width=\"1672\" height=\"941\" loading=\"eager\" alt=\"Request, policy preflight, dialing, and structured result stages\" />\n</picture>\n\n## 1. Check prerequisites\n\nConfirm the following before sending a call request:\n\n- the bearer key includes `callonline.calls` or `*`;\n- the destination and callback number use E.164 format, such as `+15551234567`;\n- the purpose matches the actual call;\n- `capturedAt` is an ISO 8601 timestamp with an offset and is not in the future;\n- `evidenceRef` points to your auditable consent record;\n- the called-party time zone is an IANA name such as `America/New_York`;\n- the account has enough available credits.\n\n## 2. Send one request\n\nThe internal-test example below is suitable for a controlled test number you own. Replace every placeholder deliberately.\n\n```bash\ncurl https://callonline.app/v1/calls \\\n  --request POST \\\n  --header \"Authorization: Bearer $CALLONLINE_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"to\": \"+15551234567\",\n    \"objective\": \"Confirm that the CallOnline test workflow is working\",\n    \"voiceTier\": \"standard\",\n    \"metadata\": { \"testCase\": \"first-call-fixture\" },\n    \"compliance\": {\n      \"purpose\": \"internal_test\",\n      \"consent\": {\n        \"basis\": \"manual_internal_test\",\n        \"capturedAt\": \"2026-07-14T14:30:00Z\",\n        \"evidenceRef\": \"test-owner-approval-001\"\n      },\n      \"calledParty\": {\n        \"timeZone\": \"America/New_York\",\n        \"state\": \"FL\",\n        \"type\": \"wireless\"\n      },\n      \"caller\": {\n        \"legalName\": \"Example Company LLC\",\n        \"onBehalfOf\": \"Example Company\",\n        \"callbackNumber\": \"+15557654321\",\n        \"aiDisclosureAcknowledged\": true,\n        \"dncPolicyAcknowledged\": true\n      },\n      \"recording\": {\n        \"enabled\": false\n      }\n    }\n  }'\n```\n\n## 3. Save the accepted result\n\nAn approved request that starts dialing returns HTTP `201`. The representative shape is:\n\n```json\n{\n  \"id\": \"call_opaque_identifier\",\n  \"status\": \"dialed\",\n  \"telnyxCallControlId\": \"redacted\",\n  \"telnyxCallSessionId\": \"redacted\",\n  \"compliance\": {\n    \"approved\": true,\n    \"aiDisclosureScript\": \"Hi, this is Jaxia, an AI assistant calling for Example Company...\"\n  }\n}\n```\n\nStore `id` as your durable CallOnline identifier. Carrier identifiers are operational details; do not use them as your primary application key.\n\n## 4. Inspect the call\n\n```bash\ncurl \"https://callonline.app/v1/calls/call_opaque_identifier\" \\\n  --header \"Authorization: Bearer $CALLONLINE_API_KEY\"\n```\n\nThe response includes `call` and an ordered `events` array. Continue only until a terminal status appears. Prefer a webhook for production notification and use polling as reconciliation.\n\n## If the request is blocked\n\nAn HTTP `403` with `error: \"compliance_blocked\"` includes a `callId`, a list of blocks, and the proposed disclosure script. Do not retry unchanged. Fix the underlying evidence or context, complete any internal approval your organization's policy requires, and create a new request only when it is appropriate.\n\nAn HTTP `502` or `503` can be ambiguous. Call creation has **no idempotency contract in v1**. Before sending another POST, check whether your application already stored a call ID or received a webhook. Blind retries can place duplicate calls.\n"
    },
    {
      "id": "getting-started/product-overview",
      "title": "What CallOnline does and when to use it",
      "description": "Learn which outbound AI phone workflows fit CallOnline, what the API returns, and which responsibilities remain with your application.",
      "summary": "CallOnline gives an authorized AI workflow a phone interface for outbound calls, policy preflight, status tracking, and structured events; it is best for specific, reviewable tasks rather than unrestricted calling.",
      "contentType": "concept",
      "category": "getting-started",
      "canonicalUrl": "https://callonline.app/resources/getting-started/product-overview/",
      "markdownUrl": "https://callonline.app/resources/getting-started/product-overview.md",
      "headings": [
        "What CallOnline does and when to use it",
        "Good fits",
        "Poor fits",
        "What the product controls",
        "What your application still owns",
        "Next step"
      ],
      "content": "# What CallOnline does and when to use it\n\nCallOnline is an outbound phone API for AI-assisted workflows. A caller submits a defined objective and the context required for policy checks; CallOnline records the preflight result, starts an approved call, and exposes its current status and audit events.\n\n## Good fits\n\nUse CallOnline when the phone task has a clear owner, a defined purpose, and an outcome your application can review. Examples include:\n\n- an appointment reminder to a person who expects the call;\n- a transactional follow-up connected to an existing customer request;\n- a consented survey with an explicit script and stop path;\n- an internal test to a controlled number using the internal-test consent basis;\n- a bounded support or operations workflow where a human can inspect exceptions.\n\nThe application initiating the call should know why the call is allowed, which organization is represented, how a person can call back, and what should happen after each possible outcome.\n\n## Poor fits\n\nDo not treat CallOnline as permission to call arbitrary numbers. Avoid workflows that cannot provide auditable consent, cannot identify the represented organization, intentionally obscure AI disclosure, ignore a do-not-call request, or operate without a responsible human owner.\n\nCallOnline performs product-level preflight checks, but those checks are not legal advice and cannot determine every obligation for every location, industry, audience, or campaign. Your organization remains responsible for its calling policy and applicable obligations.\n\n## What the product controls\n\nBefore dialing, the v1 request supplies the destination, objective, purpose, consent evidence, called-party time zone, caller identity, callback number, disclosure acknowledgements, and recording configuration. Product checks can reject malformed numbers, missing consent evidence, stale telemarketing suppression data, local quiet hours, account-specific do-not-call entries, or an unavailable compliance store.\n\nIf the request is approved, the call progresses through observable states. Your system can poll `GET /v1/calls/{callId}` or subscribe to outbound webhooks. The call record contains timestamps, current status, compliance outcome, error details when present, and ordered audit events.\n\n## What your application still owns\n\nYour application should:\n\n1. collect and retain lawful, auditable permission before creating the request;\n2. choose a narrowly scoped API key and protect it on a trusted server;\n3. enforce human approval, account policy, and spend limits before live use;\n4. handle ambiguous network failures without blindly creating duplicate calls;\n5. verify webhooks and deduplicate them before changing business state;\n6. route blocks, failed calls, and sensitive outcomes to an accountable operator.\n\n> **Compliance note:** The examples in this knowledge base describe how CallOnline fields and controls behave. They do not replace advice from qualified counsel about a particular campaign.\n\n## Next step\n\nReview the [compliant call request fields](/resources/calls/compliant-call-request-fields/) before following the [first outbound call tutorial](/resources/getting-started/first-outbound-call/).\n"
    },
    {
      "id": "index",
      "title": "CallOnline knowledge base",
      "description": "Build reliable, policy-controlled AI phone workflows with CallOnline guides, references, troubleshooting, OpenAPI, and MCP resources.",
      "summary": "Start with a tested workflow, then use focused references for call states, compliance inputs, webhooks, agent integrations, and billing readiness.",
      "contentType": "guide",
      "category": "getting-started",
      "canonicalUrl": "https://callonline.app/resources/",
      "markdownUrl": "https://callonline.app/resources/index.md",
      "headings": [
        "CallOnline knowledge base",
        "Start here",
        "Integrate events and agents",
        "Machine-readable access"
      ],
      "content": "# CallOnline knowledge base\n\n<div class=\"kb-hero\">\n  <div>\n    <span class=\"kb-section-label\">Documentation for people and agents</span>\n    <h2>Build an AI calling workflow you can inspect and operate safely.</h2>\n    <p>Use task-focused guides for the first request, exact API references for production behavior, and Markdown or MCP retrieval when an AI agent needs the same source material.</p>\n    <div class=\"kb-hero-actions\">\n      <a class=\"kb-button\" href=\"/resources/getting-started/first-outbound-call/\">Place your first call</a>\n      <a class=\"kb-button secondary\" href=\"/openapi.yaml\">Open API reference</a>\n    </div>\n  </div>\n  <div class=\"kb-terminal\" aria-label=\"Example CallOnline workflow\">\n    POST /v1/calls<br />\n    → compliance preflight<br />\n    → status: dialed<br />\n    → webhook delivery<br />\n    → structured call result\n  </div>\n</div>\n\n## Start here\n\n<div class=\"kb-grid\">\n  <a class=\"kb-card\" href=\"/resources/getting-started/product-overview/\"><strong>Understand CallOnline</strong><span>Learn which outbound workflows fit the product and where human review still belongs.</span></a>\n  <a class=\"kb-card\" href=\"/resources/getting-started/first-outbound-call/\"><strong>Place the first outbound call</strong><span>Build a compliant request, interpret the response, and track the call without retrying unsafely.</span></a>\n  <a class=\"kb-card\" href=\"/resources/authentication/api-key-security/\"><strong>Protect API access</strong><span>Create scoped keys, keep them server-side, rotate exposure, and diagnose authorization errors.</span></a>\n  <a class=\"kb-card\" href=\"/resources/calls/call-lifecycle-statuses/\"><strong>Track every call state</strong><span>Distinguish progress states from final outcomes such as ended, canceled, blocked, or failed.</span></a>\n</div>\n\n## Integrate events and agents\n\n<div class=\"kb-grid\">\n  <a class=\"kb-card\" href=\"/resources/webhooks/verify-signatures-and-retries/\"><strong>Verify webhook deliveries</strong><span>Authenticate the raw request body with HMAC-SHA256 and deduplicate delivery IDs.</span></a>\n  <a class=\"kb-card\" href=\"/resources/agents/use-callonline-through-mcp/\"><strong>Connect through MCP</strong><span>Let an agent discover public documentation and authorized call tools from one endpoint.</span></a>\n  <a class=\"kb-card\" href=\"/resources/agents/use-openapi-with-ai-agent/\"><strong>Ground an agent with OpenAPI</strong><span>Use operation IDs and the live contract while keeping approval and retry policy in your application.</span></a>\n  <a class=\"kb-card\" href=\"/resources/billing/credits-billing-and-pricing/\"><strong>Check billing readiness</strong><span>Inspect dynamic pricing, available credits, spend caps, and checkout readiness before a call.</span></a>\n</div>\n\n## Machine-readable access\n\n- Browse the canonical [OpenAPI document](/openapi.yaml).\n- Retrieve the complete public corpus from [docs-index.json](/resources/docs-index.json).\n- Use [llms.txt](/llms.txt) for a concise map of Markdown companions.\n- Connect an MCP client to `https://callonline.app/mcp` and call the public documentation tools without an API key.\n\nNo file or schema guarantees an AI citation. These surfaces make CallOnline content crawlable, stable, directly answerable, and easy to cite accurately.\n"
    },
    {
      "id": "troubleshooting/compliance-preflight-blocks",
      "title": "Troubleshoot compliance and preflight blocks",
      "description": "Diagnose every current CallOnline compliance block code and correct the underlying request or evidence without bypassing policy controls.",
      "summary": "Read every item in the 403 blocks array, fix the actual phone format, consent record, DNC context, local time, or compliance-store issue, and submit a new call only after the workflow is approved.",
      "contentType": "troubleshooting",
      "category": "troubleshooting",
      "canonicalUrl": "https://callonline.app/resources/troubleshooting/compliance-preflight-blocks/",
      "markdownUrl": "https://callonline.app/resources/troubleshooting/compliance-preflight-blocks.md",
      "headings": [
        "Troubleshoot compliance and preflight blocks",
        "Block-code reference",
        "Safe diagnostic sequence",
        "What not to do"
      ],
      "content": "# Troubleshoot compliance and preflight blocks\n\nAn HTTP `403` with `error: \"compliance_blocked\"` means no normal dial was started. The response includes a `callId`, all detected `blocks`, and the disclosure script generated from the submitted context. Preserve that response for review.\n\n## Block-code reference\n\n| Code | Cause | Corrective action |\n| --- | --- | --- |\n| `invalid_to_number` | Destination is not valid E.164. | Normalize and validate the real destination; do not guess digits. |\n| `invalid_callback_number` | Callback number is not valid E.164. | Supply a monitored, non-premium callback number. |\n| `missing_ai_voice_consent` | Purpose and consent basis do not meet current product policy. | Obtain and record the required consent; do not relabel the purpose. |\n| `missing_consent_evidence` | Timestamp is invalid/future or evidence reference is empty. | Repair the source record and submit its durable reference. |\n| `national_dnc_scrub_required` | Telemarketing DNC timestamp is missing, invalid, future, or older than 31 days. | Perform the approved suppression process and record its current timestamp. |\n| `quiet_hours` | Called-party local time is before 8 a.m. or at/after 9 p.m. | Schedule outside the blocked window. |\n| `invalid_time_zone` | Time-zone value cannot be evaluated. | Use the called party's valid IANA time zone. |\n| `entity_dnc_blocked` | Number is on the account-specific do-not-call list. | Do not call; route to the responsible policy owner if the record is disputed. |\n| `compliance_store_unavailable` | Required DNC/compliance storage could not be checked. | Wait for service recovery; the system intentionally fails closed. |\n\nOne request can contain multiple blocks. Fixing the first item does not mean the rest are cleared.\n\n## Safe diagnostic sequence\n\n1. Log the `callId` and block codes, not the full consent evidence or secrets.\n2. Compare the request with the source-of-truth consent and caller records.\n3. Confirm the stated purpose matches the intended conversation.\n4. Validate phone numbers and the IANA time zone independently.\n5. For telemarketing, verify the current approved DNC-suppression process.\n6. Escalate do-not-call matches or legal uncertainty to the assigned owner.\n7. Create a new request only after the underlying facts changed and review allows it.\n\n## What not to do\n\nDo not switch `telemarketing` to `transactional`, change the called-party time zone, use `internal_test`, fabricate an evidence reference, or remove recording context merely to pass preflight. Those actions make the request inaccurate and can create additional risk.\n\n> **Not legal advice:** This table documents current CallOnline product behavior. A qualified reviewer should determine the requirements for your organization, campaign, audience, and locations.\n"
    },
    {
      "id": "troubleshooting/missing-delayed-webhooks",
      "title": "Troubleshoot missing or delayed webhooks",
      "description": "Diagnose CallOnline webhook subscription filters, authentication, receiver responses, duplicate handling, and call-state reconciliation.",
      "summary": "Confirm the subscription is active and matches the emitted event, inspect receiver status and signature verification, deduplicate rather than discard repeats, and reconcile the call through GET /v1/calls/{callId}.",
      "contentType": "troubleshooting",
      "category": "troubleshooting",
      "canonicalUrl": "https://callonline.app/resources/troubleshooting/missing-delayed-webhooks/",
      "markdownUrl": "https://callonline.app/resources/troubleshooting/missing-delayed-webhooks.md",
      "headings": [
        "Troubleshoot missing or delayed webhooks",
        "1. Reconcile the call first",
        "2. Confirm the subscription",
        "3. Inspect receiver behavior",
        "4. Expect repeats and imperfect order",
        "5. Replace a broken subscription safely"
      ],
      "content": "# Troubleshoot missing or delayed webhooks\n\nA webhook can appear missing because the event was not selected, the endpoint was unreachable, signature verification used a transformed body, the receiver returned a non-success response, or the application discarded a duplicate or out-of-order event incorrectly.\n\n<picture>\n  <source srcset=\"/images/optimized/kb-images/webhook-troubleshooting-1280.avif\" type=\"image/avif\" />\n  <source srcset=\"/images/optimized/kb-images/webhook-troubleshooting-1280.webp\" type=\"image/webp\" />\n  <img src=\"/kb-images/webhook-troubleshooting.png\" width=\"1672\" height=\"941\" loading=\"eager\" alt=\"Expected event checked against the call record, webhook receiver, and reconciliation loop\" />\n</picture>\n\n## 1. Reconcile the call first\n\nIf you know the call ID, read the authoritative stored state:\n\n```bash\ncurl \"https://callonline.app/v1/calls/call_opaque_identifier\" \\\n  --header \"Authorization: Bearer $CALLONLINE_API_KEY\"\n```\n\nThe ordered `events` array tells you whether CallOnline recorded the expected event. If it is not present there, the webhook subscription cannot deliver it.\n\n## 2. Confirm the subscription\n\nCall `GET /v1/webhooks` with a key that has `callonline.webhooks`. Verify:\n\n- the URL is the intended environment and HTTPS route;\n- the subscription is active;\n- `eventTypes` contains the exact event string or `*`;\n- the receiver is using the signing secret for that subscription.\n\n`dialed` and `call.answered` are different event strings. Do not filter only on call-status values if you need carrier-oriented events.\n\n## 3. Inspect receiver behavior\n\nCheck the receiver's edge and application logs for the delivery ID and event header. Common causes are:\n\n- a redirect to a login or trailing-slash URL;\n- a firewall, allowlist, TLS, DNS, or timeout failure;\n- reading parsed JSON before signature verification;\n- comparing uppercase hex or omitting the `sha256=` prefix;\n- returning `500` after durable acceptance, causing a repeat;\n- returning `200` before durable acceptance, then losing the event.\n\nRedact the secret, bearer keys, and sensitive body fields from logs.\n\n## 4. Expect repeats and imperfect order\n\nUse `x-callonline-delivery` as a unique key. A repeated delivery should return success after confirming the original was accepted. Do not require the previous lifecycle event to have arrived before accepting a newer one; network timing can differ.\n\nWhen business state depends on order, compare the event with the current call record and its timestamps instead of trusting arrival order alone.\n\n## 5. Replace a broken subscription safely\n\nCreate a new subscription and secret, deploy the new receiver configuration, verify a fixture delivery in a non-billable test, and then delete the old subscription. Do not expose a production secret in a manual replay tool.\n\nCallOnline records failed attempts and a future retry time internally, but the public v1 contract does not promise an exact retry schedule. Build recovery around reconciliation, not a countdown.\n"
    },
    {
      "id": "webhooks/receive-call-status-webhooks",
      "title": "Receive call-status webhooks",
      "description": "Create a CallOnline webhook subscription, select event types, inspect delivery headers, and acknowledge events safely.",
      "summary": "Register an HTTPS endpoint with one or more event types, save the returned signing secret, verify each raw delivery, deduplicate its delivery ID, and return a successful response only after durable acceptance.",
      "contentType": "tutorial",
      "category": "webhooks",
      "canonicalUrl": "https://callonline.app/resources/webhooks/receive-call-status-webhooks/",
      "markdownUrl": "https://callonline.app/resources/webhooks/receive-call-status-webhooks.md",
      "headings": [
        "Receive call-status webhooks",
        "Create a subscription",
        "Delivery shape",
        "Acknowledge after durable acceptance",
        "List or delete subscriptions"
      ],
      "content": "# Receive call-status webhooks\n\nWebhook subscriptions belong to the authenticated account. A subscription contains a destination URL, an event-type filter, an active status, and a signing secret used to authenticate deliveries.\n\n## Create a subscription\n\n```bash\ncurl https://callonline.app/v1/webhooks \\\n  --request POST \\\n  --header \"Authorization: Bearer $CALLONLINE_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"url\": \"https://example.com/webhooks/callonline\",\n    \"eventTypes\": [\"dialed\", \"call.answered\", \"call.ended\", \"dial_failed\"]\n  }'\n```\n\n`eventTypes` must contain at least one non-empty string. Use `\"*\"` to receive all product and carrier-oriented call events. A successful create returns HTTP `201` with `id`, `url`, `eventTypes`, `status`, `signingSecret`, and `createdAt`.\n\nStore `signingSecret` in a server-side secret manager. You can provide a secret of at least 16 characters when creating the subscription, or let CallOnline generate one.\n\n## Delivery shape\n\nEach delivery is an HTTP `POST` with JSON similar to:\n\n```json\n{\n  \"id\": \"delivery-uuid\",\n  \"type\": \"call.answered\",\n  \"callId\": \"call-uuid\",\n  \"createdAt\": \"2026-07-14T14:30:08.000Z\",\n  \"data\": {},\n  \"call\": {\n    \"to\": \"+19045550101\",\n    \"from\": \"+19045550100\",\n    \"status\": \"answered\",\n    \"objective\": \"Qualify this lead and schedule an estimate.\",\n    \"onBehalfOf\": \"Acme Services\",\n    \"metadata\": {\n      \"workflowId\": \"lead-interceptor\",\n      \"deploymentId\": \"deployment-acme\"\n    }\n  }\n}\n```\n\n`data` contains the event-specific payload. `call` carries stable call context and\nthe metadata supplied to `POST /v1/calls`, so receivers can correlate lifecycle\nevents with the originating workflow without querying CallOnline first.\n\nThe request includes:\n\n| Header | Purpose |\n| --- | --- |\n| `x-callonline-delivery` | Unique delivery identifier for deduplication |\n| `x-callonline-event` | Event type used by the subscription filter |\n| `x-callonline-signature` | `sha256=` plus the lowercase HMAC-SHA256 digest of the raw body |\n| `content-type` | `application/json` |\n\n## Acknowledge after durable acceptance\n\nThe receiver should perform a small, reliable sequence:\n\n1. Read the raw request bytes without transforming them.\n2. Verify the signature before parsing or trusting fields.\n3. Check whether `x-callonline-delivery` was already accepted.\n4. Store the event or enqueue it transactionally.\n5. Return a `2xx` response quickly.\n6. Process slower business logic asynchronously.\n\nAny non-success response is treated as a failed delivery. Your endpoint should not return success before it can recover the accepted event after a crash.\n\n## List or delete subscriptions\n\nList active and non-deleted subscriptions with `GET /v1/webhooks`. Delete one with `DELETE /v1/webhooks/{webhookId}`. Deletion returns the numeric ID and `status: \"deleted\"`.\n\nFor recovery, reconcile important call IDs with `GET /v1/calls/{callId}`. A webhook is a notification channel, not the only authoritative way to inspect the stored call.\n"
    },
    {
      "id": "webhooks/verify-signatures-and-retries",
      "title": "Verify webhook signatures and handle retries",
      "description": "Verify CallOnline HMAC-SHA256 webhook signatures against the raw body, deduplicate delivery IDs, and make retry handling safe.",
      "summary": "Compute HMAC-SHA256 with the subscription secret and exact raw body, compare the lowercase `sha256=` value in constant time, then deduplicate `x-callonline-delivery` before applying side effects.",
      "contentType": "guide",
      "category": "webhooks",
      "canonicalUrl": "https://callonline.app/resources/webhooks/verify-signatures-and-retries/",
      "markdownUrl": "https://callonline.app/resources/webhooks/verify-signatures-and-retries.md",
      "headings": [
        "Verify webhook signatures and handle retries",
        "Expected signature format",
        "Node.js verification example",
        "Make processing idempotent",
        "Retry behavior",
        "Secret rotation"
      ],
      "content": "# Verify webhook signatures and handle retries\n\nThe signature covers the exact JSON string sent by CallOnline. Parsing and re-serializing JSON can change whitespace or key formatting and produce a different digest, so verification must use the raw body.\n\n<picture>\n  <source srcset=\"/images/optimized/kb-images/webhook-verification-1280.avif\" type=\"image/avif\" />\n  <source srcset=\"/images/optimized/kb-images/webhook-verification-1280.webp\" type=\"image/webp\" />\n  <img src=\"/kb-images/webhook-verification.png\" width=\"1672\" height=\"941\" loading=\"eager\" alt=\"Raw webhook body verified with HMAC-SHA256, deduplicated, and durably accepted\" />\n</picture>\n\n## Expected signature format\n\nThe header is exactly `sha256=` followed by 64 lowercase hexadecimal characters:\n\n```text\nsha256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n```\n\nReject a missing header, a malformed value, or a digest that does not match. Do not log the subscription secret or full signature in an error message.\n\n## Node.js verification example\n\n```ts\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nexport function verifyCallOnlineWebhook(\n  rawBody: Buffer,\n  signatureHeader: string | null,\n  secret: string,\n): boolean {\n  if (!signatureHeader || !/^sha256=[0-9a-f]{64}$/.test(signatureHeader)) {\n    return false;\n  }\n\n  const expected = `sha256=${createHmac(\"sha256\", secret)\n    .update(rawBody)\n    .digest(\"hex\")}`;\n  const receivedBytes = Buffer.from(signatureHeader, \"utf8\");\n  const expectedBytes = Buffer.from(expected, \"utf8\");\n\n  return (\n    receivedBytes.length === expectedBytes.length &&\n    timingSafeEqual(receivedBytes, expectedBytes)\n  );\n}\n```\n\nCapture the raw bytes using the primitive provided by your framework. For example, read `request.arrayBuffer()` before calling `request.json()` in a Fetch-compatible server.\n\n## Make processing idempotent\n\nVerification proves the sender knew the secret; it does not guarantee a delivery is new. Use `x-callonline-delivery` as a unique key:\n\n```sql\nINSERT INTO accepted_webhooks (delivery_id, event_type, call_id, body)\nVALUES (?, ?, ?, ?)\nON CONFLICT (delivery_id) DO NOTHING;\n```\n\nOnly the first insert should enqueue or apply business effects. Later attempts should return success after confirming the original was accepted.\n\n## Retry behavior\n\nCallOnline records a failed attempt when the endpoint returns a non-`2xx` response, stores the error, and calculates a progressively delayed next-attempt time capped internally. The public API does not promise an exact delivery schedule or maximum attempt count, so do not build time-sensitive logic around a fixed retry timetable.\n\nDesign the receiver so any delivery can be delayed, repeated, or followed by a newer event. Reconcile the call record when order matters.\n\n## Secret rotation\n\nWebhook v1 does not expose a separate rotate-secret operation. Create a replacement subscription with a new secret, deploy support for it, verify deliveries, and then delete the old subscription. During the overlap, associate each endpoint or subscription ID with the correct secret.\n"
    }
  ]
}
