# Calls you make to COHO

Atrium app developer spec, section 5: Calls you make to COHO.

## Public API v1.1

- Base: `{apiBaseUrl}/v1.1/public/`
- Auth: `Authorization: Bearer <apiKey>` from setup
- OpenAPI: [Scalar COHO Public API 1.1](https://api.coho.life/scalar) — 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.

## 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 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`).


```json
{
  "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)

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

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:

```json
{
  "$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:

```json
{
  "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)

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)

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

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.

## 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)](#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](#self-host-store-origins). |
| 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.
