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 5 of 11

Calls you make to COHO

View .md
  • Base: {apiBaseUrl}/v1.1/public/
  • Auth: Authorization: Bearer <apiKey> from setup
  • OpenAPI: Scalar COHO Public API 1.1 — authoritative for request/response shapes
  • Use Guids / references from the API — never database integer ids.
  • GET /v1.1/public/conversations?afterTime= returns conversations whose last activity is after that UTC time.
  • Conversation messages include medium and isAutomated. isAutomated is true when the sender is an App Connection principal (Store-connected app), otherwise false.
  • POST /v1.1/public/conversations accepts medium COHO_MESSENGER (default), EMAIL, SMS, or WHATSAPP. Email/SMS broadcasts require AllowReplies=false. SMS send calculates segments and debits landlord SMS credits server-side (optional GET /organisation/credits?type=SMS and POST /organisation/sms-credit-quote with body to inspect cost). There is no client SmsSettings field on create. WhatsApp needs an active org connection.
  • GET /v1.1/public/conversations/{reference}/messages?includeDrafts=true includes Draft messages; DELETE .../messages/{messageGuid} removes a draft; PATCH .../messages/{messageGuid} updates a draft and/or promotes it to Sent.
  • Property tenants, tenancy tenants, and conversation recipients expose messagingRestricted. Create and sent replies refuse restricted housemates (maintenance threads stay writable).
  • POST /v1.1/public/conversations/{reference}/messages replies on an existing thread. Optional files, idempotencyKey (Redis, 24h), and per-recipient recipients (sent / skipped / noEmail). Public API send does not mark the thread read for the sender. SMS rejects attachments; WhatsApp allows at most one; email and COHO Messenger allow up to 10. Sent WhatsApp replies enqueue the existing manager WhatsApp follow-up.
  • GET /v1.1/public/conversation-merge-tags lists standard [COHO:...] merge tags plus the [COHO:CUSTOM: prefix.
  • GET /v1.1/public/properties?propertyTags= ANDs the listed tags against property or building tags (room tags do not match).
  • PATCH /v1.1/public/properties/{reference} updates listing copy (headline, description, amenities, term months, bills). PATCH .../rooms/{roomReference} can attach already-uploaded image references to the gallery. POST .../rooms/{roomReference}/availability wraps MarkAsAvailableFrom / MarkAsUnavailable.
  • Property tenants include gender, approved occupation, interest references, and optional profile image.reference. Fetch bytes with GET /v1.1/public/images/{reference}/signed-url (file images stay on GET /v1.1/public/files/{reference}/signed-url). PATCH .../tenants/{tenancyTenantGuid}/photo sets that photo from an uploaded image reference.
  • Creating a maintenance issue accepts files (new upload) and fileReferences (already-uploaded conversation files), matching convert-from-conversation.
  • Info boxes expose managedByApp. Only the creating App Connection (same Atrium app) may patch or delete an app-owned box; other apps receive 403.
  • POST /v1.1/public/organisation/notify-managers sends an in-app notification (organisation.write) with a same-origin linkPath starting with /.
  • GET /v1.1/public/guarantors pages organisation guarantor details (tenancies.read). Org-wide Right to Rent remains GET /v1.1/public/right-to-rent/report.
  • Creating or converting a maintenance issue returns tenantUrl only when the issue is shared with the tenant.
  • Maintenance issues include assignedOrganisationPersonGuid, supplierGuid and updated; GET maintenance accepts detailedStatuses. Viewings include their state (agreedDate, invitedByManager, applicantResponse, managerResponse, outcome, proposedDateSet), assignee and timestamps. Tenancy tenants include cohoAccountStatus and dateInvitedToCoho. GET organisation/users includes personReference, which matches the usage token’s personReference claim.

POST {apiBaseUrl}/v1.1/public/atrium/logs

Organisation-owned app registration (manager API keys; Store Build an app parity):

  • GET /v1.1/public/atrium/apps — list apps this organisation owns
  • GET /v1.1/public/atrium/publisher-profile — get Built-by / company listing disclosures
  • PATCH /v1.1/public/atrium/publisher-profile — update company disclosures (country, support, privacy, sub-processors, licence)
  • POST /v1.1/public/atrium/apps/external — register self-hosted
  • POST /v1.1/public/atrium/apps/hosted — register COHO-hosted (queues provision)
  • PATCH /v1.1/public/atrium/apps/{appReference} — update listing (data location, permission reasons, copy, icon/screenshots; allowed while in review)
  • POST /v1.1/public/atrium/apps/{appReference}/submit-for-approval — validate disclosures and move to in_review
  • POST /v1.1/public/atrium/apps/{appReference}/hosted-update — update hosted source / reprovision

Partner publisher public API is not shipped yet (manage + MCP only; MCP: GetPublisherDeveloperProfile / UpdatePublisherDeveloperProfile).

{
"level": "info",
"message": "Birthday messages sent",
"deliveryId": "00000000-0000-0000-0000-000000000001",
"detailJson": null,
"durationMs": 120
}

level: debug | info | warning | error. Store logs are operational history on the connection; they are not the main COHO history table. The customer attention red-dot is driven by live actionable states (pending permissions, setup incomplete, unhealthy, last run failed, access blocked), not by Warning/Error log volume.

COHO does not persist customer config. The Store loads schema and values from you, and writes changes back to you, all signed with the connection signing secret (same HMAC scheme as webhooks).

Typical paths (override in the manifest):

Method Path Role
GET /config/schema?connectionReference= JSON Schema for the Store form
GET /config?connectionReference= Current values
PUT /config?connectionReference= Persist values

Use this only for small preference sets (subject line, send time, dry-run). Lists, reports, payments, approvals, multi-step setup, and anything a manager must click through belong in quick view (or an external URL) — not in this form.

COHO implements a strict subset of JSON Schema Draft 2020-12. Unsupported keywords are rejected with a useful error; they are never silently ignored.

Root:

Keyword Support
$schema Optional; use https://json-schema.org/draft/2020-12/schema
type Required; must be object
title, description Optional form heading/help
properties Required object of fields
required Optional array of property names
additionalProperties Optional; when supplied must be false

Properties:

Keyword Support
type Required: string, boolean, number, or integer
title, description, default Label, help, initial value
enum Non-empty values matching the property type; renders a dropdown
format String only: date (yyyy-MM-dd), time (HH:mm), email
minLength, maxLength, pattern String validation
minimum, maximum Number/integer validation
x-coho-control String only: multiline

Every other root or property keyword is currently unsupported. In particular: nested objects/arrays, oneOf/anyOf/allOf, conditional schemas, remote $ref, custom enum labels, file inputs, and rich text.

Renderer mapping:

Schema COHO control
string <textbox>
string + x-coho-control: multiline <multiline-textbox>
string + format <textbox> with date/time/email input type
boolean <checkbox>
number / integer <numberbox>
Any supported scalar + enum <dropdown>

Canonical example:

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "Birthday message settings",
"description": "Choose what the app sends and when it runs.",
"additionalProperties": false,
"properties": {
"subject": {
"type": "string",
"title": "Message subject",
"description": "Subject shown to the tenant.",
"default": "Happy birthday",
"minLength": 1,
"maxLength": 120
},
"body": {
"type": "string",
"title": "Message body",
"x-coho-control": "multiline",
"default": "Happy birthday, [COHO:TENANT:FORENAME]!",
"minLength": 1,
"maxLength": 2000
},
"dryRun": {
"type": "boolean",
"title": "Dry run",
"default": false
},
"sendTime": {
"type": "string",
"title": "Send time",
"format": "time",
"default": "09:00"
},
"timezone": {
"type": "string",
"title": "Timezone",
"enum": ["Europe/London", "UTC"],
"default": "Europe/London"
},
"retryCount": {
"type": "integer",
"title": "Retry count",
"minimum": 0,
"maximum": 5,
"default": 2
}
},
"required": ["subject", "body", "sendTime", "timezone"]
}

