Theme
Open the portal

Service API reference

The machine-to-machine company-data API: list connections, read connection values as ciphertext, fetch binary answers, consume the drain-on-fetch change feed, read your service activity log, and resolve request-field definitions.

This is the service API (/api/company-data/*) — the surface most developers integrate. It is read-only and machine-to-machine: your backend authenticates as a registered service API client, and every response is scoped to that client's service. The platform is zero-knowledge: person field values are returned as ciphertext that you decrypt off-platform with your service private key. The SDKs wrap all of this — use them unless you have a reason not to.

Open the live Service API explorer →

Base URL & auth

curl -s https://api-eu.allme.fyi/api/company-data/connections \
  -H "Authorization: Bearer ey..."

The examples further down write the base as https://api-eu.allme.fyi for brevity. A real integration joins each path to the api_url its token response returned — an SDK does that for you.

JSON or XML

All /api/company-data/* responses serialize as JSON (default) or XML. Choose per request with the Accept header or the ?format= query parameter:

SelectHow
JSON (default)Accept: application/json or ?format=json or nothing
XMLAccept: application/xml or ?format=xml

The XML and JSON carry the same fields — the change-feed columns map 1:1 to both, so a record materializes the same either way. (The SDKs hide the wire format entirely; you set format once in config and never see it.)

Rate-limit tiers

Poll changes freely; treat connections as expensive. The endpoints are tiered on purpose. Polling the change feed is the intended steady-state pattern; the connections endpoints are a heavy full-read and are tightly rate-limited.
EndpointTierUse it for
changesGenerousPoll as often as you like — it is a cheap drain-on-fetch queue. This is the steady-state path.
request-fields, logsModerateOccasional reads (the catalog is fetched once and cached).
connections, connections/{id} (the read and the delete), binary /fileHeavily limitedInitial full sync + occasional reconciliation only — never as a poll substitute for changes.

A 429 response carries a Retry-After header (seconds), plus error_key: "rate_limit.exceeded". Back off and retry; the SDK transport does this for you and the connections iterator paces itself within the limit. A 503 means the rate limiter's own store could not be read (carries error_key: "rate_limit.unavailable") — retry after the same Retry-After window.

Your token refresh (POST /oauth2/token with grant_type=client_credentials) is on its own rate-limit bucket, generously sized and kept separate from person logins. That bucket is keyed by your source IP, not by your client_id, so it is shared with every other client_credentials caller reaching the API from the same address — another integration on your network, or a second client on the same host. Cache the token for its full expires_in lifetime rather than fetching a fresh one on every call (the SDKs already do this — see Token lifetime & refresh); that is what keeps the shared window from being spent needlessly, for you and everyone else behind the same IP.

Endpoints

The company-data endpoints, plus GET /api/contact-fields for the field catalog. Your client's reach is wider than this page: it also holds the public-key endpoints (used to encrypt a per-person document) and the 2FA by allme challenge API. The authoritative list is the endpoint whitelist, and every endpoint in it is browsable in the Service API spec.

GET /api/company-data/connections

List the service's connections, each with its full answer map — the heavy sync read.

GET /api/company-data/connections?offset=0&limit=100
Authorization: Bearer ey...
Accept: application/json

In an SDK this is client.connections() (lazy, auto-paged) and client.connection(id) — see SDKs.

GET /api/company-data/connections/{connectionId}

One connection's provided data: values, keyed by your request slug (never the person's source field). Per answered slot the entry carries live (true for a live share, false for a one-time snapshot), updatedAt, and the value as ciphertext (value, the {"_enc":1,…} wrapper) — plus the verification fields when the value is verified.

Decryption is your job (off-platform, with the service private key). Example response — each value is the ciphertext wrapper you decrypt:

{
  "connection_id": "019xxxxxxxxxxxxxxxxxxxxxxxxx",
  "user_id": "019yyyyyyyyyyyyyyyyyyyyyyyyy",
  "share_code": "ABC123",
  "customer_type": "person",
  "values": {
    "work_email": {
      "live": true,
      "updatedAt": "2026-06-17T09:30:00Z",
      "value": "{\"_enc\":1,\"k\":\"...\",\"iv\":\"...\",\"d\":\"...\"}"
    },
    "passport": {
      "live": false,
      "updatedAt": "2026-06-17T09:31:00Z",
      "value_url": "https://api-eu.allme.fyi/api/company-data/connections/019xxxx/slots/019zzzz/file",
      "content_sha256": "3b9ff1a2c4..."
    }
  },
  "values_private": { "work_email": false, "passport": true }
}

Use GET /api/company-data/request-fields to resolve each slug to its label and type.

DELETE /api/company-data/connections/{connectionId}

Ends one of your service's connections — the service side of a disconnect. It leaves exactly the state the customer's own disconnect leaves: the link, the answers and copies held through it, the pair's messages on your service and the grants on the link are deleted; the company connection, documents, signatures and flow runs stay. Any registered client of the service may call it; no scope or role is added.

DELETE /api/company-data/connections/019xxxx
→ 200 {"deleted": true}

In an SDK this is client.delete_connection(connectionId); it returns nothing and surfaces a refusal as the SDK's API error.

GET /api/company-data/connections/{connectionId}/slots/{serviceFieldId}/file

The bytes behind a binary answer — one whose type resolves to a file storage lane — keyed by the request slot (serviceFieldId) rather than by the person's source field. A parent-typed slot answered with a descendant serves that descendant's file: the platform resolves the stored envelope from the field that answered, and you get the bytes either way. The answering field's type is never disclosed and nothing here asks you to resolve it.

Three response shapes, and you cannot predict which one you get. It depends on whether the person's source field is private AND on the type they answered with — their setting and their choice of field, both changeable, and both deliberately not exposed to you. Branch on Content-Type to tell the raw file apart from JSON, then on the body's encrypted member; never assume JSON and never sniff the body.
  • Private source → application/json with {"encrypted": true, "value": <wrapper>}. Decrypt off-platform with your service private key; the inner plaintext is the envelope below.
  • Non-private source whose type stores more than one file or declares metadata entries (the ID-document types passport, drivers_license, photo_id, residence_permit, and legal_document) → application/json with {"encrypted": false, "value": "<envelope string>"}. That is the same envelope, in the clear. Nothing to decrypt.
  • Every other non-private source → the file's own Content-Type (e.g. image/jpeg, application/pdf) with RFC 6266 Content-Disposition, and the body is the file. Nothing to decrypt.
The safe rule: treat the body as raw file bytes only when a Content-Type is present and contains neither json nor xml. Getting it wrong in the other direction writes ciphertext to disk as though it were the document, with no error to tell you. A JSON body that does not carry encrypted: false with a string value is the wrapper shape.

The envelope inlines every page — or the single file — as a data:<mime>;base64,… URI and carries every entry the type declares beside it:

// an ID document (passport / drivers_license / photo_id)
{
  "pages": [
    { "label": "front", "file": "data:image/jpeg;base64,...", "original_name": "front.jpg", "mime_type": "image/jpeg", "size": 128431 },
    { "label": "back",  "file": "data:image/jpeg;base64,...", "original_name": "back.jpg",  "mime_type": "image/jpeg", "size": 119002 }
  ],
  "document_number": "NX1234567",
  "expiry_date": "2031-04-30",
  "issuing_country": "NL",
  "name": "Jane Doe"
}

// legal_document — one file plus the entries its type declares
{
  "file": "data:application/pdf;base64,...",
  "original_name": "lease.pdf", "mime_type": "application/pdf", "size": 88213,
  "document_number": "LS-88213", "expiry_date": "2029-12-31"
}

// a photo, and any document whose type declares no entries
{ "full": "data:image/jpeg;base64,...", "thumb": "data:image/jpeg;base64,..." }

An entry the person left unset is present and null. A photo serves full; there is no thumbnail variant to select.

The SDKs handle all three shapes and expose the pages and entries directly through the binary handle's pages and metadata accessors. A direct integration branches on Content-Type and then on encrypted, as above.

GET /api/company-data/changes primary

The FIFO change feed — your steady-state integration loop. Returns the service's pending changes oldest-first and deletes exactly those rows in the same transaction (drain-on-fetch). See change-feed semantics below.

curl -s "https://api-eu.allme.fyi/api/company-data/changes?limit=200" \
  -H "Authorization: Bearer ey..."
# returns up to 200 pending changes AND drains them — a second call is empty
By default changes drains only the plain pull feed and never touches a webhook's backlog. Pass ?webhook_id=X only as a manual-recovery option when a webhook was down — see Webhooks.

GET /api/company-data/logs

Your service's activity log — ops events only, never person data. Mirrors the portal's Logs tab for code consumption.

GET /api/company-data/request-fields

Your service's request-field definitions — the slug / label / type / flags you configured. You key your value maps by these slugs; they type every value. No person data, and never the person's suggested source field.

GET /api/company-data/whoami

Your client's own identity — {company_user_id, service_id} resolved from the authenticated client. Use company_user_id to construct a flow run's bindings: the company party must bind to it (the person party's user_id comes from the connection). Read-only; no person data.

GET /api/contact-fields

The field catalog (field_id → label / type / category) so you can resolve any field id referenced in the data. Whitelisted for your client; for a client-credentials token it returns the global fields plus your company's own custom fields.

Verified values

A person can verify certain fields — email and phone by a code they receive, and an identity document or a proof of address through an identity check. When a connection's value comes from a verified field — a live value whose field is verified now, or a one-time value whose field was verified when it was shared — its entry in the connections values map — and any field_updated event for it on the change feed / webhooks — carries four extra fields alongside the ciphertext — plus three that say how it was proved:

{ "email": { "value": "<ciphertext>", "live": true,
             "verified_hash": "<sha256 hex>", "verified_salt": "<hex>",
             "verified_at": "2026-08-12T09:14:00Z", "verified_expires_at": null,
             "verified_method": "email_code", "verified_provider": "allme",
             "verification_id": "019a3f2c-...-8f21" } }

verified_at is when the value was verified. verified_expires_at is nullable, and null means the verification does not lapse — never that its lifetime is unknown. It is set only where the proof is bound to a document that expires, so an email or phone verification carries null and an ID-document one carries the document's own expiry. Treat a value whose verified_expires_at has passed as unverified, exactly as you would one whose hash does not match; the SDKs already do.

A live value can gain or lose its verification with its value unchanged, and each change reaches you as a field_updated carrying the current value, with the verification fields present or absent. One way a value loses it: a person verifies a newer identity document that disagrees with an earlier one on their name or date of birth — every value the earlier document verified loses its verification, and regains it once the person confirms it against the newer document. A one-time value keeps the verification it was shared with.

Proof of how a value was verified

The four fields above say that a value is verified. Three more say how, and they arrive together or not at all:

A value verified before this record existed carries the four verification fields and none of these three; the method is never guessed from the field's type. They are informational and are readable whatever your own hash recompute concludes — that recompute stays the only trust decision. Nothing else about the verification act is exposed on this API, and there is no company-facing route that resolves a verification_id: the metadata you are entitled to already arrived with the value.

Store the verification_id yourself if you need to refer to it later. allme keeps the verification record, but the value that carried the id can be deleted from your service:
  • Your account moves to the free level → 7 days later the data your services hold for the people connected to them is deleted: their service links and what they shared through them, their consents, pending connect requests, 2FA enrolments, and every contract-flow answer, run source file and text-tag value.
  • Your account moves to a level without contract flows → 30 days later every contract-flow answer, run source file and text-tag value is deleted. Your connections and change feed are not affected.
  • You delete a service → all of the above is deleted for that service.
Taking a level that includes the lost capability before the date cancels the deletion; you are mailed when it is scheduled and the day before. Documents, signatures and flow runs are kept in every case.

Verification is bound to the exact value by a server-created salted hash: verified_hash = sha256(verified_salt ‖ plaintext). You do not trust the presence of the fields — you recompute the hash from the plaintext you decrypt (for a file, the file you fetch from its value_url) and treat the value as verified only if it matches:

verified = ( sha256(verified_salt + decrypted_plaintext) == verified_hash )

This makes a substituted or drifted copy render unverified automatically — the hash simply won't match. The person's own field id is never exposed; the binding is purely value→hash. verified: false does not mean "wrong" — it means "not attested" (the value may be perfectly correct, just never verified). The SDKs do this recompute for you and expose a plain verified boolean. A one-time frozen answer carries the verification its source field held when the person shared it, and keeps it, so you can still check a frozen value later — its verified_at says when that field was verified, not that the value is verified today.

What a delivery costs

Requiring a verified value (a request row, a contract-flow field, or an OAuth claim — see the portal guide) is metered: your account is charged when an accepting act delivers a value your company has not been charged for before, per source — any number of fields read off one identity document is one id charge. The same verified value delivered again to you for the same person is free — this is a charge on change, not on every delivery.

SourceBasic / Pro
ID document€2.00 / $2.00
Proof of address€1.50 / $1.50
Phone€0.10 / $0.10
EmailFree — never metered

The same figure on Basic and Pro (Enterprise is negotiated). Charges appear on your invoice's usage lines, and the current metering month's charged events are listable in the portal's Billing page. Requiring a verified value needs the Basic level or above; GET /api/account/entitlements carries the published verified_check_prices per plan, in your own billing currency.

Company documents

A document is something you attach to a connected person — a file (PDF/image/Office) or a JSON body — either targeted at one person or broadcast to everyone connected to the service. Each document carries a status lifecycle (offering → ready_to_sign → active → active_but_ending → ended) and free-form metadata. The person's apps render your documents inline on the connection; if you mark one cancellable, the person can cancel it in-app and you receive a document_status_changed change. A contract-flow-generated document can additionally read waiting — a run-participant copy whose step has not been reached yet in the run's signing line; see contract-flows.html for the signing model.

Contracts (signable / acceptable documents)

A contract is just a company document with a signing layer — same endpoint, same Document model, same change feed, no separate entity. You opt in at create with three fields:

A document counts as a contract when kind is agreement/subscription or either flag is set. A contract must be per-person (target a connection / person) — a broadcast contract is rejected with 400 documents.contract_requires_person. Because it is per-person it is therefore always end-to-end encrypted to the recipient (the per-person rule above); is_private is unchanged and still only governs device-display.

The document seal. A signable file document is sealed by the act that completes it. When the last required signature or acceptance is recorded — for a contract-flow output, the last one on any copy of that output — the platform appends a Signatures page (one stamp per signer, in signing order) and signs the whole PDF with a platform certificate, inside that same act, replacing every party's copy with the sealed PDF; that seal is the platform's own attestation, layered over each signer's step-up act. The seal and the completing signature are stored together or not at all: when the seal cannot be made — the completing act carried no PDF bytes matching the document's plain_sha256, or the sealing service is unavailable — the completing act is refused with 503, nothing is stored, and the signer retries. A ward's completing signature that a custodian approves is sealed the same way, inside the approval, with the PDF the custodian's app sends. A completed signable file document therefore always carries its seal; there is no company-data route that triggers a seal, and nothing for you to do. A document's plain_sha256 is the SHA-256 (lowercase hex) of its unencrypted PDF bytes; you supply it on create (below) and every signer's client re-supplies its own computed hash, so a byte mismatch is refused rather than silently signed. sealed_at is null until the completing act seals the document. A JSON contract carries both fields as null always and is never sealed.

POST /api/company-data/documents

Create a document (config + the json body; file bytes are uploaded next). Body: {kind?, requires_signature?, requires_acceptance?, target, payload_kind, is_private?, name, description?, value?, metadata?, status?, plain_sha256?}. Set kind / requires_signature / requires_acceptance to make it a contract (per-person only; status derived). For a per-person json document (ANY is_private), value is the {"_enc":1,…} wrapper encrypted to the recipient; for a broadcast json document, value is a plaintext JSON object. Returns 201 with the document.

plain_sha256 — SHA-256 (64 lowercase hex characters) of the raw PDF bytes you are about to upload. Required for a file document with requires_signature or requires_acceptance set (400 documents.plain_sha256_required otherwise); optional and stored for any other file document; ignored for a json document. A file document created with no plain_sha256 can never be sealed — see the document seal. Every SDK computes it for you from the bytes you pass to its create call when you don't supply one.

POST /api/company-data/documents
Authorization: Bearer ey...
Content-Type: application/json

{"target": null, "payload_kind": "json", "name": "Terms v3",
 "value": {"url": "https://x/terms.pdf", "version": "3"}, "status": "active",
 "metadata": {"can_be_cancelled_in_app": false}}

A per-person document whose target has deleted their allme account returns 409 account.counterparty_deleted. The connection and the documents already on it are retained and stay readable — what a deleted counterparty ends is writing. The same 409 applies to every mutation of such a document, not only to creation: POST /documents/{id}/file, PUT /documents/{id} and DELETE /documents/{id}. Reads (GET list, document and file) are unaffected.

POST /api/company-data/documents/{id}/file

Upload the bytes for a file document (keyed on the target, not is_private). Broadcast: {"file": "data:application/pdf;base64,…", "original_name": "contract.pdf"} — plaintext, MIME/extension/size validated (default 10 MB; PDF, Word, Excel, PNG, JPEG). Per-person (ANY is_private): {"value": "{\"_enc\":1,…}"} — the opaque ciphertext wrapper (encrypted to the recipient), stored as-is. Returns 200 with the document's file value ({file, original_name, mime_type, size} for broadcast or {_enc_file} for per-person). Re-uploading the file of a contract that has already been signed/accepted (status active) returns 409 documents.contract_immutable.

GET /api/company-data/documents

List your service's documents (newest-first, paged). Query: ?person_user_id (exact target match — not broadcast-inclusive), ?status, ?limit (default 100, max 500), ?offset. Returns {total, items:[document]} (file value is the reference, not the bytes). Each document includes plain_sha256 (an ordinary file document carries one too, when its create call supplied one — it is not contract-exclusive), sealed_at (null on anything that has not sealed, including every non-signable document) and a signatures[] audit trail (empty on a non-contract, since only a contract records sign/accept acts).

GET /api/company-data/documents/{id}

One document's config + value (bytes not inlined for file documents) + its plain_sha256, sealed_at and signatures[]. A contract-flow run-participant document also carries run_signatures — the whole run's signing line, one entry per (output document, participant) in line order, each {output_key, name, party_key, document_id, position, status, action, acted_at}; every document of the run carries the same list (null on any other document).

The signatures[] array is the append-only audit trail of every sign/accept on a contract. Each entry: { action: "signed"|"accepted", method: "biometric"|"twofa"|"email"|"custodian"|null, content_sha256, plain_sha256, signer_first_name, signer_last_name, signer_name_verified, created_at }. method is null for an accept; content_sha256 is the SHA-256 of the JSON-encoded value the party's copy held at signing time — for a file document, a hash of its stored ciphertext reference (not the file's own bytes), so it identifies which stored answer was signed, not what the file contains. plain_sha256 is the SHA-256 the signer's own client computed over the unencrypted PDF bytes it showed — the content-identity proof, and the value the seal checks against the document's own plain_sha256 before it will seal. signer_first_name / signer_last_name are the acting human's own profile name at the moment they signed (for a company party, the acting delegate's — a company login never signs); signer_name_verified is true only when the platform's own check found that name matching that account's verified ID name.

"plain_sha256": "3b9ff1a2c4…", "sealed_at": "2026-09-10T14:03:02Z",
"signatures": [
  {"action": "signed", "method": "biometric",
   "content_sha256": "9f86d0818884…", "plain_sha256": "3b9ff1a2c4…",
   "signer_first_name": "Alice", "signer_last_name": "Jansen", "signer_name_verified": true,
   "created_at": "2026-06-22T10:31:04Z"}
]

GET /api/company-data/documents/{id}/file

Your own bytes as stored. Broadcast (plaintext) → the raw file with Content-Type + RFC 6266 Content-Disposition. Per-person (ciphertext) → {"encrypted": true, "value": <wrapper>}, where the wrapper was encrypted to the recipient's key. The SDKs wrap this as documentFile(id), which returns a broadcast file's plaintext bytes as-is but fails clearly on a per-person file — that copy is encrypted to the recipient, not readable with your service key. To download the company copy of a generated flow document, use the run-document endpoint below (that copy is encrypted to your service key).

GET /api/company-data/flow-runs/{runId}/documents/{outputKey}/file

The company's own copy of one output document a run generated, encrypted to your service key (the run encrypts one copy of each output per bound party; this is the company party's). outputKey is the output's output_key — from the generate response's documents, or from your participant's documents on the run read. Returns the {"_enc":1,…} ciphertext wrapper. 404 flows.run_not_found when the run does not exist or is not your service's; 404 flows.no_document when that output was not produced or your company is not a bound party of the run. The SDKs wrap it as flowRunDocument(runId, outputKey), which decrypts with your service key and returns the plaintext file bytes — the honest completion step of a full flow loop (fill → complete → flowRunAnswers → flowRunDocument per output).

PUT /api/company-data/documents/{id}

Update name / description / metadata / status. (Changing the status here pushes the new state to the person's apps and emits a document_updated change, never a document_status_changed — that event is reserved for the person's in-app sign, accept or cancel and the platform's own moves of a document; see the event table.) Returns the updated document.

409 documents.run_managed when the body carries status and the document is a contract-flow run-participant document currently at waiting, ready_to_sign or offering. Nothing is written on that refusal — a body carrying name together with status writes neither. Such a document's status moves only through flow generation, the run's own signing line, sign/accept, or a run cancel/decline; once it reaches active this route writes it down like any ordinary contract (active → active_but_ending → ended).

DELETE /api/company-data/documents/{id}

Remove the document and any on-disk file. Returns {"message": "Document deleted"}.

The recipient's key: GET /api/company-data/connections and …/{id} return each person's share_code alongside user_id — pass it to GET /api/keys/{shareCode} (your client is whitelisted for it) to fetch the recipient's public key and encrypt every per-person document for them (private or not).

Service-initiated connect requests

Normally a person starts a connection by typing your COMPANYCODE/SERVICECODE handle. This endpoint does the reverse: your service invites a person to connect, addressing them by their profile share code. The person sees the invitation in their app and either connects (fulfilling your service's current request config) or rejects it. It is fire-and-forget: the 201 request_id is the only synchronous feedback — the outcome reaches you later as a change-feed / webhook event. There is no sent-requests list, no cancel, and no expiry.

POST /api/company-data/connect-requests

Send a connect request to a person by share code. Auto-scoped to your service (1:1 with your client) — no service id in the path.

POST /api/company-data/connect-requests
Authorization: Bearer ey...
Content-Type: application/json

{"share_code": "ABC123"}

In an SDK this is client.send_connect_request("ABC123") — see SDKs.

The request carries no field values — the person provides data only when they accept, through your service's current request config. So this call needs no encryption and no keys.
Custom share codes — what you may SEND, and what you will always RECEIVE. A profile can carry a second, human-readable share code assigned by an allme operator, beside the generated one their app displays. Both places this API takes a share code as input accept either: this endpoint, and 2FA by allme's POST /api/service-2fa/challenges. Nothing else changes — same field name, same type, same shape, no SDK update needed.
Every share_code this API emits is the generated code — on the connection list and detail, on every change-feed row and in every webhook body. So a code a customer gives you by hand may differ from the one you read back for that same person, and anything you key on the emitted value (a public-key cache, your own customer record) stays internally consistent.

Errors

Statuserror_keyMeaning
404company_connections.share_code_unknownNo account owns that share code (or none was given).
400company_connections.persons_onlyThe share code belongs to a company account, or to your own — only a person can be invited.
403services.audience_b2b_onlyYour service's audience is b2b — only b2p / any services can invite a person.
403moderation.on_hold / moderation.bannedThe person's account cannot accept new connections right now.
409company_connections.already_linkedThe person is already connected to your service.
409company_connections.request_already_pendingA pending request already exists for this person on this service (one at a time).

The outcome is delivered later as a connection_request_accepted or connection_request_rejected event on the change feed / webhook (see the event table below); on accept a connection_created also fires.

Messaging

Your service can hold a conversation with a connected person. There is no separate inbox: these are the same messages the person reads in their allme app, with your service as the counterpart. A person may reply to anything you send, and their reply reaches you as a message_received change.

POST /api/company-data/connections/{connectionId}/messages

Send one message to the connected person. Auto-scoped to your service (1:1 with your client) — no service id in the path.

POST /api/company-data/connections/019xxxx/messages
Authorization: Bearer ey...
Content-Type: application/json

{"body": "{\"_enc\":1,\"k\":\"…\",\"iv\":\"…\",\"d\":\"…\"}",
 "sender_body": "{\"_enc\":1,\"k\":\"…\",\"iv\":\"…\",\"d\":\"…\"}"}

In an SDK this is client.send_message(connectionId, text) — it encrypts both copies for you and takes no key argument. See SDKs.

POST /api/company-data/broadcast

Send one plaintext message to every person connected to your service.

In an SDK: client.broadcast_message(text).

POST /api/company-data/connections/{connectionId}/messages/read

Acknowledge the inbound messages you have handled, up to a boundary.

POST /api/company-data/connections/019xxxx/messages/read
Authorization: Bearer ey...
Content-Type: application/json

{"up_to_message_id": "019yyyy"}
Replying does not acknowledge. A service that never calls this leaves every inbound message unread forever: its unread count grows without bound and the person never sees a read receipt. It is your own visible choice, not a platform behaviour that repairs itself.

In an SDK: client.mark_messages_read(connectionId, upToMessageId).

Errors

Statuserror_keyMeaning
403messages.messaging_not_entitledThe company's level does not include messaging (Pro or above does).
403messages.not_connectedThe person is not connected to this service.
403messages.blocked_by_personThe person has blocked this service's messages. They stay connected.
403messages.messaging_suspendedMessaging is suspended for this service (or company-wide) after abuse reports.
403messages.broadcast_suspendedBroadcast alone is suspended for this service.
400messages.encryption_requiredA 1-on-1 body or sender_body was not a valid encrypted wrapper.
422messages.broadcast_audience_too_largeThe service has more than 10,000 connections.
429messages.rate_limitedToo many 1-on-1 messages to the same person — honour Retry-After.
400company_data.ack_boundary_invalidThe acknowledgement boundary is not a message the person sent on that connection.

Change-feed semantics

The change feed is a server-side outbox with deliberate, important behavior. Read this before you build your consumer.

Drain-on-fetch (at-most-once on the wire)

Each GET /api/company-data/changes selects the pending rows (FIFO), returns them, and DELETEs exactly those ids in the same transaction. The API keeps no copy afterward — there is no offset/cursor and no server-side backlog to re-read. A lost HTTP response loses those notifications on the wire, which is why the SDK pump persists each batch durably before delivering it and replays its own buffer on restart (turning the wire's at-most-once into at-least-once for your handler). If you consume the raw endpoint yourself, you own that durability.

Exactly-once on your side (dedup on id)

Because safe consumption is at-least-once, your handler must be idempotent — dedup on the row id (the stable server change-row id, captured before the delete). Applying the same change twice must be a no-op.

Dedup + FIFO ordering on the server

The change feed is a state feed, not an event log: the server keeps one pending row per identity, holding that identity's latest state. An identity is a request slot, a linked flow field, a document, a flow run, the connection itself, an inbound message, a person's 2FA challenges, or a key rotation — so a flurry of edits to one slot collapses to a single pending row whose value is the latest, while keeping its original queue position (the timestamp is not bumped). Reads are oldest-first. One source field mapped to N request slots still produces N change rows — one per slot.

Collapsing ignores the event type, so between two drains you may receive fewer events than there were transitions. A slot that was updated, deleted and re-created arrives once, as the final state; a document that moved through several statuses arrives once, at its latest status; a connection created and then deleted arrives once, as the delete. In every case the row you receive is the correct current state, which is all a state feed promises — do not rely on observing intermediate transitions. Distinct identities never share a row, so one never displaces another; two inbound messages are two rows.

The event types

EventWhenCarries
connection_createdA person connects to your service.person id; NULL slot. Pull the connection detail for the provided values.
connection_deletedA person disconnects from your service, or your service ends the connection.person id; NULL slot.
field_updatedA person edits a live-mapped field, or a live-mapped field gains or loses its verification with its value unchanged — the event then carries the current value with the verification fields present or absent.person id, slot slug, and the new value inline (ciphertext for text; value_url for binary) + verified_hash/verified_salt when the source field is verified (verified values).
field_deletedA slot is dropped on a re-map within a live connection, or the person deletes the live-mapped value or field itself.person id, slot (no value).
consent_acceptedA person provides a newly-requested field (consent).person id, slot.
consent_declinedA person declines a consent request.person id, slot.
connection_request_acceptedA person accepts a connect request your service sent.person id, request_id; NULL slot. connection_created also fires.
connection_request_rejectedA person rejects a connect request your service sent.person id, request_id; NULL slot.
flow_run_turnA contract-flow run became a party's turn.person id, flow_run_id, awaiting_party; NULL slot.
flow_completedA contract-flow run reached its terminal state (both output modes).person id, flow_run_id; NULL slot.
document_status_changedA per-person document's status moved: the person signed, accepted or cancelled it in-app, the completing act sealed it, or — for a contract-flow document — the run's signing line opened its step or a run cancel ended it.person id, document_id, status, action (signed | accepted | cancelled, null for a move nobody signed), method, content_sha256, signed_at, sealed_at, plain_sha256, signer_first_name, signer_last_name, signer_name_verified, and on a cancel note + cancel_effective_date.
document_created / document_updated / document_deletedA per-person document of your service was created (including by a contract-flow generation), changed, or deleted. Broadcast documents raise none.person id, document_id, and the document's name, kind and payload_kind — on a delete too.
2fa_challenge_completedA 2FA by allme challenge your service raised was approved, denied or revoked.person id, challenge_id, status (approved | denied | revoked), completed_at. Your poll of the challenge stays the record.
key_rotatedReserved. A person's encryption keypair is set once and never replaced, so no rotation is emitted; the name stays in the event list.person id + share_code + public_key_sha256; NULL slot, no value.
message_receivedA connected person sent your service a message. Person→company only — your own sends, including a broadcast, raise no event.person id + connection_id (reply / acknowledge on it) + message_id (the acknowledgement boundary) + person_public_key (encrypt your reply to it) + created_at, plus the message ciphertext under body, encrypted to your service key; NULL slot. Never under value — on every other event that means field ciphertext.

A person's public key never changes. A person's keypair is set once and never replaced, so caching their public key for the life of your process is correct. key_rotated stays in the event list and the SDKs still handle it: all six drop the cached key when it comes through the changes feed, and over a webhook you would call invalidatePublicKey() yourself (both forms).

One-time / snapshot answers are frozen — after their one initial value they never produce further field_updated rows.

7-day TTL

An un-consumed change is purged after 7 days (a sync gap on your side). Before purging, the platform writes a purge log entry recording how many and which slots were lost, and emails your service admin email as the queue ages (daily from 2 days, hourly from 5 days). Set admin_email on the service and poll changes regularly so nothing is dropped.

The recommended loop: on a schedule, drain changes and apply each event idempotently; do a one-time full sync (and rare reconciliation) via connections; fetch binary bytes lazily only when you need them. The SDKs implement this exactly — config in, typed conclusions out.

Errors

StatusMeaning
401Missing / invalid / expired bearer token. Re-fetch a token (see Authentication).
403The endpoint is outside your client's whitelist, or the resource belongs to another service.
404Unknown connection / slot / webhook id, or the field isn't a binary answer for this connection.
429Rate limited — honor Retry-After and back off (especially on connections).
503error_key: "db.writes_paused" — saving is paused: the platform cannot complete a save in every region right now. Nothing was written, so the call is safe to repeat exactly as it was: wait Retry-After (30 seconds) and retry. Any call that is not a GET can answer it, and so can the change-feed drain GET /api/company-data/changes — which then drained nothing: the events stay queued and arrive on a later drain. Other reads keep working, and so does the token request (POST /oauth2/token). A 503 with rate_limit.unavailable is the rate limiter's (see Rate-limit tiers).
503error_key: "platform.out_of_order" — allme is temporarily out of order: the region serving your call is being rebuilt ("allme is temporarily out of order. Please try again later."). The request was not processed, so the call is safe to repeat exactly as it was: wait Retry-After (300 seconds) and retry. The platform answers normally again once the region is back in service. Every call can answer it, reads included — a change-feed drain GET /api/company-data/changes then drained nothing: the events stay queued and arrive on a later drain. The token request with grant_type=client_credentials keeps working; every other grant at POST /oauth2/token, a refresh_token grant included, answers it.

Error responses include a machine-readable error_key where applicable. The full error-key list and the typed value model are in the Reference.

Next