SDKs
Six official SDKs — Python, TypeScript, Go, C#/.NET, Java, PHP — share one identical API surface: point them at a JSON config file and they hand back typed, plaintext, your-slug-keyed conclusions. Decryption, the OAuth token, the field catalog, id plumbing, binary fetching, JSON-vs-XML, and the crash-safe changes pump are all hidden.
Each SDK turns the service API into a single object you construct from config. For every connected person you get a map of your request-field slug → typed plaintext value (plus whether it is live and when it last changed). Because the platform is zero-knowledge, all decryption happens inside the SDK with your service private key — and the person's own field choices are never exposed; you only ever see the request slots you configured.
Pick your language with the tabs in any code block below — your choice persists across every block and page.
Want to see one run? Each SDK ships a runnable example app — for PHP, Python and TypeScript it comes inside the package you install below. Start it, complete a short browser checklist, then click a scenario and watch the exact SDK calls and results.
What the SDK does for you
- Config in, conclusions out. One
config.jsonholds your credentials + keys; you call typed methods and get typed values. - Regions handled for you. The SDK authenticates at the front door you configured, reads the home-region base off the token response and targets it for every data call. A
421 region.rebase_requiredfrom the front door names the base to use; the SDK stores it as-is (no region list is fetched or checked) and retries once — see Regions. - Decryption hidden. The service private key is loaded once from the encrypted PEM + passphrase; every value is decrypted in memory at access — the key never appears in a method signature.
- Typed, slug-keyed values. The SDK holds the served field-type registry and shapes each value from the type's resolved definition — a composite primitive parses to an object, a date primitive to a date, a file lane to a lazy handle, everything else to a string. A type added to the registry types itself, with no SDK release.
- The crash-safe changes pump. Drains the drain-on-fetch feed through your handler one update at a time, durably buffering each batch before delivery, acking per item, with retry → dead-letter → continue. No daemon / follow mode — you schedule re-runs.
- Webhook helpers. Verify (HMAC), parse, and unwrap the account-key envelope — fully config-driven.
The canonical config.json
The config schema is identical across all six SDKs — the same snake_case keys everywhere. Any field can be overridden by an ALLUS_* env var (e.g. ALLUS_CLIENT_SECRET, ALLUS_KEY_PASSPHRASE), so secrets needn't live in the file. No SDK method ever takes a key, passphrase, or secret as an argument — they all come from here.
{
"api_url": "https://api-eu.allme.fyi",
"client_id": "svc_1a2b3c...",
"client_secret": "...",
"service_private_key": "./service-CRM.pem",
"key_passphrase": "...",
"account_private_key": "./account.pem",
"account_passphrase": "...",
"webhooks": {
"wh_abc123": "hmac_secret_for_that_webhook"
},
"cache_dir": "./allus-cache",
"format": "json"
}
| Key | Required | Meaning |
|---|---|---|
api_url | yes | The global front door, e.g. https://api.allme.fyi. The SDK authenticates there, reads the home-region base off the token response and sends its data calls to that base — see Regions. You never configure a region. |
client_id / client_secret | yes | The registered client_credentials credentials for one service (see Authentication). |
service_private_key | yes | Path to the OpenSSL-encrypted PKCS#8 PEM you downloaded from the portal. |
key_passphrase | yes | Decrypts that PEM in memory at startup. |
account_private_key / account_passphrase | only for encrypt_payload webhooks | The company account key, used to unwrap an encrypted webhook envelope. |
webhooks / webhook_secret | webhook auth — HMAC | Per-webhook HMAC secrets keyed by webhook id (matched via the X-Allus-Webhook-Id header). A single-webhook service can use a flat "webhook_secret": "…" instead. The default/recommended webhook auth. |
webhook_bearer_token | webhook auth — bearer | Verify Authorization: Bearer <token> deliveries. |
webhook_basic | webhook auth — basic | {"username","password"} — verify HTTP Basic deliveries. |
webhook_header | webhook auth — header | {"name","value"} — verify a custom-header delivery. |
webhook_auth_none | webhook auth — none | true — explicit opt-out; verifyWebhook always passes (use only behind your own gateway). |
cache_dir | no (default ./allus-cache) | Durable local buffer for the changes pump. Must be writable + durable. |
format | no (default json) | Wire format json or xml. Invisible in the output. |
auth_type in the portal: HMAC (webhooks/webhook_secret), webhook_bearer_token, webhook_basic, webhook_header, or webhook_auth_none. Configuring two or more fails fast with ConfigError ("configure at most one webhook auth method"). verifyWebhook/handleWebhook then check whichever one you set (constant-time). These alternative auth fields are file-config only (the ALLUS_* env override still applies to the HMAC webhook_secret).A missing/invalid config, an unreadable PEM, or a wrong passphrase fails fast at construction (a ConfigError / ConfigException).
Install
# Python ≥ 3.11 — a system/Homebrew Python refuses a bare `pip install`
# (PEP 668), so install into a virtualenv:
python3 -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install allus-company-data// Node ≥ 18 — dual ESM + CommonJS, bundled .d.ts types
npm install @allus-fyi/company-data// Go ≥ 1.26 — `go get` adds to an existing module:
go get github.com/allus-fyi/company-data-go@latest
// import companydata "github.com/allus-fyi/company-data-go/companydata"
// Starting from scratch instead of an existing module?
// mkdir my-integration && cd my-integration
// go mod init my-integration
// go get github.com/allus-fyi/company-data-go@latest// .NET 8.0+, namespace Allus.CompanyData — not on NuGet yet; reference it from the GitHub repo:
git clone https://github.com/allus-fyi/company-data-csharp.git
dotnet add reference company-data-csharp/src/Allus.CompanyData/Allus.CompanyData.csproj// Java 21+, package fyi.allme.allus.companydata — not on Maven Central yet; build from the GitHub repo:
// git clone https://github.com/allus-fyi/company-data-java.git
// cd company-data-java && mvn -q install // installs fyi.allme.allus:company-data:0.1.0 to your ~/.m2
// then depend on it:
// Maven: <dependency><groupId>fyi.allme.allus</groupId><artifactId>company-data</artifactId><version>0.1.0</version></dependency>
// Gradle: implementation 'fyi.allme.allus:company-data:0.1.0'// PHP ≥ 8.1 (ext-openssl, ext-json) — PSR-4 autoloaded
composer require allus/company-dataQuickstart — construct the client
Client is the only object you build. Construct it from the config file (env vars override secrets) or entirely from ALLUS_* env:
from allus_company_data import Client
client = Client.from_config("allus.json")
# or: Client.from_env()import { Client } from '@allus-fyi/company-data';
const client = Client.fromConfig('allus.json');
// or: Client.fromEnv()client, err := companydata.FromConfig("allus.json")
if err != nil {
log.Fatal(err)
}
// or: companydata.FromEnv()using Allus.CompanyData;
using var client = Client.FromConfig("allus.json");
// or: Client.FromEnv() — Client is IDisposable (holds the in-memory key)import fyi.allme.allus.companydata.Client;
Client client = Client.fromConfig("allus.json");
// or: Client.fromEnv()use Allus\CompanyData\Client;
$client = Client::fromConfig('allus.json');
// or: Client::fromEnv()List a connection's values
connections() is a lazy, auto-paged stream of typed Connections; each conn.values[slug] is already decrypted (or a lazy binary handle). It is heavily rate-limited — use it for the initial full sync, not as a poll loop.
B2B fields. A Connection carries customer_type ("person" | "company") and share_code; a change event carries customer_type. RequestField models an audience ("person" | "company" | "both"), but the request-fields endpoint does not send it, so it reads null/empty. All are parse-permissive.
for conn in client.connections():
print(conn.display_name, conn.person_id)
for slug, val in conn.values.items():
print(f" {slug} = {val.value!r} (live={val.live}, updated={val.updated_at})")
# one connection by id:
conn = client.connection("019xxxxxxxxxxxxxxxxxxxxxxxxx")
email = conn.values["work_email"].value # "alice@acme.com"for await (const conn of client.connections()) {
console.log(conn.displayName, conn.personId);
for (const [slug, val] of Object.entries(conn.values)) {
console.log(` ${slug} = ${JSON.stringify(val.value)} (live=${val.live}, updated=${val.updatedAt})`);
}
}
// one connection by id:
const conn = await client.connection('019xxxxxxxxxxxxxxxxxxxxxxxxx');
const email = conn.values['work_email'].value; // "alice@acme.com"conns, errs := client.Connections(ctx, 100, 0) // page size, offset — lazily auto-paged
for conn := range conns {
fmt.Println(conn.DisplayName, conn.PersonID)
for slug, val := range conn.Values {
fmt.Printf(" %s = %v (live=%v, updated=%v)\n", slug, val.Value, val.Live, val.UpdatedAt)
}
}
if err := <-errs; err != nil {
log.Fatal(err)
}
// or eagerly: client.ConnectionsList(ctx, 100, 0)
conn, err := client.Connection(ctx, "019xxxxxxxxxxxxxxxxxxxxxxxxx")
email := conn.Values["work_email"].Value.(string) // "alice@acme.com"await foreach (var conn in client.ConnectionsAsync())
{
Console.WriteLine($"{conn.DisplayName} {conn.PersonId}");
foreach (var (slug, val) in conn.Values)
Console.WriteLine($" {slug} = {val.ValueObj} (live={val.Live}, updated={val.UpdatedAt})");
}
var conn = await client.ConnectionAsync("019xxxxxxxxxxxxxxxxxxxxxxxxx");
var email = (string?)conn.Values["work_email"].ValueObj; // "alice@acme.com"for (Connection conn : client.connections()) {
System.out.println(conn.displayName() + " " + conn.personId());
conn.values().forEach((slug, val) ->
System.out.printf(" %s = %s (live=%s, updated=%s)%n",
slug, val.value(), val.live(), val.updatedAt()));
}
Connection conn = client.connection("019xxxxxxxxxxxxxxxxxxxxxxxxx");
Object email = conn.values().get("work_email").value(); // "alice@acme.com"foreach ($client->connections() as $conn) {
echo $conn->displayName, ' ', $conn->personId, PHP_EOL;
foreach ($conn->values as $slug => $val) {
printf(" %s = %s (live=%s)\n", $slug,
is_scalar($val->value) ? (string) $val->value : json_encode($val->value),
$val->live ? 'true' : 'false');
}
}
$conn = $client->connection('019xxxxxxxxxxxxxxxxxxxxxxxxx');
$email = $conn->values['work_email']->value; // "alice@acme.com"End a connection
A service can end one of its own connections. delete_connection sends DELETE /api/company-data/connections/{id}, returns nothing, and surfaces a refusal as the SDK's API error: 404 company_data.connection_not_found, or 409 company_connections.active_contract while the customer holds an active agreement or subscription on the service. The customer is told by the platform and connection_deleted reaches your pump and webhooks.
# end one of your service's connections (the service side of a disconnect):
client.delete_connection("019xxxxxxxxxxxxxxxxxxxxxxxxx")// end one of your service's connections (the service side of a disconnect):
await client.deleteConnection('019xxxxxxxxxxxxxxxxxxxxxxxxx');// end one of your service's connections (the service side of a disconnect):
err := client.DeleteConnection(ctx, "019xxxxxxxxxxxxxxxxxxxxxxxxx")// end one of your service's connections (the service side of a disconnect):
await client.DeleteConnectionAsync("019xxxxxxxxxxxxxxxxxxxxxxxxx");// end one of your service's connections (the service side of a disconnect):
client.deleteConnection("019xxxxxxxxxxxxxxxxxxxxxxxxx");// end one of your service's connections (the service side of a disconnect):
$client->deleteConnection('019xxxxxxxxxxxxxxxxxxxxxxxxx');Verified values — the verified flag
A person can verify certain fields — email and phone by a code they receive, and identity/address attributes through an identity check; which types is platform admin data, not a fixed set. For a value 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 — the SDK's value model exposes a plain boolean verified. It is computed after decryption: the platform sends a server-created binding verified_hash = sha256(verified_salt ‖ plaintext), and the SDK recomputes sha256(verified_salt + decrypted_plaintext) and sets verified = true only if it matches. You never see the raw hash.
email = conn.values["work_email"]
if email.verified: # cryptographically bound to this exact value
trust_it(email.value)const email = conn.values["work_email"];
if (email.verified) trustIt(email.value);email := conn.Values["work_email"]
if email.Verified { // cryptographically bound to this exact value
trustIt(email.Value.(string))
}var email = conn.Values["work_email"];
if (email.Verified) // cryptographically bound to this exact value
TrustIt((string?)email.ValueObj);Value email = conn.values().get("work_email");
if (email.verified()) // cryptographically bound to this exact value
trustIt(email.value());$email = $conn->values['work_email'];
if ($email->verified) { // cryptographically bound to this exact value
trustIt($email->value);
}The guarantee: verified: true proves this exact plaintext was verified by the person — a substituted or drifted copy reads verified: false automatically. false is not "wrong", just "not attested" — the value can be perfectly valid and simply never verified. A one-time frozen answer keeps the verification its field held when it was shared, so it stays checkable later; its verified_at is when that field was verified, not a promise about today. Same helper backs the "Sign in with allme" OAuth verified claim.
Company documents
Attach a document (a JSON body or a file) to a connected person, or broadcast one to everyone connected to the service. Encryption is keyed on the target, not on is_private: a per-person document (you pass a connection / person / share code) is always end-to-end encrypted to that recipient's public key automatically — for any is_private — while a broadcast (no target) is plaintext (you can't single-key-encrypt one payload to everyone). is_private is a device-display-only flag (lock-on-reveal vs decrypt-on-load) and requires a per-person target. The SDK fetches the recipient key for you (GET /api/keys/{shareCode}) — no method takes a key/secret argument.
Turn a document into a contract with kind (document / agreement / subscription) plus requires_signature / requires_acceptance. A contract must be per-person (and is therefore always encrypted to the recipient); its status is derived on create (requires_signature → ready_to_sign, else requires_acceptance → offering, else active). The Document model carries requires_signature / requires_acceptance and a signatures[] audit trail (each entry: action, method, content_sha256, plain_sha256, signer_first_name, signer_last_name, signer_name_verified, created_at). The person signs / accepts in-app — you observe it on the changes pump.
The document seal. A signable file document is sealed by the act that completes it: when the last required signature or acceptance is recorded, the platform appends a Signatures page and signs the whole PDF with a platform certificate inside that same act, and every party's copy is replaced with the sealed PDF. When the seal cannot be made, the completing act is refused and nothing is stored, so a completed signable file document always carries its seal (see the document seal; no company-data route triggers a seal). A signable file document's create call takes plain_sha256 (SHA-256 of the raw PDF bytes, lowercase hex) — every SDK computes it from the bytes you pass with a compute_plain_sha256 helper when you don't supply it yourself, so you rarely need to think about it. The Document model carries plain_sha256 and sealed_at on every file document — plain_sha256 is null on a file document with no stored plaintext hash, and such a document cannot be signed; sealed_at is null until the completing act seals it; a JSON contract carries both as null always. document_status_changed's active transition carries the same two fields plus the latest signature's signer members.
# Broadcast a plaintext doc to every connection of the service
client.create_document(name="Terms v3", payload_kind="json",
json_value={"url": "https://x/terms.pdf", "version": "3"})
# Per-person doc — AUTO-encrypted to the recipient (any is_private), no key arg
doc = client.create_document(
name="Your invoice", payload_kind="json", connection_id=conn_id,
json_value={"amount": "42.00", "due": "2026-07-01"},
metadata={"can_be_cancelled_in_app": True},
)
# A per-person FILE doc (the bytes are encrypted automatically too):
client.create_document(name="Contract.pdf", payload_kind="file",
person_user_id=pid, file_bytes=pdf, file_mime="application/pdf")
# A CONTRACT — must be per-person; status is derived (ready_to_sign here)
contract = client.create_document(
name="Service agreement", payload_kind="file", connection_id=conn_id,
kind="agreement", requires_signature=True,
file_bytes=pdf, file_mime="application/pdf")
# Observe the person's sign/accept/cancel via the changes pump (action field).
# Read the audit trail (each entry: action, method, content_sha256, plain_sha256,
# signer_first_name, signer_last_name, signer_name_verified, created_at):
signed = client.document(contract.id)
print(signed.signatures)
print(signed.sealed_at) # None until the completing act seals it
client.list_documents(status="active")
client.update_document_status(doc.id, "ended")
client.update_document_metadata(doc.id, name="Invoice #1001")
client.delete_document(doc.id)// Broadcast a plaintext doc
await client.createDocument({ name: 'Terms v3', payloadKind: 'json',
jsonValue: { url: 'https://x/terms.pdf', version: '3' } });
// Per-person doc — AUTO-encrypted to the recipient (any isPrivate), no key arg
const doc = await client.createDocument({
name: 'Your invoice', payloadKind: 'json', connectionId: connId,
jsonValue: { amount: '42.00', due: '2026-07-01' },
metadata: { can_be_cancelled_in_app: true },
});
await client.createDocument({ name: 'Contract.pdf', payloadKind: 'file',
personUserId: pid, fileBytes: pdf, fileMime: 'application/pdf' });
// A CONTRACT — must be per-person; status is derived (ready_to_sign here)
const contract = await client.createDocument({
name: 'Service agreement', payloadKind: 'file', connectionId: connId,
kind: 'agreement', requiresSignature: true,
fileBytes: pdf, fileMime: 'application/pdf' });
// Observe the person's sign/accept/cancel via the changes pump (action field).
// signatures is an untyped list of raw maps, keyed exactly as the wire sends
// them (snake_case): each entry is { action, method, content_sha256,
// plain_sha256, signer_first_name, signer_last_name, signer_name_verified,
// created_at }.
const signed = await client.document(contract.id);
console.log(signed.signatures);
console.log(signed.sealedAt); // null until the completing act seals it
await client.listDocuments({ status: 'active' });
await client.updateDocumentStatus(doc.id, 'ended');
await client.updateDocumentMetadata(doc.id, { name: 'Invoice #1001' });
await client.deleteDocument(doc.id);// Broadcast a plaintext doc
client.CreateDocument(ctx, companydata.CreateDocumentOptions{
Name: "Terms v3", PayloadKind: "json",
JSONValue: map[string]any{"url": "https://x/terms.pdf", "version": "3"}})
// Per-person doc — AUTO-encrypted to the recipient (any IsPrivate), no key arg
doc, _ := client.CreateDocument(ctx, companydata.CreateDocumentOptions{
Name: "Your invoice", PayloadKind: "json", ConnectionID: connID,
JSONValue: map[string]any{"amount": "42.00", "due": "2026-07-01"},
Metadata: map[string]any{"can_be_cancelled_in_app": true}})
// A CONTRACT — must be per-person; status is derived (ready_to_sign here)
contract, _ := client.CreateDocument(ctx, companydata.CreateDocumentOptions{
Name: "Service agreement", PayloadKind: "file", ConnectionID: connID,
Kind: "agreement", RequiresSignature: true,
FileBytes: pdf, FileMime: "application/pdf"})
// Observe the person's sign/accept/cancel via the changes pump (Action field).
// Signatures is []map[string]any — the raw wire keys (snake_case), not Go
// field names: each entry is {action, method, content_sha256, plain_sha256,
// signer_first_name, signer_last_name, signer_name_verified, created_at}.
signed, _ := client.Document(ctx, contract.ID)
fmt.Println(signed.Signatures)
fmt.Println(signed.SealedAt) // nil until the completing act seals it
client.ListDocuments(ctx, companydata.ListDocumentsOptions{Status: "active"})
client.UpdateDocumentStatus(ctx, doc.ID, "ended")
client.UpdateDocumentMetadata(ctx, doc.ID, companydata.UpdateDocumentMetadataOptions{Name: "Invoice #1001"})
client.DeleteDocument(ctx, doc.ID)// Broadcast a plaintext doc
await client.CreateDocumentAsync(new CreateDocumentOptions {
Name = "Terms v3", PayloadKind = "json",
JsonValue = new { url = "https://x/terms.pdf", version = "3" } });
// Per-person doc — AUTO-encrypted to the recipient (any IsPrivate), no key arg
var doc = await client.CreateDocumentAsync(new CreateDocumentOptions {
Name = "Your invoice", PayloadKind = "json", ConnectionId = connId,
JsonValue = new { amount = "42.00", due = "2026-07-01" },
Metadata = new { can_be_cancelled_in_app = true } });
// A CONTRACT — must be per-person; status is derived (ready_to_sign here)
var contract = await client.CreateDocumentAsync(new CreateDocumentOptions {
Name = "Service agreement", PayloadKind = "file", ConnectionId = connId,
Kind = "agreement", RequiresSignature = true,
FileBytes = pdf, FileMime = "application/pdf" });
// Observe the person's sign/accept/cancel via the changes pump (Action field).
// Signatures is an untyped list of raw maps, keyed exactly as the wire sends
// them (snake_case): each entry is { action, method, content_sha256,
// plain_sha256, signer_first_name, signer_last_name, signer_name_verified,
// created_at }.
var signed = await client.DocumentAsync(contract.Id);
Console.WriteLine(signed.Signatures);
Console.WriteLine(signed.SealedAt); // null until the completing act seals it
await client.ListDocumentsAsync(status: "active");
await client.UpdateDocumentStatusAsync(doc.Id, "ended");
await client.UpdateDocumentMetadataAsync(doc.Id, name: "Invoice #1001");
await client.DeleteDocumentAsync(doc.Id);// Broadcast a plaintext doc
client.createDocument(CreateDocumentRequest.builder()
.name("Terms v3").payloadKind("json")
.jsonValue(Map.of("url", "https://x/terms.pdf", "version", "3")).build());
// Per-person doc — AUTO-encrypted to the recipient (any isPrivate), no key arg
Document doc = client.createDocument(CreateDocumentRequest.builder()
.name("Your invoice").payloadKind("json").connectionId(connId)
.jsonValue(Map.of("amount", "42.00", "due", "2026-07-01"))
.metadata(Map.of("can_be_cancelled_in_app", true)).build());
// A CONTRACT — must be per-person; status is derived (ready_to_sign here)
Document contract = client.createDocument(CreateDocumentRequest.builder()
.name("Service agreement").payloadKind("file").connectionId(connId)
.kind("agreement").requiresSignature(true)
.fileBytes(pdf).fileMime("application/pdf").build());
// Observe the person's sign/accept/cancel via the changes pump (action field).
// signatures() is an untyped list of raw maps, keyed exactly as the wire sends
// them (snake_case): each entry is { action, method, content_sha256,
// plain_sha256, signer_first_name, signer_last_name, signer_name_verified,
// created_at }.
Document signed = client.document(contract.id());
System.out.println(signed.signatures());
System.out.println(signed.sealedAt()); // null until the completing act seals it
client.listDocuments("active");
client.updateDocumentStatus(doc.id(), "ended");
client.updateDocumentMetadata(doc.id(), Map.of("name", "Invoice #1001"));
client.deleteDocument(doc.id());// Broadcast a plaintext doc
$client->createDocument(['name' => 'Terms v3', 'payload_kind' => 'json',
'json_value' => ['url' => 'https://x/terms.pdf', 'version' => '3']]);
// Per-person doc — AUTO-encrypted to the recipient (any is_private), no key arg
$doc = $client->createDocument([
'name' => 'Your invoice', 'payload_kind' => 'json', 'connection_id' => $connId,
'json_value' => ['amount' => '42.00', 'due' => '2026-07-01'],
'metadata' => ['can_be_cancelled_in_app' => true],
]);
// A CONTRACT — must be per-person; status is derived (ready_to_sign here)
$contract = $client->createDocument([
'name' => 'Service agreement', 'payload_kind' => 'file', 'connection_id' => $connId,
'kind' => 'agreement', 'requires_signature' => true,
'file_bytes' => $pdf, 'file_mime' => 'application/pdf',
]);
// Observe the person's sign/accept/cancel via the changes pump (action field).
// signatures is an untyped list of raw arrays, keyed exactly as the wire sends
// them (snake_case): each entry is [action, method, content_sha256,
// plain_sha256, signer_first_name, signer_last_name, signer_name_verified,
// created_at].
$signed = $client->document($contract->id);
print_r($signed->signatures);
var_dump($signed->sealedAt); // null until the completing act seals it
$client->listDocuments(['status' => 'active']);
$client->updateDocumentStatus($doc->id, 'ended');
$client->updateDocumentMetadata($doc->id, ['name' => 'Invoice #1001']);
$client->deleteDocument($doc->id);document_status_changed event on the same changes pump / webhook rail, slug-free, carrying document_id, status and action plus the signing members listed in the event table. The typed Change exposes document_id + status + action (signed | accepted | cancelled, or null for a move nobody signed; no slug/value); handle it alongside the other events (e.g. if change.event == "document_status_changed": apply(change.document_id, change.status, change.action)).Service-initiated connect requests
Invite a person to connect with your service by their share code
(the reverse of the person typing your handle). One thin call wrapping
POST /api/company-data/connect-requests
— no crypto, no keys (the request carries no values; the person provides data
only when they accept). It returns the request_id; the outcome arrives on the
changes pump / webhook as
connection_request_accepted or connection_request_rejected.
request_id = client.send_connect_request("ABC123")
# Outcome later on the pump:
# change.event == "connection_request_accepted" / "connection_request_rejected"
# change.request_id == request_idconst requestId = await client.sendConnectRequest('ABC123');
// Outcome later on the pump:
// change.event === 'connection_request_accepted' | 'connection_request_rejected'
// change.requestId === requestIdrequestID, err := client.SendConnectRequest(ctx, "ABC123")
// Outcome later on the pump:
// c.Event == "connection_request_accepted" / "connection_request_rejected"
// c.RequestID == requestIDstring requestId = await client.SendConnectRequestAsync("ABC123");
// Outcome later on the pump:
// change.Event == "connection_request_accepted" / "connection_request_rejected"
// change.RequestId == requestIdString requestId = client.sendConnectRequest("ABC123");
// Outcome later on the pump:
// change.event().equals("connection_request_accepted") / "connection_request_rejected"
// change.requestId().equals(requestId)$requestId = $client->sendConnectRequest('ABC123');
// Outcome later on the pump:
// $change->event === 'connection_request_accepted' / 'connection_request_rejected'
// $change->requestId === $requestIdprocess_changes handler beside
connection_created etc. On accept, a connection_created
event also fires (the connection itself); the typed Change exposes
request_id on these two events (no slug, no value).Messaging
Hold a conversation with a connected person — the same messaging surface the person already uses, with your service as the counterpart. Three calls wrap the messaging endpoints: a 1-on-1 send, a broadcast, and the read acknowledgement.
- 1-on-1 is end-to-end encrypted, and the SDK does it for you: one copy encrypted to the person's public key and one to your service key, before anything leaves the process. The person reads it in their app; you can re-read your own outbound text. The platform stores ciphertext only, and no method takes a key argument — the person's public key rides the inbound event, and your own key comes from config.
- A broadcast is plaintext — one body cannot be single-key-encrypted to every connection, exactly as for a broadcast document. It seeds each person's ordinary 1-on-1 thread, and their reply comes back end-to-end encrypted.
- Inbound arrives on the pump / webhook as
message_received— a person→company message only. A broadcast raises no event of its own. The typedChangeexposes the connection, the message id, the person's public key and the already-decrypted message text.
def handle(change):
if change.event != "message_received":
return
print(change.person_id, change.message_body) # already decrypted
client.send_message(change.connection_id, "Thanks — we're on it.",
person_public_key=change.person_public_key)
client.mark_messages_read(change.connection_id, change.message_id)
client.process_changes(handle)
client.broadcast_message("We're closed on Friday.")await client.processChanges(async (change) => {
if (change.event !== 'message_received') return;
console.log(change.personId, change.messageBody); // already decrypted
await client.sendMessage(change.connectionId, "Thanks — we're on it.",
{ personPublicKey: change.personPublicKey });
await client.markMessagesRead(change.connectionId, change.messageId);
});
await client.broadcastMessage("We're closed on Friday.");err := client.ProcessChanges(func(c companydata.Change) error {
if c.Event != "message_received" {
return nil
}
log.Println(c.PersonID, c.MessageBody) // already decrypted
if _, err := client.SendMessage(ctx, c.ConnectionID, "Thanks — we're on it.",
companydata.SendMessageOptions{PersonPublicKey: c.PersonPublicKey}); err != nil {
return err
}
return client.MarkMessagesRead(ctx, c.ConnectionID, c.MessageID)
}, companydata.PumpOptions{})
_, err = client.BroadcastMessage(ctx, "We're closed on Friday.")await client.ProcessChangesAsync(async change => {
if (change.Event != "message_received") return;
Console.WriteLine($"{change.PersonId}: {change.MessageBody}"); // already decrypted
await client.SendMessageAsync(change.ConnectionId!, "Thanks — we're on it.",
personPublicKey: change.PersonPublicKey);
await client.MarkMessagesReadAsync(change.ConnectionId!, change.MessageId!);
});
await client.BroadcastMessageAsync("We're closed on Friday.");client.processChanges(change -> {
if (!"message_received".equals(change.event())) return;
log(change.personId(), change.messageBody()); // already decrypted
client.sendMessage(change.connectionId(), "Thanks — we're on it.",
change.personPublicKey(), null);
client.markMessagesRead(change.connectionId(), change.messageId());
});
client.broadcastMessage("We're closed on Friday.");$client->processChanges(function ($change) use ($client): void {
if ($change->event !== 'message_received') {
return;
}
echo $change->personId, ': ', $change->messageBody, "\n"; // already decrypted
$client->sendMessage($change->connectionId, "Thanks — we're on it.",
personPublicKey: $change->personPublicKey);
$client->markMessagesRead($change->connectionId, $change->messageId);
});
$client->broadcastMessage("We're closed on Friday.");company_data.ack_boundary_invalid (400).ApiError equivalent carrying the
platform error_key — the full table is on
the Service API reference. Nothing here enumerates keys
in code, so a new one reaches you without an SDK release.The customer role (b2b)
The SDKs also ship a customer client — the mirror image of the service client, for a
company that connects to another company's service (a b2b link). Where the service client reads
the people connected to your service, the customer client manages your own company↔company
connections. It authenticates with your account-level acct_* credentials
(customer_client_id / customer_client_secret) and decrypts received documents / flow
copies with your account private key (config key account_private_key).
CustomerClient.from_config() / fromCustomerFile / ConfigFromCustomerFile
— one per language) instead of the service one. Config-only keys still apply: no method ever takes a key.What it does:
connections()/connection(id)— your company↔company connections (the counterparty's shared values + company profile are plaintext; your own typed answers are never re-read).provide_consent()/edit_answers()— answer a service's request rows by typing a value (kindtyped, orone_time); the SDK encrypts each to the target service public key.edit_answers()sends the whole answer set: a row it sends nothing for is withdrawn, and a row given kindkeep(no value) keeps the answer it holds now.pending_consents()lists the requests awaiting your answer anddecline_consent()declines one.documents()/document_file()— read + decrypt documents/contracts issued to you (broadcast = plaintext; per-company = account-key ciphertext);cancel_document()cancels one the issuing service marked cancellable.flow_runs()/flow_run()/submit_flow_answers()/decline_flow_run()— run a contract flow as a bound party (per-party encryption: your own copy → your account key, the owning company → its service key, persons → the batch keys endpoint).generate_flow_document(connection_id, run)— generate the output documents of a document-mode run whose last step YOU answered: the party that answers the leaf generates, from its own copy of the answers (your account key opens it, and it holds the whole run), first uploading every participant PDF the run holds for you at the leaf as a generation input. Returns{documents, status}— one{output_key, party_key, document_id, position}per produced (output document, participant). See Contract flows.pump/process_changes()/drain_batch()— the same crash-safe pump over your account change feed (GET /api/customer/changes).verify_webhook()/parse_webhook()/handle_webhook()— the same receiver helpers for your account-level webhooks.
sign / accept. Signing or
accepting a contract is a deliberate human step-up that stays in the portal; a machine acct_*
token is rejected by the API for those routes, so the customer client deliberately exposes no sign/accept
method. The read fields Connection.customer_type, Change.customer_type and
RequestField.audience are present on the service client too (they parse permissively).The changes pump
The steady-state loop. process_changes drains the drain-on-fetch feed through your handler one update at a time, durably buffering each batch before delivery and acking per item — so a mid-batch crash replays exactly what wasn't acked. It returns when the feed is empty; you schedule re-runs (cron / timer). Delivery is at-least-once, so dedup on change.id.
def handle(change):
if already_processed(change.id): # idempotency — dedup on the stable id
return
if change.event == "field_updated":
store(change.person_id, change.slug, change.value)
elif change.event in ("connection_deleted", "field_deleted"):
remove(change.person_id, change.slug)
mark_processed(change.id)
client.process_changes(handle) # returns when the feed is emptyawait client.processChanges((change) => {
if (alreadyProcessed(change.id)) return; // dedup on change.id
if (change.event === 'field_updated') {
store(change.personId, change.slug, change.value);
} else if (change.event === 'connection_deleted' || change.event === 'field_deleted') {
remove(change.personId, change.slug);
}
markProcessed(change.id);
});err = client.ProcessChanges(func(c companydata.Change) error {
if alreadyProcessed(c.ID) { // dedup on c.ID
return nil
}
switch c.Event {
case "field_updated":
store(c.PersonID, c.Slug, c.Value)
case "connection_deleted", "field_deleted":
remove(c.PersonID, c.Slug)
}
markProcessed(c.ID)
return nil // returning nil ACKS; an error RETRIES → then dead-letters
}, companydata.PumpOptions{})await client.ProcessChangesAsync(async change =>
{
if (AlreadyProcessed(change.Id)) return; // dedup on change.Id
if (change.Event == "field_updated")
Store(change.PersonId, change.Slug, change.ValueObj);
else if (change.Event is "connection_deleted" or "field_deleted")
Remove(change.PersonId, change.Slug);
MarkProcessed(change.Id);
await Task.CompletedTask;
});client.processChanges(change -> {
if (alreadyProcessed(change.id())) return; // dedup on change.id()
switch (change.event()) {
case "field_updated" -> store(change.personId(), change.slug(), change.value());
case "connection_deleted", "field_deleted" -> remove(change.personId(), change.slug());
default -> note(change.event(), change.personId());
}
markProcessed(change.id());
});use Allus\CompanyData\Model\Change;
$client->processChanges(function (Change $change): void {
if (alreadyProcessed($change->id)) return; // dedup on $change->id
if ($change->event === 'field_updated') {
store($change->personId, $change->slug, $change->value);
} elseif (in_array($change->event, ['connection_deleted', 'field_deleted'], true)) {
remove($change->personId, $change->slug);
}
markProcessed($change->id);
}); // returns when the feed is emptydead_letters() and re-drive with retry_dead_letters(handler) after fixing the cause. Pump options (per SDK): batch_size (≤ 500, default 100), max_retries (default 3), on_error (deadletter default, or halt), and a backoff function.The typed value model
You work with these objects and nothing else:
RequestField { slug, label, type, one_time, mandatory, audience?, verified, verified_max_age_days?, plugin? } // YOUR request config
Connection { id, person_id, display_name, connected_at, values: {slug: Value},
customer_type?, share_code?, values_private }
Value { value, live, updated_at, verified, verified_at?, verified_expires_at?,
verified_method?, verified_provider?, verification_id?, unreadable }
Change { id, event, person_id, share_code?, customer_type?, slug?, value?, live?, at,
verified, verified_at?, verified_expires_at?, verified_method?, verified_provider?, verification_id?,
request_id?, document_id?, status?, action?, …,
connection_id?, message_id?, person_public_key?, message_body? }
LogEntry { type, message, metadata, at }
- Keyed by your slug.
conn.values["work_email"].value→"alice@acme.com". The key is the stable slug you set per request field — rename the label freely; the slug is the contract. The person's source field is never exposed (no source slug, nofield_id). Value.liveistruewhen the person chose "keep connected" (auto-updates) andfalsefor a one-time snapshot.Value.unreadableistruewhen the answer is present but your configured service key cannot open it — sealed to a key your service has since replaced, or a wrong configured key. Itsvalueis then null andverifiedfalse, and it never fails theconnections()/connection(id)call it arrived in: every other value and connection is returned as usual. Not readable is not empty — an unanswered value is null withunreadablefalse. When every value of every connection readsunreadable, check the configuredservice_private_key.- Binaries — a value whose type resolves to a file storage lane — are a lazy handle: nothing is fetched or decrypted until you call its bytes/save method, which GETs the slot-keyed file endpoint and decrypts for you.
A value's shape comes from the type's RESOLVED definition, which the SDK reads off the field-type registry it fetches beside your request-field catalog. That reference section is the one place the types, the tree and the rules are described.
| The type's resolved… | Decrypted value |
|---|---|
the reserved type key plugin (checked first) | a PluginValue |
storage lane photo / document | a lazy binary handle |
primitive composite | object / map (parsed JSON) |
primitive date | a date (falls back to the raw string if unparseable) |
primitive multilist | a list of the chosen option strings |
| anything else, and a type the registry does not carry | string |
Reach the registry itself as client.fieldTypes() (idiomatic per language) for
resolve(), accepts(), isBinary(), labelFor() and
validate().
Plugins
A company can answer a request row, a flow field or a sign-in claim through a plugin it added in the portal. The SDKs cover all three sides of that.
Plugin values
A request row answered through a plugin has the reserved type key plugin, and its RequestField carries plugin — {pluginName, fieldType, snapshot}, the plugin's frozen description of the field type (null on every other row). The SDK types its value before consulting the field-type registry, as a PluginValue:
PluginValue { plugin, type, blocks: [{key, kind, label, id, value}], outputs: [{key, type, label, value}] }
blocks is in declared order — a search_select pick carries its id and its option label as value, a typed block its typed value — and outputs are typed as the plugin declared them. A plugin answer is what the person's client submitted, sealed to your key but not signed: check an output with the plugin itself when you must rely on it. A FlowRun carries privateSlugs (null when the API did not send it).
What each SDK adds
| What | Python | TypeScript | Go | C# | Java | PHP |
|---|---|---|---|---|---|---|
A flow pass (service Client; CustomerClient takes a leading connection id) | plugin_pass | pluginPass | PluginPass | PluginPassAsync | pluginPass | pluginPass |
| A block's options | plugin_options | pluginOptions | PluginOptions | PluginOptionsAsync | pluginOptions | pluginOptions |
The outputs, or PluginPicksInvalid | plugin_outputs | pluginOutputs | PluginOutputs | PluginOutputsAsync | pluginOutputs | pluginOutputs |
| A field's min/max over the live answer map | check_flow_value | checkFlowValue | CheckFlowValue | CheckFlowValue | checkFlowValue | checkFlowValue |
| A required input that cannot be sent | PluginInputUnavailable | PluginInputUnavailable | *PluginInputUnavailableError | PluginInputUnavailableException | PluginInputUnavailableException | Errors\PluginInputUnavailable |
| Read a plugin claim's value | parse_plugin_value | parsePluginValue | ParsePluginValue | OAuthClient.ParsePluginValue | OAuthClient.parsePluginValue | OAuthClient::parsePluginValue |
| A plugin server: open a request | plugin_open_request | pluginOpenRequest | PluginOpenRequest | Crypto.PluginOpenRequest | Crypto.pluginOpenRequest | Crypto::pluginOpenRequest |
| A plugin server: seal a reply | plugin_seal_reply | pluginSealReply | PluginSealReply | Crypto.PluginSealReply | Crypto.pluginSealReply | Crypto::pluginSealReply |
| A reply key pair (RSA-2048) | generate_reply_key_pair | generateReplyKeyPair | GenerateReplyKey | Crypto.GenerateReplyKey | Crypto.generateReplyKey | Crypto::generateReplyKeyPair |
| A public key as base64 SPKI | export_public_key_spki | exportPublicKeySpki | ExportPublicKeySPKI | Crypto.ExportPublicKeySpki | Crypto.exportPublicKeySpki | Crypto::exportPublicKeySpki |
| Expand plugin answers into dotted keys | expand_plugin_answers | expandPluginAnswers | ExpandPluginAnswers | FlowCondition.ExpandPluginAnswers | FlowCondition.expandPluginAnswers | FlowCondition::expandPluginAnswers |
- Flow fields — the pass, options and outputs calls, the
draftmap, the private-value rule andcheckFlowValueare described on Contract flows.submitFlowAnswersrefuses a value outside its field's minimum or maximum withValidationErrorcarryingbound(min|max) andboundValue, and markssource_privateon every answer whose field default reads a private source (never on a plugin answer). - Building a plugin — the open/seal pair is for a plugin's own server, never a call on the allme API; see the builder routine. Every SDK's private-key loader also accepts an unencrypted PKCS#8 PEM (no passphrase).
- The evaluator gains the variadic
max/minmath ops,expandPluginAnswers,pluginAnswerSummaryandpluginAnswerView; the resolved-constants helper takes an optional trailing list of plugin slugs and expands first when given it. Routing evaluates every edge over the expanded, constants-computed map.
Webhooks
Each SDK exposes config-driven webhook receiver helpers — verify_webhook, parse_webhook, and the one-liner handle_webhook (verify then parse). They take no key or secret arguments (everything comes from config) and operate on the raw request body bytes (never a re-serialized parsed body — the HMAC is over the exact bytes sent). A parsed webhook is the same typed Change as the pull feed — but do not carry the pull feed's id-dedup over to it: on the webhook channel the id is neither reliably stable nor reliably fresh (a live delivery mints it for that POST; a backlog replay carries the durable row's id, repeated on every re-attempt of that row), so an id check catches some duplicates and misses others. Deliveries are effectively unique; the rare replay is keyed on content, not on the id. See the webhook delivery guarantee, and consume webhooks or the pull feed, never both.
200. That is deliberate: allus counts only a 200 as delivered, so a handler that processes an event successfully and answers 202, 204 or a 3xx has its delivery recorded as failed — backlogged, and the webhook's circuit-breaker tripped. Do not "tidy" these to 204.# Flask
@app.post("/allus/webhook")
def allus_webhook():
try:
change = client.handle_webhook(request.get_data(), dict(request.headers))
except WebhookError:
abort(401) # bad / unknown signature, or unparseable envelope
apply_change(change) # NOT dedup-on-id: see the webhook delivery guarantee
return ("", 200) # 200 — the ONLY status allus counts as delivered// Express — capture the RAW body bytes. handleWebhook is async: the handler must be
// async and await it, or the try/catch cannot catch a signature or parse failure.
app.post('/allus/webhook', express.raw({ type: '*/*' }), async (req, res) => {
let change;
try {
change = await client.handleWebhook(req.body /* Buffer */, req.headers);
} catch (e) {
if (e instanceof WebhookError) return res.sendStatus(401);
throw e;
}
applyChange(change); // NOT dedup-on-id: see the webhook delivery guarantee
res.sendStatus(200); // 200 — the ONLY status allus counts as delivered
});func webhook(client *companydata.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body) // the RAW bytes — the HMAC is over them
change, err := client.HandleWebhook(body, r.Header) // verify + parse
if err != nil { // *WebhookError on a bad signature
http.Error(w, "invalid webhook", http.StatusUnauthorized)
return
}
apply(change)
w.WriteHeader(http.StatusOK) // 200 — the ONLY status allus counts as delivered
}
}// ASP.NET minimal API — read the EXACT bytes
app.MapPost("/allus/webhook", async (HttpRequest req, Client client) =>
{
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
var headers = req.Headers.ToDictionary(h => h.Key, h => (string)h.Value!);
try
{
var change = client.HandleWebhook(ms.ToArray(), headers); // verify + parse + decrypt
Apply(change);
return Results.Ok(); // 200 — the ONLY status allus counts as delivered
}
catch (WebhookException) { return Results.Unauthorized(); }
});// Servlet
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
byte[] body = req.getInputStream().readAllBytes(); // the RAW bytes
Map<String, String> headers = new HashMap<>();
var names = req.getHeaderNames();
while (names.hasMoreElements()) { String n = names.nextElement(); headers.put(n, req.getHeader(n)); }
try {
Change change = client.handleWebhook(body, headers); // verify + parse
apply(change);
resp.setStatus(200); // 200 — the ONLY status allus counts as delivered
} catch (WebhookException e) {
resp.setStatus(401); // bad / unknown signature
}
}$rawBody = file_get_contents('php://input'); // the RAW bytes
$headers = function_exists('getallheaders') ? getallheaders() : [];
try {
$change = $client->handleWebhook($rawBody, $headers);
} catch (WebhookError) {
http_response_code(401); // bad / unknown signature, or unparseable envelope
exit;
}
applyChange($change);
http_response_code(200); // 200 — the ONLY status allus counts as deliveredThe full webhook story — configuring them in the portal, the X-Allus-Signature HMAC, and the encrypt_payload account-key envelope — is on the Webhooks page.
Key rotation — key_rotated and the public-key cache
Every SDK caches the RSA public keys it fetches. A person's keypair is set once and never replaced,
so a cached person key stays valid; the key_rotated
event is reserved and is not emitted. The SDKs still handle it.
On the pump, this is automatic — the SDK drops the cached key as the change
passes through, before your handler sees it. Over a webhook it is not: the signature
verifier is static and has no client instance, so it cannot reach the cache. Call the invalidator
yourself — and note that the two clients key their caches differently: the service
Client by share_code, the CustomerClient by the person's
user id. Passing a share code to the customer client removes nothing and leaves you
encrypting to the old key. Both identifiers ride every change, so use whichever your client needs:
if change.event == "key_rotated":
# service Client — cache is keyed by SHARE CODE
client.invalidate_public_key(change.share_code)
# CustomerClient — cache is keyed by PERSON USER ID
customer.invalidate_public_key(change.person_id)if (change.event === 'key_rotated') {
client.invalidatePublicKey(change.shareCode); // service Client — keyed by SHARE CODE
customer.invalidatePublicKey(change.personId); // CustomerClient — keyed by PERSON USER ID
}if c.Event == "key_rotated" {
client.InvalidatePublicKey(c.ShareCode) // service Client — keyed by SHARE CODE
customer.InvalidatePublicKey(c.PersonID) // CustomerClient — keyed by PERSON USER ID
}if (change.Event == "key_rotated") {
client.InvalidatePublicKey(change.ShareCode); // service Client — keyed by SHARE CODE
customer.InvalidatePublicKey(change.PersonId); // CustomerClient — keyed by PERSON USER ID
}if ("key_rotated".equals(change.event())) {
client.invalidatePublicKey(change.shareCode()); // service Client — keyed by SHARE CODE
customer.invalidatePublicKey(change.personId()); // CustomerClient — keyed by PERSON USER ID
}if ($change->event === 'key_rotated') {
$client->invalidatePublicKey($change->shareCode); // service Client — keyed by SHARE CODE
$customer->invalidatePublicKey($change->personId); // CustomerClient — keyed by PERSON USER ID
}service_key_rotated — the same thing, the other way round
If you are a customer of another company's service, your client also caches that
service's public key — the one you encrypt consent answers and documents to —
keyed "companyCode/serviceCode". When the company replaces its service keypair, the
service_key_rotated change on your account feed is your only
signal; companies receive no pushes. It carries company_share_code and
service_share_code (what the cache is keyed on) plus
service_public_key_sha256, the fingerprint of the new key.
On the pump this is automatic, exactly as above — the cached service key is dropped as the change passes through. Over a webhook it is not: call the invalidator yourself from your handler, with the two share codes off the event body.
customer.invalidate_service_key(body["company_share_code"], body["service_share_code"])customer.invalidateServiceKey(body.company_share_code, body.service_share_code);customer.InvalidateServiceKey(body["company_share_code"], body["service_share_code"])customer.InvalidateServiceKey(body["company_share_code"], body["service_share_code"]);customer.invalidateServiceKey(body.get("company_share_code"), body.get("service_share_code"));$customer->invalidateServiceKey($body['company_share_code'], $body['service_share_code']);Note the identifiers are share codes, not the ids invalidatePublicKey takes:
the two caches are keyed differently, and the wrong call removes nothing. Also
eventual, not fail-closed.
Field validation
The value-submit methods type-check every value before encrypting it, so a malformed email / phone / URL / date, an out-of-shape structured object (address / credit-card / bank), or a bad sub-field (e.g. a non-numeric CVC) is caught client-side rather than encrypted into an unusable answer. The rules are identical across all six SDKs (and the allme apps), pinned by a shared parity vector.
submit_flow_answers()resolves each answer's field type from the run's pinned flow definition and validates it; an unknown slug is skipped (never guessed).provide_consent()/edit_answers()resolve each answer's type from the service's request-field catalog and validate it; an unresolved field is skipped, and akeeprow carries no value to validate.- On a bad value the method raises
ValidationError(naming the slug/field + type) before any network call or encryption. Empty values are accepted (a value is not required by the validator — mandatory-ness is a separate server rule).
The rules are the served registry's, not a table in the SDK: the type's resolved PRIMITIVE, then its named CHECK, then every inherited regex root-first, then its sub-field entries — the same canonical order every allme client applies. See the two vocabularies.
Errors
Same taxonomy and names across all six SDKs:
| Error | When |
|---|---|
ConfigError | Missing/invalid config, unreadable key file, or wrong passphrase — at construction (fail fast). |
ValidationError | A submitted value fails its field-type check (see Field validation), or a flow field's minimum or maximum (then it carries bound and boundValue) — raised before encryption. ValidationException in C#/Java. |
PluginInputUnavailable | A required plugin input is unwired, unanswered, another party's private value, or not convertible to its declared type — carries input, source and reason. PluginInputUnavailableException in C#/Java, *PluginInputUnavailableError in Go. |
AuthError | Token fetch/refresh failed, or a 401 survives the one automatic refresh-and-retry. |
ApiError(status, error_key, message) | Any non-2xx from the API. |
DecryptError | A ciphertext wrapper is malformed, the key is wrong, or the GCM tag mismatches. It surfaces when a binary value's bytes are read, on a change event and from flow-run routing and generation; connections() / connection(id) never raise it for a value — they mark it unreadable — and flowRunAnswers lists such an answer under unreadable. |
WebhookError | Signature verification failed, or an envelope couldn't be unwrapped/parsed. |
RateLimitError(retry_after) | A 429 from a rate-limited endpoint (subclass of ApiError); carries retry_after seconds. |
(Naming follows each language's conventions — e.g. ConfigException/WebhookException in C# and Java.)
503 db.writes_paused means saving is paused — retry it. While the platform cannot complete a save in every region, any call that is not a GET, the change-feed drains (processChanges/drainBatch on both the service and the customer client) and the OAuth client's pollResult can raise ApiError with status 503 and error_key db.writes_paused, sent with Retry-After: 30. Nothing was written — a drain drained nothing and your local buffer is untouched — so wait 30 seconds and repeat the same call. The SDKs do not retry it for you, and ApiError does not carry the Retry-After header. Reads and token requests keep working. See API → errors.503 platform.out_of_order means allme is temporarily out of order — retry it. While the region serving a call is being rebuilt, every call — reads, the change-feed drains (processChanges/drainBatch on both the service and the customer client) and the OAuth client's calls included — can raise ApiError with status 503 and error_key platform.out_of_order, sent with Retry-After: 300. The request was not processed — a drain drained nothing and your local buffer is untouched — so wait 300 seconds and repeat the same call. The SDKs do not retry it for you, and ApiError does not carry the Retry-After header. The client_credentials token request the SDKs make keeps working, so the 503 arrives on the call itself; every other grant at /oauth2/token, the OAuth client's code exchange included, answers it. See API → errors.Manuals & packages
Each SDK ships its own full manual (README + a docs/ reference for config, model, pump, webhooks, and errors). The summary here is the cross-language view; reach for the language manual for the idiomatic details.
| SDK | Requires | Install |
|---|---|---|
| Python | Python ≥ 3.11 | pip install allus-company-data (inside a virtualenv — see Getting started) |
| TypeScript / Node | Node ≥ 18 | npm install @allus-fyi/company-data |
| Go | Go ≥ 1.26 | go get github.com/allus-fyi/company-data-go@latest (inside an existing Go module — see Getting started to start one from scratch) |
| C# / .NET | .NET 8.0 | From GitHub — clone + dotnet add reference (not on NuGet yet) |
| Java | Java 21+ | From GitHub — clone + mvn install → fyi.allme.allus:company-data:0.1.0 (not on Maven Central yet) |
| PHP | PHP ≥ 8.1 | composer require allus/company-data |
Next
- Getting started — the end-to-end on-ramp from a service to your first received update.
- Service API reference — the endpoints the SDKs wrap, including the change-feed semantics.
- Webhooks — the push alternative to polling.
- Contract flows — the run methods (
publishedFlow,triggerFlowRun— which compiles and seals a run's text values — with its optionalsourceFiles,stageRunFile,uploadAnswerFile,flowRunSourceFile,submitFlowAnswers,generateFlowDocument— which uploads the held participant PDFs as generation inputs —processFlowRun,flowRunDocument(runId, outputKey)), the run model (each participant'sdocuments— its own copy of every output document, in signing-line order — andsource_files) and the ported condition evaluator. - Reference — field types, error keys, and the raw encryption spec for non-SDK integrators.