The corresponding GET /config response and PUT /config body:

{
"subject": "Happy birthday",
"body": "Happy birthday, [COHO:TENANT:FORENAME]!",
"dryRun": false,
"sendTime": "09:00",
"timezone": "Europe/London",
"retryCount": 2
}

COHO validates the schema when it is loaded and validates config against that same schema before proxying a save to your app. Your app must still validate before persistence: it owns the data and may be called independently.

MCP is a capability of COHO Atrium, not a separately named product. Managers connect Claude (or similar) through their COHO account using the default address https://mcp.coho.life/mcp. Your app still integrates via Public API + this runtime contract. Do not implement a second MCP server inside the app unless you are building an AI product that happens to use COHO.

(Path aliases under /mcp/{label} exist only so Claude can hold more than one COHO connector when the same URL would otherwise be rejected — support/product guidance, not something apps should depend on.)

Over MCP you can:

  • Register and update self-hosted or hosted apps (org tools and partner publisher tools)
  • Update listing fields including data location, permission reasons, icon, and screenshots
  • Read and update the publisher / developer profile (company disclosures) — org: GetAtriumPublisherProfile / UpdateAtriumPublisherProfile; partner: GetPublisherDeveloperProfile / UpdatePublisherDeveloperProfile
  • Submit for approval once disclosures are complete (SubmitAtriumAppForApproval / SubmitPublisherAtriumAppForApproval)

Connecting GitHub is not available over MCP — it is an OAuth-style install that has to happen in a browser, so it stays a Store / publisher-portal action (§5.5). Once GitHub is connected, MCP registration of a private repository works exactly like a public one.

To expose your own endpoints to the manager’s AI (and to Public API callers with apps.operations), declare them under manifest operations (§3.5). COHO then surfaces them as list_app_operations / call_app_operation — you do not host MCP yourself.

