Authenticating
Every request carries an API key as a bearer token. Keys are issued in the portal, under Settings, API keys: the full key is shown once at issuance and stored hashed, 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, and sending one would not change which organization you reach.
https://api.ringfully.com/api/v1
Issue a key in the portalScopes
A key is issued with a chosen subset of these five. What is enforced on each request is the intersection of that subset and the list below, so a key can never hold something absent from it. Ask GET /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. |
Endpoints
Eleven of them. Responses are JSON, timestamps are ISO 8601 in UTC, and phone numbers are E.164 in and out.
| Method | Path | Scope | What it allows |
|---|---|---|---|
| GET | /me | – | Who this key is, which organization it belongs to, and the scopes it actually holds. The call to make first. |
| GET | /calls | calls.view.team | Calls, newest first. Filter by direction or by start time; page with the cursor the previous page returned. |
| GET | /calls/:id | calls.view.team | One call, by the id the list returns. That is our id for the call, not the carrier’s. |
| GET | /contacts | contacts.view | The shared directory. Numbers the system saw on a call are included by default; pass includeAutoCreated=false for the ones a person entered. |
| POST | /contacts | contacts.manage | Create a contact. If the organization already had this number as an auto-created row, that row is named instead of duplicated: the answer is 200 with adopted true, rather than 201. |
| GET | /contacts/:id | contacts.view | One contact. |
| PATCH | /contacts/:id | contacts.manage | Update a contact. Any subset of the fields, and an empty body is refused so a mistyped field name cannot look like success. |
| DELETE | /contacts/:id | contacts.manage | Delete a contact. 204 on success. |
| GET | /messages | sms.view | One row per conversation, newest activity first. Threads are keyed on the outside number across the whole organization. |
| GET | /messages/:counterpart | sms.view | The messages in one conversation. Reading a thread here does not mark it read: that happens when a person opens it in the softphone. |
| POST | /messages | sms.send | Send a text message as the organization. This bills the organization at its carrier rate, and a number that has replied STOP is refused. |
Rate limits
120 requests per minute per key, and 600 per minute per network before a key is checked. The limit is keyed on the key rather than the organization, so a batch job that needs headroom can have a second key instead of starving the live integration. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so a well behaved client can slow down before it is refused.
What we promise not to break
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.
The machine-readable spec
An OpenAPI 3.1 document, generated from the same schemas the handlers validate against and served unauthenticated. Point a client generator at it.
https://api.ringfully.com/api/v1/openapi.json
Open the OpenAPI documentWhat 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.
Outbound webhooks
Instead of polling, an endpoint you own can receive events. Each delivery carries four headers, and the body is signed with a secret shown once when the endpoint is created.
| X-Ringfully-Event | The event name, for example call.completed. |
| X-Ringfully-Timestamp | Milliseconds since the Unix epoch. Use the header value as an opaque string; do not parse and reformat it. |
| X-Ringfully-Signature | Lowercase hex HMAC-SHA256 over the signed payload below. |
| X-Ringfully-Delivery | Stable across every retry of one delivery. Use it as the idempotency key: delivery is at least once, never exactly once. |
Verifying a delivery
- Read X-Ringfully-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-Ringfully-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.
Deliveries are retried, so treat X-Ringfully-Delivery as the idempotency key and expect to see the same event more than once.
Ask us something about the API