Receiving SMS API
Reads the same inbound messages the dashboard shows under Conversations — this isn't a second inbox, it's an API in front of the same rows. This page is the complete contract: every field, its guarantees, and how to sync reliably.
Authentication
Same as every other v1 endpoint, including sending: Authorization: Bearer YOUR_API_KEY, a key generated from the dashboard's API Keys page. There's no separate credential type for reading inbound messages, and no separate scope to request — a key that can send can read what its own SIMs received. See Authentication.
Every result is implicitly scoped to whichever account the key belongs to. If you integrate one Connecfy account per organization (for example, one account per school), a key never needs an explicit "school id" parameter — the key itself is the boundary, enforced server-side, not by anything the caller passes.
Query parameters
All optional. Omit everything for "give me everything, newest first."
| Parameter | Type | Description |
|---|---|---|
from | string | Only messages from this sender number, exact match. |
to | string | Only messages received on this SIM's number, exact match. |
device_id | UUID | Only messages received on this specific SIM — the same id returned as device_id below. |
since | ISO 8601 | Only messages received at or after this time. A simple filter — see Incremental sync for the gap-proof mechanism. |
date_to | ISO 8601 | Only messages received at or before this time. |
cursor | opaque string | Resume exactly from a previous response's next_cursor. This is the reliable incremental-sync mechanism — see below. |
page, page_size | integer | Standard offset paging for casual browsing. page_size caps at 200, defaults to 50. |
There's no after_id. Message ids are random UUIDs, not sequential integers — there's no meaningful "after this id" ordering to seek from directly. cursor is what replaces it: internally it's built from (received_at, id) together, which gives a fully deterministic order without requiring ids to be sequential.
Response schema
The envelope:
| Field | Type | Description |
|---|---|---|
count | integer | Total messages matching your filters — not just this page. |
next | string or null | Ready-to-use URL for the next page. null on the last page. |
previous | string or null | Ready-to-use URL for the previous page. |
next_cursor | string or null | Opaque token for the sync mechanism — see Incremental sync. null only when there are zero results in this response. |
results | array | The messages themselves — schema below. |
Each object in results:
| Field | Type | Can it change later? | Description |
|---|---|---|---|
message_id | UUID string | No — permanent | Unique, permanent identifier for this message. Never reused, never reassigned. This is your deduplication key — see Incremental sync. |
direction | string | No | Always "inbound" on this endpoint. |
from_number | string | No | The external sender's number, as reported by the SIM. |
to | string | No | The receiving SIM's own number, when it's been reported. Can be "" (empty string, never null) if that number isn't known yet — don't treat that as an error. |
message | string | No | The message body. Can be an empty string; never null. |
status | string | No — fixed | Always "received" here. Inbound messages have no further status lifecycle the way outbound ones do. |
device_id | UUID string | No — permanent | Permanent identifier for the SIM/gateway that received it. This is the stable field to associate a message with a specific SIM — combined with the account your API key belongs to, that's the message's full "which school, which SIM" association, with no extra parameter needed. |
device_name | string or null | Yes — cosmetic only | The SIM's current display name. Unlike device_id, this reflects whatever the SIM is named right now — if it's renamed later, old messages will show the new name on a future fetch. Use it for display, never for correlation or deduplication. |
received_at | ISO 8601 datetime | No | Exactly when Connecfy received the report from the gateway. This is what ordering and cursor are based on. |
created_at | ISO 8601 datetime | No | When the database row was written — in practice the same instant as received_at or a few milliseconds later. |
Can a returned message ever change?
With one narrow exception, no. Every field on a results object except device_name is fixed at the moment the message is created and never written to again — this isn't just convention, it's enforced: the two endpoints that update a message's delivery status are explicitly restricted to outbound messages only, so there's no code path anywhere that can modify an inbound row after ingestion. Fetching the same message twice, a hundred times, a year apart, returns identical field values every time.
The one exception is device_name, which is a live label, not a snapshot — if you rename the SIM in the dashboard, every message it ever received will show the new name on the next fetch. Don't key anything off it; use device_id for that.
Timestamp format
Every timestamp (received_at, created_at) is ISO 8601, always UTC, always with an explicit Z designator, at microsecond precision — e.g. 2026-08-27T09:48:26.576653Z. Never local time, never omitted timezone. The format is fixed-width, so two timestamps can be compared correctly as plain strings without parsing them first.
Ordering
Newest-first (received_at descending) by default — the same convention as every other list a Connecfy account looks at. The moment you supply cursor, ordering switches to oldest-of-the-unsynced-batch-first (ascending), since a forward sync loop only makes sense advancing in one direction. Both orders are fully deterministic, including for two messages that arrived in the same instant — ties are broken by message_id, never left ambiguous.
Example request
curl "https://www.connecfy.com/api/v1/sms/received/?page_size=20" \
-H "Authorization: Bearer $CONNECFY_API_KEY"
import os, requests
resp = requests.get(
"https://www.connecfy.com/api/v1/sms/received/",
headers={"Authorization": f"Bearer {os.environ['CONNECFY_API_KEY']}"},
params={"page_size": 20},
)
data = resp.json()
for msg in data["results"]:
print(msg["from_number"], msg["message"])
const res = await fetch("https://www.connecfy.com/api/v1/sms/received/?page_size=20", {
headers: { "Authorization": `Bearer ${process.env.CONNECFY_API_KEY}` },
});
const { results } = await res.json();
results.forEach(m => console.log(m.from_number, m.message));
$ch = curl_init("https://www.connecfy.com/api/v1/sms/received/?page_size=20");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("CONNECFY_API_KEY")],
]);
$data = json_decode(curl_exec($ch), true);
foreach ($data["results"] as $msg) {
echo $msg["from_number"] . ": " . $msg["message"] . "\n";
}
Example response
// 200 OK
{
"count": 1,
"next": null,
"previous": null,
"next_cursor": "MjAyNi0wOC0yN1QxMDoyMjo0MS4xNDc4MDdafDkzNGE2NDFjLTY4MjYtNDcwMy05YWJjLTQ5ZDMyOGMwYWE0Zg==",
"results": [
{
"message_id": "934a641c-6826-4703-9abc-49d328c0aa4f",
"direction": "inbound",
"from_number": "+15551234567",
"to": "+10000000077",
"message": "Thank you for letting us know!",
"status": "received",
"device_id": "19478233-26fe-420b-b93f-8fe71aade79e",
"device_name": "School Office Phone",
"received_at": "2026-08-27T10:22:41.147807Z",
"created_at": "2026-08-27T10:22:41.148121Z"
}
]
}Captured from a real production request during verification — not a hand-written mock.
Incremental sync
For an integration that needs to keep its own copy of received messages up to date — a conversation UI, a sync job — don't re-fetch everything and don't paginate with page numbers for this purpose. An insert happening between two page fetches can shift what an offset-based "page 2" contains, silently repeating or skipping a row. Instead:
- Call the endpoint with whatever filters you want (or none, for everything).
- Process
results— for each message, usemessage_idas your own deduplication key anddevice_idto know which SIM it came in on. - Store
next_cursorfrom the response as your sync checkpoint. - Next time, call again with
?cursor=<stored value>— this returns only messages that arrived after everything you've already seen, however many there are, in order. - Repeat. Store the new
next_cursorafter every call, including empty ones.
Treat cursor as opaqueDon't construct one yourself from a message's received_at — two messages can share an identical timestamp, and a hand-built cursor can't express "and also skip past this specific one." Always use the exact string the API gave you in next_cursor.
next_cursor is still returned on a plain, no-cursor request — it points to "now," so a brand-new integration can do one initial call to establish a starting point, then switch to cursor-only calls from then on without a separate bootstrap step.
Duplicate protection
Handled before a message ever reaches this endpoint: the gateway tags each report with a unique id, and a retried report for the same physical SMS is recognized and never creates a second row — see Receiving SMS. Every message_id this endpoint ever returns corresponds to exactly one physical SMS, permanently. If you're deduplicating on your own side (recommended for any sync job that might be interrupted and resumed), message_id is the only field you need for that — you never need to inspect message content or timestamps to tell two fetches of the same message apart from two different messages.
Associating a message with a SIM (and, in turn, an organization)
device_id is the permanent identifier for which SIM received a message. Since every request is already scoped to the API key's own account, there's no separate "organization id" parameter to pass or check — one Connecfy account per organization (for example, one per school) plus device_id for which of that organization's SIMs is the complete, unambiguous association, enforced server-side rather than trusted from anything the client sends.
Errors
| HTTP | Cause |
|---|---|
| 401 | Missing or invalid API key — see Errors. |
| 400 | A filter failed validation — malformed since/date_to, a device_id that isn't a valid UUID, or a corrupted cursor. |
| 429 | More than 120 requests/minute — see Rate Limits. |
An unrecognized device_id, or one belonging to a different account, is never an error — it's treated like any filter that happens to match nothing, returning an empty results list rather than exposing whether that id exists at all.
Backward compatibility
This endpoint is purely additive. It shares its authentication and account-scoping with Sending SMS API but doesn't change that endpoint's request or response shape in any way — existing send integrations are unaffected by anything on this page.