C Connecfy / Docs
v1

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.

GET/api/v1/sms/received/

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."

ParameterTypeDescription
fromstringOnly messages from this sender number, exact match.
tostringOnly messages received on this SIM's number, exact match.
device_idUUIDOnly messages received on this specific SIM — the same id returned as device_id below.
sinceISO 8601Only messages received at or after this time. A simple filter — see Incremental sync for the gap-proof mechanism.
date_toISO 8601Only messages received at or before this time.
cursoropaque stringResume exactly from a previous response's next_cursor. This is the reliable incremental-sync mechanism — see below.
page, page_sizeintegerStandard 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:

FieldTypeDescription
countintegerTotal messages matching your filters — not just this page.
nextstring or nullReady-to-use URL for the next page. null on the last page.
previousstring or nullReady-to-use URL for the previous page.
next_cursorstring or nullOpaque token for the sync mechanism — see Incremental sync. null only when there are zero results in this response.
resultsarrayThe messages themselves — schema below.

Each object in results:

FieldTypeCan it change later?Description
message_idUUID stringNo — permanentUnique, permanent identifier for this message. Never reused, never reassigned. This is your deduplication key — see Incremental sync.
directionstringNoAlways "inbound" on this endpoint.
from_numberstringNoThe external sender's number, as reported by the SIM.
tostringNoThe 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.
messagestringNoThe message body. Can be an empty string; never null.
statusstringNo — fixedAlways "received" here. Inbound messages have no further status lifecycle the way outbound ones do.
device_idUUID stringNo — permanentPermanent 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_namestring or nullYes — cosmetic onlyThe 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_atISO 8601 datetimeNoExactly when Connecfy received the report from the gateway. This is what ordering and cursor are based on.
created_atISO 8601 datetimeNoWhen 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

bash
curl "https://www.connecfy.com/api/v1/sms/received/?page_size=20" \
  -H "Authorization: Bearer $CONNECFY_API_KEY"
python
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"])
javascript
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));
php
$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

JSON
// 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:

  1. Call the endpoint with whatever filters you want (or none, for everything).
  2. Process results — for each message, use message_id as your own deduplication key and device_id to know which SIM it came in on.
  3. Store next_cursor from the response as your sync checkpoint.
  4. 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.
  5. Repeat. Store the new next_cursor after 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

HTTPCause
401Missing or invalid API key — see Errors.
400A filter failed validation — malformed since/date_to, a device_id that isn't a valid UUID, or a corrupted cursor.
429More 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.