Public API v1.1
Section titled “Public API v1.1”- 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
mediumandisAutomated.isAutomatedistruewhen the sender is an App Connection principal (Store-connected app), otherwisefalse. POST /v1.1/public/conversationsacceptsmediumCOHO_MESSENGER(default),EMAIL,SMS, orWHATSAPP. Email/SMS broadcasts requireAllowReplies=false. SMS send calculates segments and debits landlord SMS credits server-side (optionalGET /organisation/credits?type=SMSandPOST /organisation/sms-credit-quotewithbodyto inspect cost). There is no clientSmsSettingsfield on create. WhatsApp needs an active org connection.GET /v1.1/public/conversations/{reference}/messages?includeDrafts=trueincludes 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}/messagesreplies on an existing thread. Optionalfiles,idempotencyKey(Redis, 24h), and per-recipientrecipients(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-tagslists 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}/availabilitywrapsMarkAsAvailableFrom/MarkAsUnavailable.- Property tenants include
gender, approvedoccupation, interest references, and optional profileimage.reference. Fetch bytes withGET /v1.1/public/images/{reference}/signed-url(file images stay onGET /v1.1/public/files/{reference}/signed-url).PATCH .../tenants/{tenancyTenantGuid}/photosets that photo from an uploaded image reference. - Creating a maintenance issue accepts
files(new upload) andfileReferences(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-managerssends an in-app notification (organisation.write) with a same-originlinkPathstarting with/.GET /v1.1/public/guarantorspages organisation guarantor details (tenancies.read). Org-wide Right to Rent remainsGET /v1.1/public/right-to-rent/report.- Creating or converting a maintenance issue returns
tenantUrlonly when the issue is shared with the tenant. - Maintenance issues include
assignedOrganisationPersonGuid,supplierGuidandupdated;GET maintenanceacceptsdetailedStatuses. Viewings include their state (agreedDate,invitedByManager,applicantResponse,managerResponse,outcome,proposedDateSet), assignee and timestamps. Tenancy tenants includecohoAccountStatusanddateInvitedToCoho.GET organisation/usersincludespersonReference, which matches the usage token’spersonReferenceclaim.
Store logs
Section titled “Store logs”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 ownsGET /v1.1/public/atrium/publisher-profile— get Built-by / company listing disclosuresPATCH /v1.1/public/atrium/publisher-profile— update company disclosures (country, support, privacy, sub-processors, licence)POST /v1.1/public/atrium/apps/external— register self-hostedPOST /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_reviewPOST /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.
Light config (app-owned)
Section titled “Light config (app-owned)”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.
Supported JSON Schema contract
Section titled “Supported JSON Schema contract”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 (AI access)
Section titled “MCP (AI access)”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 |
Private GitHub details
Section titled “Private GitHub details”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.
- In the Store (managers) or the publisher portal (publishers), choose GitHub repository as the source and select Connect GitHub.
- GitHub asks which account and which repositories to grant. Grant only the repositories you want COHO to build (Selected repositories).
- 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.
- 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.
Dockerfile contract (Atrium Hosting)
Section titled “Dockerfile contract (Atrium Hosting)”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.