Browse by resource
Your key
Calls
Contacts
Text messages
GET /api/v1/calls
curl "https://api.inboundcx.com/api/v1/calls?direction=inbound&limit=1" \
-H "Authorization: Bearer $API_KEY"{
"calls": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"direction": "inbound",
"fromNumber": "+15145550142",
"toNumber": "+14385550199",
"status": "completed",
"startedAt": "2026-09-11T15:04:12.000Z",
"answeredAt": "2026-09-11T15:04:19.000Z",
"endedAt": "2026-09-11T15:07:31.000Z",
"durationSec": 192,
"isInternal": false,
"agentId": "a3f1c2d4-5b6e-4f70-8a9b-0c1d2e3f4a5b",
"contactId": "e2b4c6d8-1a3f-4b5d-9e7c-8f0a2b4c6d8e"
}
],
"nextCursor": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}Keys and scopes
Every request carries an API key as a bearer token. Keys are created in the portal under Settings, API Keys, on the Pro and Business plans. The full key is shown once and stored as a hash, so a lost key is replaced rather than recovered.
Authorization: Bearer rk_live_…
A key belongs to one organization, and the organization is resolved from the key itself. There is no account id to send. If the organization moves to a plan without API access, its keys are refused with 403 until it moves back.
https://api.inboundcx.com/api/v1
Scopes
What is enforced on each request is the intersection of the key’s own scopes and these eight, so a key can never hold something absent from them. A key created in the portal’s form holds the first five: the form takes a name and an expiry, and offers no scope list. The last three are available on request, because there is no way to ask for them yourself today. Ask GET /api/v1/me for the scopes a key actually has, which is how you tell not allowed from not implemented.
| Scope | Access | What it allows |
|---|---|---|
| calls.view.team | Read | Read call history. |
| contacts.view | Read | Read the directory. |
| contacts.manage | Write | Create, change and delete contacts. |
| sms.view | Read | Read text conversations. |
| sms.send | Write | Send text messages, which bills the organization. |
| calls.transcripts.view | Read | Read a call's transcript and its AI summary. Available on request, and each read of a finished transcript is audited. |
| calls.disposition | Write | Set or clear how a call was filed. Available on request, and the change is recorded in the audit log as that key. |
| webhooks.manage | Write | Subscribe and unsubscribe webhooks through the API. Available on request, and the address still has to be one your organization approved. |
The machine-readable description is an OpenAPI 3.1 document, generated from the same schemas the handlers validate against and served without a key.
Open the OpenAPI document11 events, each one signed
Each delivery is an HTTPS POST signed with HMAC-SHA256 over the timestamp and the raw body. A failed one is retried with exponential backoff: 8 attempts in all, over a little more than two hours.
- call.completedA call ended: direction, both numbers, status, who answered, the duration and the times.
- voicemail.receivedA caller left a message and the recording is ready. The transcript comes with it when it has already arrived.
- sms.receivedA text message arrived on one of the organization’s numbers, with its body.
- contact.updatedA contact was edited, or one created automatically from a call was given a name. Consent changes do not raise it yet.
- call.transcribedA finished call's transcript is ready: its id, language and length, never the text. The text is read with the transcript endpoint above.
- call.summarizedThe AI summary of a finished call is ready, with the one word it gives the call and the outcome code it suggests. The summary itself is read from the API.
- call.dispositionedA call's outcome was set, changed or cleared, with its code and label. Notes are never included.
- ai.escalatedAn automated assistant handed a caller to a person, once per conversation, with a reason code. The summary it wrote for that person is never included.
- survey.respondedA caller answered a survey after a call, by keypad or by text, with the score. A written comment is never included, only whether there was one.
- call.recording.pausedA recording was paused, by the person on the call or by your own integration, with the reason code and when it resumes on its own.
- call.recording.resumedA paused recording started again, by hand or at the pause limit. A pause that ended because the recording did raises nothing.
- 8 attemptsRetried with exponential backoff from 1 minute, capped at 60, over a little more than two hours.
- At least onceEvery retry carries the same X-InboundCX-Delivery value. Use it to ignore a repeat.
- Delivery historyEach endpoint in the portal lists its recent deliveries: status, attempts, the next retry and the last error.
The headers
| Header | What it carries |
|---|---|
| X-InboundCX-Event X-Ringfully-Event Deprecated | The event name, for example call.completed. |
| X-InboundCX-Timestamp X-Ringfully-Timestamp Deprecated | Milliseconds since the Unix epoch. Use the header value as an opaque string; do not parse and reformat it. |
| X-InboundCX-Signature X-Ringfully-Signature Deprecated | Lowercase hex HMAC-SHA256 over the signed payload below. |
| X-InboundCX-Delivery X-Ringfully-Delivery Deprecated | Stable across every retry of one delivery. Use it as the idempotency key: delivery is at least once, never exactly once. |
Every delivery also carries the same four headers under their deprecated names, from X-Ringfully-Event to X-Ringfully-Delivery, with identical values. Those names will be removed 12 months after the rename: read the X-InboundCX ones. Nothing else changes, because the signature covers neither name.
An attempt that does not answer with a 2xx within 10 seconds counts as failed, and a redirect is not followed. Delivery is queued, so an event can arrive up to a minute after it happened. The address must be https, on a host your organization has approved.
Verifying a delivery
- Read X-InboundCX-Timestamp as a string, exactly as sent.
- Take the raw request body, before any JSON parsing. Parsing and re-serialising does not round-trip byte for byte and will not match.
- Join them: signedPayload = timestamp + "." + rawBody.
- Compute HMAC-SHA256 of that with your endpoint secret, as lowercase hex.
- Compare it to X-InboundCX-Signature with a constant-time comparison.
- Reject a timestamp more than five minutes from your own clock. Step five proves we sent it; only this step proves we sent it now.
import crypto from "node:crypto";
// Deprecated names with the same values: x-ringfully-timestamp, x-ringfully-signature.
const timestamp = req.get("x-inboundcx-timestamp") ?? "";
const signature = req.get("x-inboundcx-signature") ?? "";
const expected = crypto
.createHmac("sha256", process.env.INBOUNDCX_WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const valid =
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)) &&
Math.abs(Date.now() - Number(timestamp)) < 5 * 60 * 1000;The sample reads the secret from INBOUNDCX_WEBHOOK_SECRET. An integration that keeps it in RINGFULLY_WEBHOOK_SECRET holds the same value, so renaming the variable is optional.
Limits and versioning
Rate limits
120 requests a minute per key, and 600 a minute per network before a key is checked. The limit follows the key rather than the organization, so a batch job can have a key of its own without slowing the live integration.
Responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. A refused request gets 429 with Retry-After.
Versioning
Version 1, at /api/v1, is the only version, served since August 2026. There is no version header to send.
Within v1, fields are added and never removed or renamed. New optional parameters, new response fields and new endpoints can appear at any time, so ignore fields you do not recognise. Anything that would remove or rename a field ships as v2, at a new path.
What version 1 does not cover
Three things are missing on purpose, and each is a decision rather than a queue position.
- Live call control (hold, transfer, conference, record). It only makes sense while somebody is on the call, and there is no realtime channel here yet, so an integration could not know a call was live.
- Recordings and voicemail audio. It is the most sensitive material we hold, and handing it to a credential pasted into a third-party automation tool is not a default.
- Users, roles, billing, phone numbers, call flows and emergency settings. They change the account, and a leaked key must not be able to invite a user, move a number, or edit where a company’s calls go.