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
- Auth base:
https://api-eu.allme.fyi(production:https://api.allme.fyi, the global front door) — where you request the token. - Data base: the
api_urlthe token response returns (e.g.https://api-eu.allme.fyi), your company's home region. Every/api/company-data/*call goes there; the same call to the front door is refused with421 region.rebase_required. Full rules: Regions. - Every request needs
Authorization: Bearer <token>— see Authentication. - The token's client resolves to your service; there is no service id in any path. A client only ever reads its own service's data.
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:
| Select | How |
|---|---|
| JSON (default) | Accept: application/json or ?format=json or nothing |
| XML | Accept: 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
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.| Endpoint | Tier | Use it for |
|---|---|---|
changes | Generous | Poll as often as you like — it is a cheap drain-on-fetch queue. This is the steady-state path. |
request-fields, logs | Moderate | Occasional reads (the catalog is fetched once and cached). |
connections, connections/{id} (the read and the delete), binary /file | Heavily limited | Initial 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.
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.
- Query:
?offset&?limit(paginated;limitdefault 100, max 500). - Returns:
{total, items}, and per connection:connection_id(the service-link id — your stable handle),user_id,display_name,share_code,customer_type("person"|"company"),connected_at,values(the slug-keyed answer map described under the connection detail),values_private, andpending_consent(the request slugs awaiting the person's consent).
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.
- Path:
connectionId= theconnection_id(service-link id) from the list. - Binary answers (photo / document / legal document) carry a
value_urlinstead of avalue— fetch the bytes via the file endpoint below. A one-time binary answer also carriescontent_sha256, the digest of the bytes behind that URL; once its retention has elapsed it carriesexpired: trueandexpired_at. - Note: this endpoint returns
{connection_id, user_id, share_code, customer_type, values, values_private}and nodisplay_name/connected_at— those identity fields are on the list endpoint. values_private— per answered slug, whether the value is private: the person's source field's privacy,falsefor an answer with no source field (a typed, one-time or plugin answer). A slug absent from it counts as private. The list endpoint carries it per connection too. It decides which values a flow's text may show to every participant of a run.
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.
- Path:
connectionId= theconnection_id(service-link id) from the list. - Returns:
200 {"deleted": true}. - Refusals:
403company_data.no_client;404company_data.connection_not_foundfor an id that is not a connection of your service (a malformed id included);409company_connections.active_contractwhile the customer holds anagreementorsubscriptiondocument in statusactiveoractive_but_endingon your service — end the contract first. - The customer is told. A person's apps drop the service from the company's connection and show it as a closed service labelled “disconnected”; a company customer receives the
connection_deletedaccount event. A customer whose account was deleted can still be disconnected; nobody is notified then. - Your feed. Your own change feed and webhooks receive
connection_deleted. - Reconnecting stays possible: nothing bars the customer from connecting again or you from sending a connect request.
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.
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/jsonwith{"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, andlegal_document) →application/jsonwith{"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 6266Content-Disposition, and the body is the file. Nothing to decrypt.
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.
- Path:
connectionId+ theserviceFieldIdof the binary request slot. - Header on every 200:
X-Allus-Content-Sha256— the SHA-256 of exactly what was served: the raw bytes on the file shape, thevaluestring on either JSON shape. Record it and you can later show that your archived copy has not drifted. It is unsigned: it demonstrates agreement with the platform's record, not proof to a third party who disputes that record. - No variant selection. A photo serves the authoritative image, never the thumbnail.
- All or nothing. A page the platform cannot read makes the whole answer
404, never a partial envelope — you never archive a document that is missing a side. 410company_data.file_expired— a Share once answer's bytes are kept 90 days. The slot still reads as answered, and the 410 body carriescontent_sha256andexpired_atso you can still identify what you once held.value_urlis deliberately not removed on expiry.404if the field is not a binary answer for this connection/service, or if the person's source field is private and no copy was ever shared with your service.
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.
- Query:
?limit(page size, default 100, max 500);?webhook_id=X(advanced — drains that webhook's failed-delivery backlog instead of the plain pull feed;404if X isn't your service's webhook). - Returns
{changes: […]}, and per row:id(the stable change-row id — your dedup key),event(one of the event types),person_user_id,share_code(the person's profile share code — every event; may be null),customer_typeon a field, consent or connection event ("person"|"company"; may be null),slug(YOUR request slug — never the person's source field),at, and onfield_updatedaliveflag plus thevalue(ciphertext inline for a text field; avalue_urlto the file endpoint for a binary) — a value from a verified source field, text or file, live or one-time, also carriesverified_hash+verified_salt+verified_at+verified_expires_at, and the proof trioverified_method+verified_provider+verification_id(verified values). Connection / consent / delete events carry no value; afield_deletedfor a one-time binary answer whose retention elapsed carriesexpired: trueand thecontent_sha256of the bytes it held. Document, flow, connect-request, challenge and message events carry their own members (the event table). The payload is slug-keyed and never exposes the internalfield_id/service_field_id.
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
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.
- Query:
?offset&?limit(default 50, max 200); newest-first. Returns{total, items}. - Returns: entries of
typeemail(a stale-queue alert was sent),purge(the 7-day cleanup deleted N un-fetched changes — what + how many were lost), orwebhook(delivery/breaker ops), each with amessage, a type-specificmetadataobject, and a timestamp.
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.
- Returns
{request_fields: […]}, and per request field:slug,label,type,one_time, the mandatory flagsmandatory_provideandmandatory_connected, the verified demand —verifiedplus an optionalverified_max_age_days— andplugin. - Demanding a verified value: a request row set to
verifiedaccepts only an answer that comes from one of the person's own verified fields, andverified_max_age_daysnarrows that to a verification no older than N days. It is enforced when the person answers, so a delivery you receive against such a row was proved at that moment — but a standing link is not re-checked afterwards, which is why every value carriesverified_atandverified_expires_atfor you to apply your own policy. A typed or one-time answer can never satisfy it (there is no stored field behind it to bind a proof to), and the flag is only available on rows asked of PEOPLE. Demanding verified values needs the Basic level or above. - Plugin rows: every row carries
plugin—nullon an ordinary row, and on a row answered through a plugin (itstypeis the reserved keyplugin){"plugin_name", "field_type", "snapshot"}, wheresnapshotis the plugin's frozen description of that field type (label,blocks,inputs,outputs). A plugin request row is always one-time and is asked of people only. Its value, once decrypted, is the plugin answer's self-describing JSON string —{"plugin", "type", "blocks": [{key, kind, label, id?, value}], "outputs": [{key, type, label, value}]}— which the SDKs type as aPluginValue. It is what the person's client submitted, sealed to your service key but not signed: check an output with the plugin itself when you must rely on it. Flow fields carrypluginthe same way. - Value semantics:
phone= one E.164-style string (dial code embedded, e.g.+31612345678);country/nationality= ISO 3166-1 alpha-2 code (a CODE, not a name);address.country= ISO code andaddress.state= USPS 2-letter code (US only). - Flow fields too: your service's contract-flow fields are returned in the same
request_fieldsarray, each additionally tagged withflow_id,flow_name,flow_version, andnode_key(so you can tell them apart from plain request rows and group them by flow).flow_versionis the flow's latest published version (null if never published) — pair it withflow_nameto identify a flow from its name alone, without reading any id out of a portal URL. The pair is not guaranteed unique — nothing stops you from naming two flows on the same service identically, so if you resolve a flow this way, treat more than one match as an error to surface, never as "pick the first one." Pass?flow_id=…to scope to one flow. A flow field carries noone_time/mandatory flags (a run is per-run). These fields are additive — parse-permissively.
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:
verified_method—email_code,sms_code,sumsub_idorsumsub_address: the channel or check that actually carried the proof.verified_provider—allmefor a code allme itself sent,sumsubfor an identity or address check run by our verification provider.verification_id— the proof id. Quote it back to allme and we resolve the full record of that verification act, including facts you never receive: the IP the request came from, the app it came from, and whether the person had two-factor authentication enrolled and a live step-up at the time. That is what a dispute is answered from.
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.
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.
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.
| Source | Basic / Pro |
|---|---|
| ID document | €2.00 / $2.00 |
| Proof of address | €1.50 / $1.50 |
| Phone | €0.10 / $0.10 |
| Free — 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.
- Targeting (
target):null= broadcast (every linked person);{"connection_id": "…"}or{"person_user_id": "…"}= one connected person. A404 documents.connection_not_foundif the target isn't connected to your service. payload_kind:json(the body is inline invalue) orfile(bytes uploaded afterwards via the file endpoint).- Encryption (keyed on the target): every per-person document is end-to-end encrypted to the recipient's public key (
valueis the{"_enc":1,…}wrapper) — regardless ofis_private. Broadcast documents are plaintext (you can't single-key-encrypt to all connections). Resolve the recipient key withGET /api/keys/{shareCode}using theshare_codenow returned on each connection (see below); the SDK does this for you. is_private(device-display-only): a separate flag that only governs how the recipient's app reveals the document (lock-on-reveal vs decrypt-on-load), exactly like a contact field'sis_private. It does not gate encryption. Because a plaintext broadcast can't be locked,is_private=truerequires a per-person target (otherwise400 documents.broadcast_cannot_encrypt) and a broadcast must keepis_private=false.
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:
kind:document(default) |agreement|subscription.requires_signature(bool) — the person must sign it in-app.requires_acceptance(bool) — the person must accept it in-app.
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.
- Status is derived on create (signature wins; any client-sent
statusis ignored for contracts):requires_signature→ready_to_sign; elserequires_acceptance→offering; elseactive. The lifecycle enum itself is unchanged (offering→ready_to_sign→active→active_but_ending→ended). - Immutability: once a contract is signed/accepted (status
active), re-uploading its file returns409 documents.contract_immutable. - The person signs / accepts in-app. The sign / accept actions live on the person's own apps — they are not company-data endpoints. You observe the result on the change feed / webhook: the
document_status_changedevent carries anaction(signed|accepted|cancelled). - Recognized
metadatakeys.metadatais free-form, but the person's apps render a few keys specially instead of as plain rows:valid_until,renewal_date,renewal_term,cancel_before(shown as labelled fields);can_be_cancelled_in_app+cancel_url/cancel_phone/cancel_email(drive the in-app or external cancel control, preferred in that order); andmanagement_url— a URL the apps surface as a gear icon that opens it externally, so the person can manage/change the contract on your site. Any other key is shown as a plainLabel: valueline.
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"}.
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.
- Body:
{ "share_code": "ABC123" }— the target person's profile share code (case-insensitive). Either the code their app shows them, or a custom share code an allme operator has assigned to that profile — both resolve to the same person, and nothing in the response tells them apart. - Returns:
201 { "request_id": "…" }— the id of the pending request, which correlates to the later outcome event.
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.
Errors
| Status | error_key | Meaning |
|---|---|---|
404 | company_connections.share_code_unknown | No account owns that share code (or none was given). |
400 | company_connections.persons_only | The share code belongs to a company account, or to your own — only a person can be invited. |
403 | services.audience_b2b_only | Your service's audience is b2b — only b2p / any services can invite a person. |
403 | moderation.on_hold / moderation.banned | The person's account cannot accept new connections right now. |
409 | company_connections.already_linked | The person is already connected to your service. |
409 | company_connections.request_already_pending | A 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.
- 1-on-1 is end-to-end encrypted, and you do the encrypting. You send TWO copies of
the same text:
bodyencrypted to the person's public key (what they read) andsender_bodyencrypted to your service key (what lets you re-read your own outbound message). The platform stores ciphertext only and can read neither. Abodyorsender_bodythat is not a valid{"_enc":1,…}wrapper is refused400 messages.encryption_required. - Messaging needs the Pro level or above — in both directions: a person cannot message a service whose company's level does not include it either.
- A broadcast is plaintext. One body cannot be single-key-encrypted to every
connection, so a broadcast is the one message the platform can read — exactly as for a
broadcast document. It seeds each recipient's ordinary 1-on-1 thread, and
any reply comes back end-to-end encrypted. A broadcast takes no
sender_body. - You must acknowledge what you handle. Inbound messages stay unread until you say otherwise; sending a reply does not acknowledge anything.
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.
- Path:
connectionId= theconnection_id(service-link id) from the connection or the inbound event. - Body:
{"body": "<wrapper encrypted to the PERSON>", "sender_body": "<wrapper encrypted to YOUR SERVICE>"}— each the{"_enc":1,…}wrapper as a JSON string. Resolve the person's public key from theperson_public_keyon the inbound event, or fromGET /api/keys/{shareCode}using the connection'sshare_code. - Returns:
201with the created message carryingmessage_id— the value you later pass as the acknowledgement boundary.
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.
- Body:
{"body": "<plaintext>"}— no encryption. A body carrying asender_bodyis refused400. - Returns:
202 {"job_id": "…", "audience": N}— the send is queued and fans out per recipient;audienceis the number of connections it will reach. Each seeded thread is an ordinary 1-on-1 the person can reply to. - Refused
422 messages.broadcast_audience_too_largewhen the service has more than 10,000 connections.
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.
- Body:
{"up_to_message_id": "<message id>"}— required. - Only the person's messages on this connection at or before that message are marked read. A message that arrived while you were working is not swept, so you never acknowledge something you have not seen.
- Returns:
{"marked_read": N}— how many messages this call marked. - Idempotent — repeating the same boundary is a no-op. It fires the person's read receipt for exactly the messages it marked.
- The boundary must be a message the person sent on this connection. Anything else
— your own outbound message, or a message from another thread — is refused
400 company_data.ack_boundary_invalid, so a foreign id can never lend its timestamp to sweep messages you never handled.
POST /api/company-data/connections/019xxxx/messages/read
Authorization: Bearer ey...
Content-Type: application/json
{"up_to_message_id": "019yyyy"}
In an SDK: client.mark_messages_read(connectionId, upToMessageId).
Errors
| Status | error_key | Meaning |
|---|---|---|
403 | messages.messaging_not_entitled | The company's level does not include messaging (Pro or above does). |
403 | messages.not_connected | The person is not connected to this service. |
403 | messages.blocked_by_person | The person has blocked this service's messages. They stay connected. |
403 | messages.messaging_suspended | Messaging is suspended for this service (or company-wide) after abuse reports. |
403 | messages.broadcast_suspended | Broadcast alone is suspended for this service. |
400 | messages.encryption_required | A 1-on-1 body or sender_body was not a valid encrypted wrapper. |
422 | messages.broadcast_audience_too_large | The service has more than 10,000 connections. |
429 | messages.rate_limited | Too many 1-on-1 messages to the same person — honour Retry-After. |
400 | company_data.ack_boundary_invalid | The 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
| Event | When | Carries |
|---|---|---|
connection_created | A person connects to your service. | person id; NULL slot. Pull the connection detail for the provided values. |
connection_deleted | A person disconnects from your service, or your service ends the connection. | person id; NULL slot. |
field_updated | A 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_deleted | A 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_accepted | A person provides a newly-requested field (consent). | person id, slot. |
consent_declined | A person declines a consent request. | person id, slot. |
connection_request_accepted | A person accepts a connect request your service sent. | person id, request_id; NULL slot. connection_created also fires. |
connection_request_rejected | A person rejects a connect request your service sent. | person id, request_id; NULL slot. |
flow_run_turn | A contract-flow run became a party's turn. | person id, flow_run_id, awaiting_party; NULL slot. |
flow_completed | A contract-flow run reached its terminal state (both output modes). | person id, flow_run_id; NULL slot. |
document_status_changed | A 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_deleted | A 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_completed | A 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_rotated | Reserved. 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_received | A 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.
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
| Status | Meaning |
|---|---|
401 | Missing / invalid / expired bearer token. Re-fetch a token (see Authentication). |
403 | The endpoint is outside your client's whitelist, or the resource belongs to another service. |
404 | Unknown connection / slot / webhook id, or the field isn't a binary answer for this connection. |
429 | Rate limited — honor Retry-After and back off (especially on connections). |
503 | error_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). |
503 | error_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
- SDKs — the same API as typed, slug-keyed conclusions with decryption + the crash-safe pump handled for you.
- Webhooks — the push counterpart to the pull change feed.
- Contract flows — run a published branching flow turn-by-turn, then auto-share the answers or generate a signable contract.
- Service API explorer — try every endpoint live.