Skip to content
AI Building

Build an Atrium app with AI

One minimised file with everything an AI tool needs: how to work, the hard rules, the full app contract and the design system. Upload it to any LLM, then describe your app.

ATRIUM-AI-BUILD.mdMarkdown
  1. Upload the file to ChatGPT, Claude, Cursor or any AI tool (or paste its contents).
  2. Describe the app you want in plain English.
  3. Let the AI ask you for the API key when it needs one.
View the file
Auto

Section 3 of 11

Calls COHO makes to you

View .md

Signed POST (lifecycle, schedules, events)

Section titled “Signed POST (lifecycle, schedules, events)”

Health is not signed. Everything else is.

Header Value
Content-Type application/json
X-Atrium-Signature t=<unix_seconds>,v1=<hex_hmac_sha256>
X-Atrium-Connection-Reference Connection Guid (D format)
X-Atrium-Delivery-Id Guid — treat as idempotency key; dedupe within the 300s window
X-Atrium-Event lifecycle.setup | lifecycle.disconnect | schedule.<key> | event.<key>

Signed string (UTF-8): {t}.{METHOD}.{pathAndQuery}.{connectionReference}.{rawBody}.

  • METHOD is uppercase (GET, POST, PUT).
  • pathAndQuery is the request path plus query string, without atriumSignature (do not reorder other params).
  • connectionReference is the connection Guid, also sent as X-Atrium-Connection-Reference (iframe loads put it on the query string instead of a header).
  • HMAC-SHA256 with the connection signing secret from setup, except lifecycle.setup, which uses your app’s setup secret described below (never the Public API bearer token).
  • Reject timestamps more than 30 seconds in the future. Reject timestamps more than 300 seconds in the past.

Three details cause most verification bugs:

  • Sign the raw request body bytes exactly as received. Do not parse and re-serialise JSON first — key order and whitespace change, and every signature then fails.
  • An empty body is valid and still signed. Signed GETs (config schema, config values, quick view) still include method, path, and connection in the signed string.
  • Compare digests in constant time (timingSafeEqual, hmac.compare_digest, hmac.Equal, CryptographicOperations.FixedTimeEquals). A == string compare leaks timing.

Assert against these before you trust your implementation. Secret atrium_test_secret, timestamp 1767225600, connection 11111111-1111-1111-1111-111111111111, so the header is t=1767225600,v1=<expected>. For the setup row, treat this as the app’s setup secret; for later rows, treat it as the connection signing secret.

METHOD pathAndQuery Raw body Expected v1
POST /webhooks/atrium/setup {"type":"lifecycle.setup","data":{}} dff6a1bb99bc74adf1c24bfbae39f9b511582c98e3eda0a520e23b37feca56df
GET /config?connectionReference=11111111-1111-1111-1111-111111111111 (empty string) 7dde59340b9b657a939e69760412d0ba615a0052d74674399ba7df87f7e4b906
POST /webhooks/atrium/triggers/event {"type":"event.tenancy.started","data":{"name":"Zoe"}} f62b7d1059edffe1c88013f0a20c9dc5a96036ed722dc71f0f9d5687bedaad90

Your verifier must also reject each of these: a tampered body, a different path or method, a v1 that does not match, a timestamp more than 300 seconds in the past, and a timestamp more than 30 seconds in the future. COHO pins its platform verifier to these same vectors.

Envelope:

{
"deliveryId": "00000000-0000-0000-0000-000000000001",
"occurredAt": "2026-08-07T15:00:00Z",
"appReference": "…",
"connectionReference": "…",
"organisationReference": "…",
"type": "lifecycle.setup",
"data": { }
}

Return HTTP 2xx quickly. Do long work asynchronously. Treat deliveryId as idempotent.

Your status COHO
2xx Success
Timeout, 408, 429, 5xx Retry with backoff, then dead-letter. On 429, COHO honours your Retry-After (capped at 60s)
401 / 403 / 400 No retry
404 on setup Connect fails and rolls back

Health is app-scoped (one probe per app base URL). After the first not-OK result the app is degraded; after three consecutive not-OK results COHO pauses schedule/event triggers for all connections of that app. Any OK result immediately clears the failure count and resumes delivery. Disconnect may still be attempted while paused.

COHO signs lifecycle.setup with your app’s setup secret. Your process must verify that signature before accepting or storing anything from the request. Never authenticate setup with data.signingSecret: that value is untrusted until setup verification succeeds.

Read the setup secret from the environment as ATRIUM_SETUP_SECRET (the hello samples, the Node kit, and the Hosting task definition use that name).

Where it comes from. COHO mints a setup secret per app when the app is registered and shows it once in the create response (Store UI, publisher portal, Public API v1.1, and MCP registration all return it). It is never shown again on a later read.

  • Self-hosted (external URL) and Draft apps: copy the value when you register and set it as ATRIUM_SETUP_SECRET on your service before connecting. Draft apps receive it alongside the one-time API key and signing secret.
  • Atrium Hosting: COHO injects ATRIUM_SETUP_SECRET into your task automatically on every provision. You only need a copy if you also run the same build somewhere else.
  • Lost or leaked: the app owner can regenerate it from My apps (org Store) or the publisher portal. The old value stops working immediately; hosted apps are re-provisioned with the new value; self-hosted apps must update their environment before the next connect. Regeneration does not affect connections that already completed setup.