Hosted source choices (GitHub private / public / zip)

Section titled “Hosted source choices (GitHub private / public / zip)”

Reach this section after a Draft (or local self-host) already works — see Default builder workflow above. Hosted source is how you convert a working app onto Atrium Hosting, not how you start building.

Decide the source before you need a Hosting build. AI agents cannot complete GitHub App install or Connect for you — those are browser steps for a human. Connect binds the GitHub App to your COHO org or publisher; you still paste the full repository URL into the form (Connect alone does not pick a repo).

Source Use when Human steps
Public GitHub Code can be public Register https://github.com/<org>/<repo> — no Connect
Private GitHub Repo stays private 1) Connect GitHub in the Store / publisher portal. 2) A GitHub org owner adds the repo to the COHO Atrium App under Selected repositories (do not switch the install to All repositories). 3) Register / rebuild
Zip upload No GitHub, or App install cannot be changed Upload a source zip in the Store; re-upload when the code changes
Self-host You already run HTTPS Register base URL + manifest; skip Hosting

Atrium Hosting can build from a private GitHub repository. COHO never holds a personal access token or deploy key: access comes from a GitHub App installation that you control and can revoke from GitHub at any time.

  1. In the Store (managers) or the publisher portal (publishers), choose GitHub repository as the source and select Connect GitHub.
  2. GitHub asks which account and which repositories to grant. Grant only the repositories you want COHO to build (Selected repositories).
  3. You are returned to COHO and the connection is recorded against your organisation or publisher account. One connection covers every hosted app you own — but each new private repo still needs adding to that Selected list by a GitHub org owner.
  4. Register or update with the plain https://github.com/<org>/<repo> URL. There is no separate “private” flag. Register rejects a private URL when Connect is missing or the App cannot see the repo (you should not reach a generic “Provisioning failed unexpectedly” for that case).

What this means in practice:

  • Each build mints a fresh, short-lived token from the installation. Nothing long-lived is stored, and the token never reaches your image.
  • Uninstalling the App, suspending it, or removing a repository from the installation stops future builds of anything that needs it. Already-running apps keep running; the next provision fails with a message telling you to reconnect.
  • Public repositories still work with no connection at all.
  • If several COHO organisations Connect the same GitHub App installation, every private repository on that install’s Selected list is buildable by each of those organisations. Keep customer or partner private repos on an install that only their COHO accounts bind — do not share one installation across trust boundaries.

COHO reads only repository contents and metadata. It does not subscribe to push events, so pushing to your repository does not trigger a deploy — you still update the app explicitly.

When the Store registers your app as COHO-hosted, CodeBuild builds your repository’s Dockerfile and runs the image on Atrium Hosting. The runtime contract in §§3–5 still applies. On top of that, the image must satisfy:

Requirement Detail
Platform linux/amd64 (Hosting builds with --platform linux/amd64).
Listen port Honour PORT. Hosting sets PORT=8080 and maps container port 8080. Listening on both your local default and 8080 is fine.
Data directory Persist under ATRIUM_DATA_DIR (Hosting sets /data). That path is an EFS mount and must survive re-provision.
Process UID/GID Hosting runs the task as UID 65532 and GID 65532, matching the /data EFS access point. Dockerfile USER is overridden.
Setup secret Read ATRIUM_SETUP_SECRET from the environment. Hosting injects it; do not bake it into the image.
Runtime secrets Optional publisher-set env (API keys, proxy URLs, …). Hosting injects each key on provision — see Runtime secrets (Hosting). Do not bake secrets into the image.
Frame ancestors Read ATRIUM_FRAME_ANCESTORS and set Content-Security-Policy: frame-ancestors … from it on quick-view HTML. Hosting injects one Store origin for that environment. Do not hardcode a domain list in your image. Self-host values: §6.
Parent origin Read ATRIUM_PARENT_ORIGIN for atrium.quickView.close postMessage. Hosting injects the same Store origin as ATRIUM_FRAME_ANCESTORS. Self-host: same table as frame ancestors.

Why Hosting sets 65532: the /data volume is an EFS access point owned by 65532:65532. ECS on EC2 chowns an empty mount to the task user. If the process ran as another UID (Node USER node at 1000, Microsoft aspnet USER app at 1654), the task never starts: CannotCreateContainerError / lchown ... operation not permitted, and provision times out as Failed. Hosting therefore sets the ECS container user to 65532:65532 so those Dockerfiles still boot. Access points also reject lchown to the same UID, so Hosting creates /{app}/.keep owned by 65532 on the filesystem before the first real task so Docker skips copy+chown on an empty volume. Keep application files readable by that user (avoid chmod 700 trees owned only by another UID). Writing belongs under ATRIUM_DATA_DIR (/data).

samples/hello-go already uses distroless nonroot (65532). You do not need to remap USER in the Dockerfile.

Self-hosted apps ignore this section — you own the process and the disk.