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 five, so a key can never hold something absent from them. A key created in the portal holds all five. 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. |
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 documentThree 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.
- 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.