CarlyEmail docs

Team

Share an organization's inboxes with the people you work with.

An organization can have more than one person in it. Each person signs in with their own email address, gets their own keys, and can use the inboxes they were given — so a support address can be worked by the whole team instead of through one shared key.

Roles

Role Can
owner Everything. The address the organization was opened with; there is one, and it cannot be removed or demoted.
admin Everything the owner can, including inviting and removing people.
member Read, reply, draft and label mail in their inboxes. Cannot change the organization — no keys, domains, webhooks, inboxes, lists or people.

A member can be given every inbox (the default) or only some. Owners and admins always see every inbox.

Inviting someone

curl -X POST https://api.carlyemail.com/v0/members \
  -H "Authorization: Bearer $CARLYEMAIL_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"email": "priya@rivercycles.co.uk", "role": "member",
       "inbox_ids": ["support@rivercycles.co.uk"]}'

They get an email saying who added them and which inboxes they can use. There is no link to click: they sign in with that address — at the console, with the CLI, or through POST /v0/agent/sign-in — and the invitation becomes a membership then. Until they do, their status is invited.

Only owners and admins can invite, and only once the owner email is confirmed. Each plan has a cap on people, the owner included — see limits.

Changing and removing people

PATCH /v0/members/{member_id} changes a role, inboxes or name; only the fields you send change. DELETE /v0/members/{member_id} removes someone and revokes every key they held, including chat clients they connected. Anyone can remove themselves.

GET /v0/members lists everyone. Every role can read it, because assigning work starts with knowing who is on the team.

Signing in to more than one organization

One address can belong to several organizations — its own, and teams it was invited to. Signing in lands in the one it owns, or else the one used most recently; pass organization_id to POST /v0/agent/sign-in/verify to choose.

GET /v0/auth/me lists them all, and POST /v0/auth/switch with an organization_id returns a key there, without another code:

curl -X POST https://api.carlyemail.com/v0/auth/switch \
  -H "Authorization: Bearer $CARLYEMAIL_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"organization_id": "org_..."}'

A chat client connected over OAuth with a teammate's address joins their organization as them, with their role and inboxes.

Keys for people and keys for agents

A key from signing in speaks for that person: their role and inbox access apply to everything it does. Keys made with POST /v0/api-keys speak for the organization, as they always have, and are how you hand one agent one inbox — see authentication.

A member limited to several inboxes is asked to name one (inbox_id_required) wherever a listing would otherwise cover the whole organization, such as GET /v0/threads. Over MCP, reading without an inbox returns the newest mail from all of theirs.

Who did what

Everything the organization sends records who sent it, as sent_by on the message: a person (type: "member", with member_id, name and email) or a key made for an agent (type: "api_key", with api_key_id and the key's name). Name your agents' keys and each shows up as itself.

"sent_by": { "type": "member", "member_id": "mem_...", "name": "Priya Shah", "email": "priya@rivercycles.co.uk" }

Drafts carry created_by and updated_by, and a scheduled draft is sent as whoever wrote it. Label changes in an inbox's history (GET /v0/inboxes/{inbox_id}/events) carry actor, which is how one teammate or agent can tell that another already handled a message.

Mail received from outside, and anything sent before this was recorded, has no sent_by.

Working the inbox together

Assigning. Hand a conversation to a person, to an agent's key, or to yourself, and take it back with null:

curl -X PATCH https://api.carlyemail.com/v0/inboxes/support@rivercycles.co.uk/threads/thread_... \
  -H "Authorization: Bearer $CARLYEMAIL_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"assignee_id": "mem_..."}'

assignee_id takes a member_id, an api_key_id, or "me". Mail is never handed to someone who cannot open its inbox. Every conversation carries assignee, and listings filter with ?assignee=me, ?assignee=none for the unassigned, or an id. Over MCP, update_thread takes assignee_id and list_threads takes assignee. A new message on a conversation leaves it with whoever has it.

Notes. Leave a note on a conversation for the team. Notes are never sent, never delivered to webhooks, and come back with the conversation, so an agent reading it sees what a teammate wrote:

curl -X POST https://api.carlyemail.com/v0/inboxes/support@rivercycles.co.uk/threads/thread_.../notes \
  -H "Authorization: Bearer $CARLYEMAIL_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"text": "Customer since 2019. Refund approved."}'

Over MCP it is add_note. Whoever wrote a note can delete it, and so can an owner or admin.

Someone is already replying. GET on a conversation lists drafts: the unsent replies started on it, with created_by and updated_by. The console shows "Priya is writing a reply" so two people do not answer the same email.