The setup secret is only for setup, and only your app’s setup — it cannot verify or forge setup for any other app. After successful setup, verify config, quick-view, lifecycle, schedule, and event requests with the per-connection data.signingSecret.

data:

{
"apiKey": "<guid>",
"apiBaseUrl": "https://api.example.com",
"signingSecret": "<hex>"
}
  • apiKey — the background / unattended app credential for this install. Authorization: Bearer <apiKey> on Public API v1.1 and Store log/config calls. Use this for schedules, webhooks, and any work with no manager signed in. Shown once.
  • apiBaseUrl — no trailing slash. Call {apiBaseUrl}/v1.1/public/….
  • signingSecret — verify all later signed POSTs (and signed config GET/PUT).

Store these against connectionReference. If you return non-2xx, COHO deletes the connection and the key.

Credentials are already dead. data may include a reason. Delete org-scoped data you hold for that connection.

type is schedule.<key> or event.<key>. Domain payloads in data use references / Guids only, never database integer ids.

Published event keys (subscribe via triggers.events; delivery requires the Store dispatcher):

Key When
tenancy.started Tenancy becomes active (e.g. FUTURE to ACTIVE)
tenancy.ended Tenancy becomes past (e.g. ACTIVE to PAST)
tenancy.updated Manager patches tenancy details or moves the tenancy to another room
onboarding.started Onboarding is created / first pushed
onboarding.updated Onboarding status or step state changes
onboarding.completed Onboarding reaches completed
onboarding.cancelled Onboarding is cancelled
property.updated Property listing / lettings-relevant fields change
room.availability.changed Room AvailableFrom is persisted with a new value (any manager / Public API / occupancy path that saves that change)
maintenance.created A maintenance issue is created (including convert-from-conversation)
message.created A conversation message is saved as Sent (references only; no body)
atrium.test Manager hits Run test in the Store (not a domain event)

The catalogue will grow. Ignore unknown keys.

event.tenancy.started:

{
"tenancyReference": "00000000-0000-0000-0000-000000000001",
"propertyReference": "12-example-street-abcde",
"activatedAt": "2026-09-17T08:00:00Z"
}

event.tenancy.ended:

{
"tenancyReference": "00000000-0000-0000-0000-000000000001",
"propertyReference": "12-example-street-abcde",
"endedAt": "2026-09-17T08:00:00Z"
}

event.tenancy.updated:

{
"tenancyReference": "00000000-0000-0000-0000-000000000001",
"propertyReference": "12-example-street-abcde",
"roomReference": "room-3",
"updatedAt": "2026-10-05T08:00:00Z"
}

event.maintenance.created:

{
"maintenanceIssueGuid": "00000000-0000-0000-0000-000000000001",
"jobNumber": "M0042",
"propertyReference": "12-example-street-abcde",
"roomReference": "room-3",
"raisedBy": "manager",
"severity": "medium",
"maintenanceCategoryGuid": null,
"tenancyTenantGuid": null,
"createdAt": "2026-10-05T08:00:00Z"
}

event.message.created:

{
"messageGuid": "00000000-0000-0000-0000-000000000001",
"conversationReference": "message-abcde",
"medium": "email",
"createdAt": "2026-10-05T08:00:00Z"
}

schedule.<key> (COHO cron tick):

{
"scheduleKey": "daily",
"fireTime": "2026-09-17T08:00:05Z"
}

tenancyReference is the tenancy Guid. Times are UTC.

Declare operations in the manifest so managers and integrations can call your app through COHO (one Public API / MCP connection — your app is never a second MCP endpoint).

Each operation:

Field Rules
name Lowercase letter, then letters/digits/underscores (max 64). Callers use this name.
path Relative path on your base URL (e.g. /operations/list). Same signing as config.
method GET, POST, PUT, PATCH, or DELETE.
description 8–500 characters; written for an AI reader.
effect Required. read, write, or delete — no default. Omit or mistype and the manifest write fails. MCP confirms write/delete before calling.

Public API: METHOD /v1.1/public/apps/{appReference}/{name} with scope apps.operations. appReference is your app Guid or ShortName. The HTTP method must match the manifest. Org API keys may call operations only on unrestricted organisation installs. Personal installs and organisation installs with a person allow-list need a manager session (MCP / manage), not a bare API key.

MCP: list_app_operations / call_app_operation (manager session). Write/delete effects ask for confirmation.

Your handler: verify X-Atrium-Signature the same way as config GET/PUT (signed string includes method, path+query with connectionReference, connection Guid, raw body). Return JSON. Timeout 10s; no retries. Max 50 operations per manifest.