Short answer: A practical guide to the Aamu API for newsletters, tasks, docs, meetings, files, forms, database automations, GraphQL rows, and activity timelines.

The Aamu API lets integrations search workspace content, manage projects, work with private and group Chats, and use project Posts, Tasks, Docs, meetings, Forms, Newsletters, Git, files, Helpdesk, Email and Databases. Database rows are available through GraphQL and REST; webhooks notify external systems of changes.

The API is described by an OpenAPI document at /.well-known/openapi.json. That document is useful both for humans and for AI tools that need to discover available operations.

Authentication and project scope

Every API request uses a Team API key in the x-api-key header. Project-scoped resources, including Posts, also use x-project-id. Chat is scoped to selected rooms and does not use a project header. When an API key has access to multiple projects, the project header disambiguates which project the request should use.

x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

API keys can be scoped by feature and permission. For example, one key can have read-only Docs access, while another can create tasks, upload files, submit forms, manage Git repositories and SSH keys, manage newsletters, and manage database automations in selected projects.

Database schema and row access use the Database scope. Automation definitions use a separate Automations scope, because permission to work with database data should not automatically grant permission to create workflows with side effects. Automation actions also require the scope of their destination: Tasks write for create_task and Emails write for send_email.

Team admins create keys from Team settings under API keys. The generated secret is shown only once, so copy it immediately. The stored key list later shows safe metadata such as name, creation time, last-used time, scopes, permissions, endpoints and required headers, but it never shows the secret again.

The API documentation links are also available from the same settings page: /.well-known/openapi.json, the alternative /api/openapi.json, and the AI plugin manifest at /.well-known/ai-plugin.json.

API actor

Most project write operations can set the acting user with x-aamu-actor. The value can be a username, such as ai or badding, or a user id. The actor must be a member of the scoped project.

When the header is omitted, Aamu uses the project ai user when available, otherwise the project owner. The same fallback applies across project item writes: tasks, docs, meetings, newsletters, form submissions, file upload registration, and database creation/schema operations.

x-aamu-actor: ai

Chat, Posts, discussion comments on Docs and Meetings, and project administration use the API key creator and ignore x-aamu-actor. Chat messages additionally display the agent name from the API key. Posts and discussion comments have no agent label. This differs from creating or editing a Doc or Meeting itself, which uses the project actor rules above.

API key introspection

Integrations can inspect the API key they are currently using with GET /api/v1/key. This is useful for setup checks, debugging missing permissions, and choosing the right project header when a key has multiple scopes.

The endpoint returns safe metadata only. It never returns the API key value, salt, hash, or any other secret used to validate the key.

GET: inspect current API key

GET /api/v1/key
x-api-key: YOUR_API_KEY

Example response:

{
  "key": {
    "id": "API_KEY_ID",
    "name": "Support automation",
    "permissions": ["read", "write", "comment"],
    "scopes": [
      {
        "feature": "helpdesk",
        "resource_id": "PROJECT_ID",
        "permissions": ["read", "write", "comment"]
      },
      {
        "feature": "team-brain",
        "resource_id": "PROJECT_ID",
        "permissions": ["read", "write", "comment"]
      }
    ],
    "created": 1780000000000,
    "created_by": "USER_ID",
    "last_used_at": 1780000000000,
    "expires_at": null
  }
}

Use the returned scopes list to confirm which features the key can access. Project-scoped endpoints still use x-project-id when the project needs to be disambiguated.

Workspace search

GET /api/v1/search searches visible workspace content using the key's existing feature Read scopes; no separate Search scope is required. Results respect the creator's current team/project membership, private Doc ownership, personal Email ownership and Chat room/recipient access.

GET /api/v1/search?q=release&types=docs,posts,tasks&limit=50
x-api-key: YOUR_API_KEY

q is a literal, case-insensitive substring of 2–200 characters. Available types are chat, databases, docs, emails, files, helpdesk, meetings, posts and tasks. Omit types to search all readable types. An optional project_id query or x-project-id header limits the project and excludes private/group Chats. Search includes published threaded discussion replies, excludes deleted/system bodies and returns safe summaries: type, id, project_id, room_id, title, excerpt and updated.

Search always returns bounded pages (default 50, maximum 100), ordered by updated time, id and resource type. Use cursor=next_cursor while has_more is true. Keep the query, filters and access unchanged; changed access invalidates the cursor. Narrow q, types or project if the search times out.

Project administration

Enable Project administration on a Team API key. The scope is team-wide: { "feature": "projects", "resourceId": "TEAM_ID" }. Individual project selections do not restrict this scope. Mutations need explicit Admin permission and a currently active team administrator as key creator. A wildcard permission alone is insufficient. No project header is needed.

POST /api/v1/projects
x-api-key: YOUR_API_KEY
Idempotency-Key: integration-project-001
Content-Type: application/json

{ "name": "Integration", "description": "Shared integration work" }

Creation returns HTTP 201 and initializes normal counters, task lists, Helpdesk defaults and owner membership. Names are 1–21 characters; descriptions are at most 10,000. Duplicate names or channels return 409.

  • GET /api/v1/projects?status=active&limit=50 lists projects; status also accepts archived or all. Existing feature Read keys can list their scoped projects with current owner access.

  • GET /api/v1/projects/PROJECT_ID reads safe metadata. An existing project Read scope with current project access is also accepted.

  • PATCH /api/v1/projects/PROJECT_ID changes name, description or icon. Icons accept HTTPS or app-relative URLs. Owner, tenant and channel cannot be overwritten.

  • POST /api/v1/projects/PROJECT_ID/archive and /unarchive change lifecycle state with an empty body. The main project cannot be archived through this API.

  • GET /api/v1/projects/PROJECT_ID/users lists active members. POST on the same path adds { "user_id": "USER_ID" }, an active same-team member.

  • DELETE /api/v1/projects/PROJECT_ID/users/USER_ID removes a member. The project owner cannot be removed.

Membership changes use the normal graph, project membership and notifications. Project-management POST operations support Idempotency-Key.

List pagination and change filters

Common list parameters are limit, cursor and updated_since; paged responses include has_more and next_cursor. Treat cursors as opaque and retain the same API key, project and filters. Every page rechecks access.

On legacy Docs, Meetings, Forms, Newsletters, issues/subscribers, Git repositories, Users, Projects and Webhooks lists, any of those parameters opts into bounded pages (default 50, maximum 100). Calls without paging arguments preserve their previous order and response. Git's legacy limit-only values above 100 retain their existing behavior. Email and Helpdesk also accept this paged mode. Filtering may leave short or empty pages; advance while has_more is true.

Files, Search and unread Chat summaries always use bounded pages. Chat room lists support opt-in paging. Posts and Chat message history retain their message/post ID cursors as described above; discussion comments use scoped opaque cursors. Tasks retains its existing paging/sort contract and accepts updated_since as an alias for updated_after. Database rows, activity reports and external Git histories retain their specialized contracts.

updated_since is exclusive and accepts an ISO timestamp or Unix milliseconds. Lists use created when updated is absent. Personal Email pages are limited to the key creator. These filters expose current visible records rather than a complete deletion ledger; use deletion webhooks where available. Concurrent changes can alter page membership.

Safe retries beyond Tasks

Chat/Posts creates, their comments, Docs/Meetings discussion comments and the new Chat/Project management POST operations support Idempotency-Key, a nonempty string of up to 200 characters. Generate one key per intended action and reuse it only for an identical retry. Completed requests replay the saved HTTP status and body for 24 hours with Idempotency-Replayed: true. Current access is checked before replay.

A changed payload with the same key returns 409. Processing or unconfirmed results also return 409: inspect the resource and do not resend under a fresh key. Storage or result-confirmation failures return 503. Completed errors are replayed too. Without the header, inspect current content before retrying an uncertain create. This contract covers the listed operations; consult each operation in OpenAPI for other APIs' retry and precondition support.

Team Brain

Team Brain is the shared knowledge layer behind Aamu AI. It can be queried directly through the API when an integration needs grounded context before it writes a task, drafts a support reply, creates a doc, or decides what to do next.

The retrieve endpoint returns matching curated Team Brain entries and source chunks. It does not generate a final answer by itself; callers can use the results as context for their own model, or let Aamu use the same knowledge through a workflow such as Helpdesk reply-draft generation.

POST: retrieve knowledge

limit controls the maximum number of matching results returned. Results are ranked by relevance score, so a smaller limit is useful when an AI prompt needs only the strongest few pieces of context, while a larger limit gives the caller more material to inspect or rerank.

POST /api/v1/team-brain/retrieve
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "query": "How should we answer a billing cancellation question?",
  "limit": 8
}

Example response:

{
  "results": [
    {
      "kind": "brain",
      "score": 0.82,
      "title": "Cancellation billing policy",
      "text": "Explain the policy clearly and ask for the account email if needed.",
      "urls": []
    }
  ]
}

Use Team Brain read scope for the project. When Helpdesk draft generation is configured to use Team Brain, the same Team Brain read scope is required in addition to Helpdesk write scope.

Users

The Users API resolves project members for actor headers, task assignees and integrations that need stable Aamu user ids. It accepts any project read scope, such as Tasks, Docs, Git, Newsletters, Helpdesk or Emails.

GET: list users

GET /api/v1/users/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

The older ?username=... query filter is still supported for backwards compatibility, but one-user lookups should use the resource endpoint below.

GET: get one user

GET /api/v1/users/badding
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

The path value can be a username or user id. Example response:

{
  "user": {
    "id": "USER_ID",
    "username": "badding",
    "name": "Badding",
    "email": "badding@example.com"
  }
}

User activity reports

The Reports API provides project-scoped user activity totals, day/week/month time series, and event timelines grouped by local calendar day. Use a Team API key with Reports read scope and the normal x-project-id header.

An active day includes every matching activity type rather than comments alone. The event stream can contain activity from Tasks, Docs, Git, Helpdesk, meetings, files, databases, comments, and other supported project features.

GET: report all project users

GET /api/v1/reports/users/?from=2026-07-01&to=2026-08-01&timezone=Europe/Helsinki&interval=week
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

from is inclusive, to is exclusive, and the maximum range is 366 days. The timezone controls calendar-day boundaries. Supported intervals are day, week, and month. Use users with comma-separated ids or usernames and metrics to select only the required values.

The first metrics include total events, active days, distinct touched items, comments, commits, created branches, and created, merged, closed, or reopened pull requests. Project totals, per-user values, and continuous intervals are returned together.

GET: report one user

GET /api/v1/reports/users/ada?from=2026-07-01&to=2026-08-01&timezone=Europe/Helsinki&interval=day

The path accepts a username or user id.

GET: list a user’s activity by day

GET /api/v1/reports/users/ada/activity?from=2026-07-01&to=2026-08-01&timezone=Europe/Helsinki&categories=git,comments&limit=100

The activity endpoint returns safe event metadata grouped by date. Filter with broad categories or exact event types, and continue with the opaque next_cursor. Its summary covers the complete filtered range rather than only the current page.

Activity history begins when normalized event collection is enabled. Check the response’s data_coverage object before comparing older periods; the API does not infer a complete event history from the current state of tasks, tickets, documents, or repositories.

For complete examples and metric definitions, see Report on user activity with the Aamu API.

Git

The Git API manages repositories, files, branches, commits, diffs and pull requests, alongside repository settings, branch protection, push mirrors, CI and public SSH keys.

Use Git read scope for reads, Git write scope for code and pull request changes, and Git admin scope for repository creation, deletion and administrative settings. SSH key changes use Git write scope. Project-scoped requests use x-project-id when needed.

SSH key endpoints only work with public keys. Aamu never receives, stores, or returns private SSH keys. By default the key owner is the user who created the Team API key; x-aamu-actor is intentionally not used for Git SSH key management because SSH keys are persistent credentials.

GET: list Git repositories

GET /api/v1/git/repos/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Example response:

{
  "repos": [
    {
      "id": "REPO_ID",
      "pid": "YOUR_PROJECT_ID",
      "name": "customer-portal",
      "status": "active",
      "default_branch": "main",
      "private": true,
      "ssh_url": "git@git.example.com:team/customer-portal.git",
      "html_url": "https://git.example.com/team/customer-portal"
    }
  ]
}

POST: create a Git repository

POST /api/v1/git/repos/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "name": "customer-portal",
  "description": "Customer-facing portal code",
  "private": true,
  "auto_init": true
}

The response returns the Aamu repository id together with Git clone URLs from the team Git service. Use that id for repo-specific task endpoints such as /api/v1/repos/{repoId}/tasks/.

DELETE: delete a Git repository

DELETE /api/v1/git/repos/REPO_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Deleting a repository removes it from the team Git service and marks the linked Aamu repository record deleted.

GET: list Git SSH keys

GET /api/v1/git/keys/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Example response:

{
  "keys": [
    {
      "id": "KEY_ID",
      "title": "CI deploy key",
      "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...",
      "fingerprint": "SHA256:...",
      "owner": "USER_ID"
    }
  ]
}

POST: add a Git SSH key

POST /api/v1/git/keys/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "title": "CI deploy key",
  "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... ci@example"
}

DELETE: remove a Git SSH key

DELETE /api/v1/git/keys/KEY_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Helpdesk

The Helpdesk API is designed for human-in-the-loop support automation. Integrations can read new tickets, prepare reply drafts, and send a draft only through an explicit send command.

Reply drafts are stored in the same user-specific comment draft location the UI uses, and the ticket is marked with hasDraft. In practice this means the draft appears in the Helpdesk reply editor for the API actor, ready for a human to review and send.

GET: list tickets

GET /api/v1/helpdesk/tickets/?status=open&unanswered=true
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

GET: resolve a Helpdesk actor

Use this endpoint to check a Helpdesk actor and the project Helpdesk mailbox used for email tickets. The actor can be a username or user id. The response includes safe mailbox metadata only, never passwords or tokens.

GET /api/v1/helpdesk/actors/ile
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Example response:

{
  "actor": {
    "id": "USER_ID",
    "username": "ile",
    "name": "Ilkka Huotari",
    "email": "user-account@example.com",
    "helpdesk_name": "Ilkka",
    "mailbox": {
      "email": "support@example.com",
      "name": "Support",
      "configured": true
    }
  }
}

PUT: write a reply draft

PUT /api/v1/helpdesk/tickets/TICKET_ID/reply-draft
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ai
Content-Type: application/json

{
  "html": "<p>Hei, kiitos viestistä. Tarkistin tilanteen ja...</p>",
  "mode": "replace"
}

POST: generate a reply draft

Aamu can also generate the draft with Team AI. By default this uses Team Brain retrieval as context, so the API key needs both Helpdesk write scope and Team Brain read scope for the project.

POST /api/v1/helpdesk/tickets/TICKET_ID/reply-draft/generate
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ai
Content-Type: application/json

{
  "instructions": "Answer in Finnish, friendly and concise.",
  "use_team_brain": true,
  "mode": "replace"
}

The generated draft is saved as the API actor’s Helpdesk comment draft. It is not sent automatically.

POST: send a reply draft

Sending is deliberately separate from writing or generating a draft. The send endpoint takes the current draft for the API actor, sends it through the same Helpdesk comment/email path as the UI, clears the draft, and returns the sent comment plus the updated ticket.

Use Helpdesk comment permission for this endpoint. x-aamu-actor selects whose draft is sent, so integrations can safely keep AI-generated drafts under an ai actor or send as a specific user when that is intended.

POST /api/v1/helpdesk/tickets/TICKET_ID/reply-draft/send
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ai

Example response:

{
  "comment_path": "comments.3",
  "comment": {
    "id": "COMMENT_ID",
    "html": "<p>Hei, kiitos viestistä...</p>",
    "from": "ai"
  },
  "ticket": {
    "id": "TICKET_ID",
    "has_draft": false
  }
}

Emails

The Email API follows the same human-in-the-loop model as Helpdesk: integrations can list email threads, write or generate a user-specific reply draft, and send that draft only through an explicit send command.

Drafts are stored in the same user-specific comment draft location the email UI uses and the email is marked with hasDraft. Use x-aamu-actor to select whose draft is written, generated or sent.

GET: list emails

limit controls the maximum number of email threads returned. Use status=unanswered or unanswered=true when building an AI reply queue.

GET /api/v1/emails/?status=unanswered&limit=20
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

GET: resolve an email actor

Use this endpoint to check which project-specific email address an actor uses when sending email replies. The actor can be a username or user id. The response includes safe mailbox metadata only, never passwords or tokens.

GET /api/v1/emails/actors/ile
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Example response:

{
  "actor": {
    "id": "USER_ID",
    "username": "ile",
    "name": "Ilkka Huotari",
    "email": "user-account@example.com",
    "mailbox": {
      "email": "project-mailbox@example.com",
      "name": "Kansalaiskeskustelu",
      "configured": true
    }
  }
}

GET: search email contacts

Use contacts when selecting recipients for new drafts or sent emails. The API also auto-resolves address-only recipients to existing contacts by email address. Missing contacts are created only when create_missing_contacts is explicitly set to true.

GET /api/v1/emails/contacts/?q=ilkkah&limit=10
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ile

POST: create a new email draft

Use this endpoint to create a new outbound email draft for the API actor without sending it. The draft appears in the same email draft list the UI uses. Use Emails write permission for this endpoint.

POST /api/v1/emails/drafts/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ile
Content-Type: application/json

{
  "subject": "Draft from Aamu API",
  "html": "<p>This draft has not been sent.</p>",
  "to": [
    { "address": "ilkkah@gmail.com", "name": "Ilkka" }
  ],
  "create_missing_contacts": true
}

POST: send a new email

Use this endpoint to send a new outbound email, not a reply to an existing thread. It creates a draft for the API actor, sends it through the same project email path as the UI, and returns the sent email thread. Use Emails comment permission for this endpoint.

POST /api/v1/emails/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ile
Content-Type: application/json

{
  "subject": "Test email from Aamu API",
  "html": "<p>Hello from the Aamu Email API.</p>",
  "to": [
    { "address": "ilkkah@gmail.com", "name": "Ilkka" }
  ]
}

PUT: write a reply draft

The API chooses the original sender/contact as the default recipient when possible. You can pass to explicitly if the integration wants to override recipients.

PUT /api/v1/emails/EMAIL_ID/reply-draft
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ai
Content-Type: application/json

{
  "html": "<p>Hei, kiitos viestistä. Palaamme tähän pian.</p>",
  "mode": "replace",
  "to": [
    { "address": "customer@example.com", "name": "Customer" }
  ]
}

POST: generate a reply draft

Email draft generation supports optional instructions for tone, language or constraints. By default it uses Team Brain retrieval as context, so the API key needs both Emails write scope and Team Brain read scope for the project.

POST /api/v1/emails/EMAIL_ID/reply-draft/generate
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ai
Content-Type: application/json

{
  "instructions": "Answer in Finnish and keep it short.",
  "use_team_brain": true,
  "mode": "replace"
}

POST: send a reply draft

Sending uses the same email-comment/send path as the UI. The endpoint sends the current draft for the API actor, clears it, and returns the sent comment plus the updated email thread. Use Emails comment permission for this endpoint.

POST /api/v1/emails/EMAIL_ID/reply-draft/send
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ai

Chat API

The Chat API lets an external AI agent read and reply in private and group chats, create rooms, manage participants and acknowledge unread messages. Enable Chat on a Team API key and select the allowed rooms. Chat uses room scopes and does not need x-project-id. The key creator must remain an active team member and room participant; history includes only messages sent by or addressed to that user.

Messages are sent as the user who created the API key, with a separate visible Agent: API key name label. Give each agent a descriptive key name, such as Codex. The server sets the sender, recipients and agent identity. Human messages have agent: null; API messages return agent: { name, key_id }. The label is a snapshot and survives later edits.

GET: list chats and read messages

GET /api/v1/chats?limit=50
x-api-key: YOUR_API_KEY

Room objects contain id, users, admin, created and updated. Use GET /api/v1/chats/ROOM_ID for one room. Without paging arguments the list returns up to 100 rooms; adding limit, cursor or updated_since opts into bounded pages.

GET /api/v1/chats/ROOM_ID/messages?limit=50
x-api-key: YOUR_API_KEY

Message pages contain messages, has_more and next_cursor. Each message exposes its id, room_id, from, html, text, created, updated, deleted and agent. The default is newest first, 50 messages; limit accepts 1–100. Read older messages with before=next_cursor or its cursor alias. Poll newer messages with after=MESSAGE_ID, which returns oldest first. Do not combine before/cursor with after. Optional updated_since filters created or edited messages.

Drain every has_more page before waiting. An empty page has a null cursor: retain the previous polling position. Ignore the agent's own replies to avoid loops. Deleted messages appear as content-free tombstones.

POST: send a message

POST /api/v1/chats/ROOM_ID/messages
x-api-key: YOUR_API_KEY
Idempotency-Key: release-check-message-001
Content-Type: application/json

{ "text": "The requested checks are complete." }

Write includes Read. Provide exactly one of text or html; content is sanitized, must contain text, and is limited to 100,000 characters of final HTML. Success returns HTTP 201 with { "message": ... }. Sending uses normal unread tracking and realtime notifications without triggering Aamu's built-in AI replies.

Create rooms and manage participants

Enable All my chats, including new rooms to grant Chat scope for *. With Write, POST /api/v1/chats accepts { "user_ids": ["OTHER_USER_ID"] }, includes the key creator and makes that user room administrator. Every invitee must be an active member of the same team. A key restricted to existing room IDs cannot create new rooms.

  • POST /api/v1/chats/ROOM_ID/users accepts user_ids to invite participants.

  • DELETE /api/v1/chats/ROOM_ID/users/USER_ID removes a participant; the key creator must administer the room and cannot remove themselves.

  • PATCH /api/v1/chats/ROOM_ID/messages/MESSAGE_ID edits an own message using text or html.

  • DELETE /api/v1/chats/ROOM_ID/messages/MESSAGE_ID soft-deletes an own message, clearing its content and embeds.

Own-message changes require Write and author ownership by the key creator, including that user's earlier human messages. Other participants' messages cannot be changed.

Unread state and private-chat webhooks

GET /api/v1/chats/unread returns paginated scoped unread rooms with room_id, unread_count and up to 100 message_ids each. POST /api/v1/chats/ROOM_ID/read acknowledges current unread messages; optional { "through_message_id": "MESSAGE_ID" } preserves newer unread messages. Both need Read. Read acknowledgements and incoming unread updates are serialized to preserve concurrent arrivals.

Subscribe to chat.message.created, chat.message.updated or chat.message.deleted with explicit room_ids in a webhook. Payloads contain room_id, message_id, participant_ids and agent metadata, without message content. The subscription creator must participate in the selected rooms, and membership is checked again at delivery. Fetch bodies using a separately scoped Chat key. Team/project wildcards do not grant private-chat access.

Message sends, room creation, invitations and read acknowledgements support the durable Idempotency-Key retry contract below. Invalid keys return 401, missing permissions or inactive owners 403, inaccessible rooms/messages 404 and invalid input 400.

Posts API

The Posts API reads and publishes project discussions, handles threaded comments, and edits or soft-deletes the author's own content. Select Posts, allowed projects and Read or Write on a Team API key. Use x-project-id to select the project when needed. The key creator must remain an active team member and project participant; team administrators can use projects covered by their key scopes.

Posts and comments are published directly as the user who created the API key, with no agent label or agent metadata. The body cannot override author, project, status or attribution; x-aamu-actor is ignored.

Read and publish posts

GET /api/v1/posts?limit=50
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

The response contains posts, has_more and next_cursor. Posts sort newest first, then by id; limit accepts 1–100, default 50. Continue with before=next_cursor or cursor=next_cursor. Optional updated_since filters changed posts. GET /api/v1/posts/POST_ID returns one published post; drafts, deleted posts, shared-comment wrappers and other projects' posts are unavailable.

POST /api/v1/posts
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Idempotency-Key: release-discussion-001
Content-Type: application/json

{ "text": "The release is ready for review. Please add feedback below." }

Publishing requires Write and returns HTTP 201 with { "post": ... }. Post fields include id, project_id, from, html, text, created, updated and comment_count.

Threaded comments and replies

GET /api/v1/posts/POST_ID/comments?limit=50
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Comment pages contain comments, has_more, next_cursor and the compatibility next_offset. Published replies at every depth are included, ordered by creation time and id. System comments and deleted bodies are excluded. Each comment includes parent_comment_id (null for top-level comments) and updated. A published reply under a deleted parent remains visible without the parent's body.

Prefer cursor=next_cursor. Legacy offset remains supported but cannot be combined with cursor. Optional reply_to filters direct replies, and updated_since filters changes. Keep all filters unchanged when advancing.

POST /api/v1/posts/POST_ID/comments
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Idempotency-Key: release-review-reply-001
Content-Type: application/json

{ "text": "The review is complete.", "reply_to": "COMMENT_ID" }

Omit reply_to for a top-level comment. The reply target must be a published comment on this post. Creation requires Comment (included in Write keys from the UI) and returns HTTP 201 with { "comment": ... }.

Edit and soft-delete own content

  • PATCH /api/v1/posts/POST_ID changes an own published post with text or html; DELETE on the same path soft-deletes it. Requires Write.

  • PATCH /api/v1/posts/POST_ID/comments/COMMENT_ID edits an own comment; DELETE on the same path soft-deletes it. Requires Comment.

Ownership means the key creator, even with Admin permission. System comments cannot be edited. Content creation and editing accept exactly one of text or html, sanitize it, require nonempty text and limit final HTML to 100,000 characters. Replies additionally accept reply_to. Normal publication, notification and webhook pipelines are used. Both POST creation endpoints support Idempotency-Key.

Tasks

The project-scoped Tasks API can list and filter work, create tasks, update task content and scheduling, assign project members, set tags and priority, manage custom fields and todo checklists, connect related tasks, and read or manage comments. Use a Team API key with the appropriate Tasks read, write or comment permission. Send x-project-id when a key can access more than one project, and use x-aamu-actor to attribute a write to a particular project member.

GET: list and query tasks

The default list contains non-repository tasks that are not drafts, archived or deleted; completed tasks are included. Use view=active for the current work queue. Other views are overdue, running, starred, complete and archived. Use either a shortcut view or an explicit status filter.

GET /api/v1/tasks/?view=active&assignee_id=USER_ID&sort=end_at&order=asc&limit=50
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

List queries support comma-separated status, tag and assignee_id values, and literal text search with q in the title and HTML. Date boundaries accept ISO strings or millisecond timestamps and are exclusive: start_after, start_before, due_after, due_before, created_after, created_before, updated_after and updated_before.

GET /api/v1/tasks/?q=customer+import&status=active&tag=support
GET /api/v1/tasks/?due_after=2026-11-01T00:00:00Z&due_before=2026-11-08T00:00:00Z&sort=end_at&order=asc
GET /api/v1/tasks/?updated_after=2026-10-01T00:00:00Z&sort=updated&order=asc

Supported sort fields are created, updated, start_at, end_at, completed_at and priority. Responses include has_more and next_cursor. Continue with the returned opaque cursor and the same filters, sort and order:

GET /api/v1/tasks/?view=active&sort=updated&order=desc&limit=50&cursor=NEXT_CURSOR

The task cursor is bound to the project, repository scope, sort and order. Store and resend it rather than constructing one.

Comments in list responses: task lists return comments: [] by default. Add include_comments=true to include embedded comments, or use the paginated comments endpoint below. A single-task GET still includes non-deleted comments.

GET: repository tasks and project users

Repository tasks use a separate resource path. The project list can also select one repository with repo_id, or include both normal and repository tasks with include_repo_tasks=true.

GET /api/v1/repos/REPO_ID/tasks/?view=overdue&sort=end_at&order=asc
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Resolve project members through GET /api/v1/users/ before assigning work or filling a custom user field. Responses provide stable user ids and safe profile fields. A one-user lookup accepts a username or id at GET /api/v1/users/USER_ID_OR_USERNAME.

GET: discover the project task workflow

GET /api/v1/tasks/metadata
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

This read-scoped endpoint returns a metadata object containing project_id, workflow statuses, lifecycle_statuses, priorities and active custom_fields. Status fields also expose their allowed values. Read these definitions before writing a custom field or choosing a project-specific workflow status.

POST: create a task

A task requires non-empty title and html strings. Dates accept milliseconds or ISO strings. Repetition can be daily, weekly, monthly or yearly and requires an end date. Reminder offsets are non-negative integers, expressed either as minutes_before or day/hour/minute fields d, h and m.

POST /api/v1/tasks/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: USER_ID
Idempotency-Key: customer-import-review-001
Content-Type: application/json

{
  "title": "Review customer import",
  "html": "<p>Check the import logs and report the result.</p>",
  "status": "active",
  "priority": 2,
  "tags": [
    "customer",
    "import"
  ],
  "start_at": "2026-11-03T09:00:00Z",
  "end_at": "2026-11-03T10:00:00Z",
  "users": [
    "USER_ID"
  ],
  "reminders": [
    {
      "minutes_before": 30
    }
  ],
  "todos": [
    {
      "label": "Check rejected rows"
    },
    {
      "label": "Confirm customer totals"
    }
  ],
  "comments": [
    {
      "html": "<p>Created from the support queue.</p>"
    }
  ]
}

Priority is 0 for Low, 1 for Normal and 2 for High; creation defaults to Normal. Tags are trimmed and deduplicated. A successful creation returns status 201 and a task object. If a non-essential post-creation operation fails, inspect the response’s warnings rather than assuming the task itself was not created.

PATCH: update task properties

PATCH accepts any combination of title, HTML, status, assignees, dates, repetition, reminders, tags, priority, custom fields and todos. Omitted properties stay unchanged. A tags array replaces the existing tags, and [] clears them. Send null for a date or repetition to clear it, and an empty reminders array to remove reminders.

PATCH /api/v1/tasks/TASK_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: USER_ID
Content-Type: application/json

{
  "status": "complete",
  "tags": [
    "customer",
    "reviewed"
  ],
  "priority": 1,
  "end_at": null,
  "repeat": null,
  "reminders": []
}

Repository task reads and updates use GET and PATCH /api/v1/repos/REPO_ID/tasks/TASK_ID, with the same update fields.

Custom fields and todo checklists

Use the actual field ids from /api/v1/tasks/metadata. The fields object merges supplied keys into the task’s current values; omitted keys remain, and null removes a value. The following example assumes that the selected project defines a numeric size field and a status field with the value ready:

PATCH /api/v1/tasks/TASK_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "fields": {
    "SIZE_FIELD_ID": 3,
    "STAGE_FIELD_ID": "ready"
  },
  "todos": [
    {
      "id": "check-rows",
      "label": "Check rejected rows",
      "checked": true
    },
    {
      "label": "Confirm customer totals",
      "checked": false
    }
  ]
}

Text and long-text fields take strings. Number fields take JSON numbers; their stored values are returned as decimal strings. Status fields take an allowed status value and are returned as definition objects. Date/time fields take milliseconds or an ISO string. User fields take arrays of project user ids. Location fields take an object with description and optional place_id, tz and numeric lat/lng/lon coordinates. Unknown or deleted field definitions and users outside the project are rejected.

The todos array replaces the task’s checklist. Each item has a label, an optional boolean checked, and an id. Keep an existing id when replacing the list; omit it for a new item and Aamu generates one. Duplicate ids are rejected, [] clears the list, and a task can have up to 500 items. Both fields and todos are also accepted when creating a task.

Connect blocking and duplicate tasks

Task responses include a relations object with blocked_by, blocking and duplicate arrays of related task ids within the same project. Add a relation with:

POST /api/v1/tasks/TASK_ID/relations
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "type": "blocked_by",
  "task_id": "OTHER_TASK_ID"
}

Aamu also updates the inverse relationship: a task that is blocked by another appears in that other task’s blocking list. Duplicate relationships are symmetric. Repeating an add does not duplicate the relationship. Remove one with:

DELETE /api/v1/tasks/TASK_ID/relations/blocked_by/OTHER_TASK_ID

Relation writes require Tasks write permission. The two tasks must be different, non-deleted tasks in the selected project. Repository tasks use the same suffixes under /api/v1/repos/REPO_ID/tasks/TASK_ID; the related task can be any non-deleted task in that project.

Read, add, edit and delete comments

Read comments with Tasks read permission. The endpoint returns non-deleted comments ordered by creation time and id, together with limit, has_more and next_cursor. The default page size is 50 and the maximum is 200; a cursor is bound to its task and project.

GET /api/v1/tasks/TASK_ID/comments?limit=50
GET /api/v1/tasks/TASK_ID/comments?limit=50&cursor=NEXT_CURSOR

Adding a comment requires comment permission and returns the updated task. Use a fresh idempotency key for a new comment and reuse that same key only when retrying the identical request:

POST /api/v1/tasks/TASK_ID/comments
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: USER_ID
Idempotency-Key: customer-import-comment-001
Content-Type: application/json

{
  "html": "<p>The import is ready for review.</p>"
}
PATCH /api/v1/tasks/TASK_ID/comments/COMMENT_ID
Content-Type: application/json

{
  "html": "<p>The reviewed import is ready.</p>"
}

DELETE /api/v1/tasks/TASK_ID/comments/COMMENT_ID

Editing or deleting a comment requires comment access; changing another author’s comment additionally requires Tasks write permission. System comments cannot be changed. Deletion is soft, and deleted comments disappear from normal reads. These routes are also available for repository tasks below /api/v1/repos/REPO_ID/tasks/TASK_ID/comments.

Retry mutations with Idempotency-Key

All task, relation and comment mutations accept Idempotency-Key, a non-empty string of up to 200 characters. Aamu records the request and its response durably. Retrying the same request with the same key replays the original HTTP status, body and ETag for at least 24 hours, with Idempotency-Replayed: true. This makes a timeout recoverable without creating another task or comment.

Keys are scoped to the API key, project, method, route and path parameters. The body, actor and If-Match header must agree on retries. Reusing a key with different input returns 409 IDEMPOTENCY_KEY_REUSED. A request that is still processing, or whose outcome is not confirmed, returns 409 IDEMPOTENCY_IN_PROGRESS instead of running again. An interrupted request can remain in that state until its retention period expires. Completed error responses are replayed too; a deliberately revised request needs a new key.

Protect changes with ETag and If-Match

Read a single task with GET /api/v1/tasks/TASK_ID and retain its ETag response header. Send that exact value, including its quotation marks, in If-Match when updating the task, its comments or its relations:

PATCH /api/v1/tasks/TASK_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
If-Match: "ETAG_FROM_TASK_GET"
Idempotency-Key: customer-import-update-001
Content-Type: application/json

{
  "priority": 2
}

A stale ETag returns 412. Aamu checks for intervening changes again at the actual task write, so a concurrent user edit is preserved. Without an explicit precondition, an intervening change returns 409. Read the task again and reconcile the change before submitting a revised request. An idempotent retry is replayed before the old If-Match is evaluated, so retry the original request with its original headers.

Validate inputs and handle partial failures

Unknown fields, invalid types and empty PATCH requests return 400. Use JSON numbers for priority and numeric custom fields, booleans for checklist state, and a workflow status allowed by the selected project. A failed storage operation returns an error instead of a success response.

Multi-step changes are not rolled back automatically. When a failure occurs after some writes have committed, inspect error.details.partial, committed_paths, related_commits and the current task in the error details. If a task was created before a later creation failure, its details are returned for reconciliation. A 503 TASK_RESULT_UNCONFIRMED means the final result could not be confirmed; read the current state before deciding what to retry.

Task responses provide project and repository context, ownership, scheduling, assignees, tags, priority, custom fields, checklists, relations and completion state. Combined with explicit retries and conflict handling, these operations support task queues, synchronization and workflows connected to Docs or database rows. Consult the host’s /.well-known/openapi.json for the complete operation and error schemas.

Docs

The Docs API creates durable written material directly into Aamu. It is a good fit for AI-generated summaries, runbooks, meeting notes, release notes, customer handoff documents, and internal knowledge articles.

GET: list docs

GET /api/v1/docs/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Example response:

{
  "docs": [
    {
      "id": "DOC_ID",
      "pid": "YOUR_PROJECT_ID",
      "title": "Weekly API Report",
      "status": "public",
      "html": ""
    }
  ]
}

POST: create a doc

POST /api/v1/docs/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "title": "Weekly API Report",
  "html": "<h1>Weekly API Report</h1><p>All checks passed.</p>"
}

Example response:

{
  "doc": {
    "id": "DOC_ID",
    "pid": "YOUR_PROJECT_ID",
    "title": "Weekly API Report",
    "status": "public",
    "html": "<h1>Weekly API Report</h1><p>All checks passed.</p>"
  }
}

PATCH: update a doc

PATCH /api/v1/docs/DOC_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "title": "Weekly API Report, revised",
  "html": "<h1>Weekly API Report</h1><p>All checks passed after the retry.</p>"
}

Example response:

{
  "doc": {
    "id": "DOC_ID",
    "title": "Weekly API Report, revised",
    "html": "<h1>Weekly API Report</h1><p>All checks passed after the retry.</p>"
  }
}

Threaded discussion comments

GET /api/v1/docs/ITEM_ID/comments?limit=50 reads published comments and nested replies. POST on the same path creates a comment with exactly one of text or html and optional reply_to. PATCH /api/v1/docs/ITEM_ID/comments/COMMENT_ID edits an own comment; DELETE soft-deletes it. Reads need the feature's Read permission; creates, edits and deletion need Comment. POST supports Idempotency-Key.

Discussion comments always use the API key creator, ignore x-aamu-actor and have no agent label. Lists expose parent_comment_id and updated with cursor/updated_since filtering, following the Posts discussion contract. System comments cannot be changed, and private Doc visibility still applies.

Meetings

The Meetings API can create and update project meetings. It supports fields such as name, HTML description, start time, end time, and invitee emails.

GET: list meetings

GET /api/v1/meetings/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Example response:

{
  "meetings": [
    {
      "id": "MEETING_ID",
      "pid": "YOUR_PROJECT_ID",
      "name": "API rollout review",
      "status": "public",
      "html": "<p>Review integration status.</p>",
      "start_time": 1779458400000,
      "end_time": 1779462000000
    }
  ]
}

POST: create a meeting

POST /api/v1/meetings/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "name": "API rollout review",
  "html": "<p>Review integration status and next steps.</p>",
  "start_time": 1779458400000,
  "end_time": 1779462000000
}

Example response:

{
  "meeting": {
    "id": "MEETING_ID",
    "pid": "YOUR_PROJECT_ID",
    "name": "API rollout review",
    "status": "public",
    "html": "<p>Review integration status and next steps.</p>"
  }
}

PATCH: update a meeting

PATCH /api/v1/meetings/MEETING_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "name": "API rollout review, updated",
  "html": "<p>Review production results and decide follow-up actions.</p>",
  "start_time": 1779462000000,
  "end_time": 1779465600000
}

Example response:

{
  "meeting": {
    "id": "MEETING_ID",
    "name": "API rollout review, updated",
    "html": "<p>Review production results and decide follow-up actions.</p>",
    "start_time": 1779462000000,
    "end_time": 1779465600000
  }
}

Threaded discussion comments

GET /api/v1/meetings/ITEM_ID/comments?limit=50 reads published comments and nested replies. POST on the same path creates a comment with exactly one of text or html and optional reply_to. PATCH /api/v1/meetings/ITEM_ID/comments/COMMENT_ID edits an own comment; DELETE soft-deletes it. Reads need the feature's Read permission; creates, edits and deletion need Comment. POST supports Idempotency-Key.

Discussion comments always use the API key creator, ignore x-aamu-actor and have no agent label. Lists expose parent_comment_id and updated with cursor/updated_since filtering, following the Posts discussion contract. System comments cannot be changed, and inaccessible meetings remain unavailable.

Forms API

The authenticated Forms API lets integrations create Forms, update their questions and settings, list and inspect Forms, and submit responses. Published Form responses become rows in the connected database table.

POST: create a Form

Use Forms write scope. With publish: true, Aamu creates the backing database and table, maps Form fields to database columns, and publishes the Form.

POST /api/v1/forms/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: USER_ID_OR_USERNAME
Content-Type: application/json

{
  "name": "Contact Form",
  "description": "Send us a message.",
  "publish": true,
  "fields": [
    { "title": "Name", "type": "short_text", "required": true },
    { "title": "Email address", "type": "email", "required": true },
    { "title": "Message", "type": "long_text" }
  ]
}

Supported field types include short and long text, email, number, date/time, choice fields, files, paragraphs, and page breaks. Email fields use a text-backed database column but render as HTML email inputs in the public Form.

GET: list and inspect Forms

GET /api/v1/forms/
GET /api/v1/forms/FORM_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

PATCH: update a Form

Update metadata without changing questions, or supply fields to replace the question list. Stable item_id values preserve field identity across updates. Setting publish: true publishes a draft Form or publishes newly added fields into the backing table.

PATCH /api/v1/forms/FORM_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "description": "Updated contact form.",
  "publish": true,
  "fields": [
    { "item_id": "name", "title": "Name", "type": "short_text", "required": true },
    { "item_id": "email-address", "title": "Email address", "type": "email", "required": true },
    { "item_id": "message", "title": "How can we help?", "type": "long_text", "required": true }
  ]
}

POST: submit a Form response

POST /api/v1/forms/FORM_ID/submissions
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "fields": {
    "name": "Ada Example",
    "email-address": "ada@example.com",
    "message": "I would like to hear more."
  }
}

The response contains the created row id together with the Form, database, and table ids.

Newsletters

Use the Newsletters read or write scope with x-project-id to manage Newsletters, issues, subscribers, audience selection, reviews, scheduling, reports and public archive copies.

Before sending, configure a verified email domain and a sending mailbox. Email-domain and mailbox setup is available in the UI and through the corresponding API operations described in OpenAPI.

Create a newsletter

POST /api/v1/newsletters/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "name": "Product updates"
}

List Newsletters with GET /api/v1/newsletters/. Read or update one with GET or PATCH /api/v1/newsletters/{id}. Creating a Newsletter also creates its Audience Database, Subscribers table and managed subscription automations.

Create a public signup workflow

POST /api/v1/newsletters/NEWSLETTER_ID/signup-form
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ai

This idempotent endpoint creates a published signup Form connected to the Newsletter’s existing Audience Database and its subscription workflow. It returns the Form, database and table ids, automation and public URL. The Audience Database is the source of truth; the Subscribers view is derived from it.

Create and update an issue

POST /api/v1/newsletters/NEWSLETTER_ID/issues
{
  "subject": "June product update",
  "html": "<p>What changed this month...</p>"
}

PATCH /api/v1/newsletters/NEWSLETTER_ID/issues/ISSUE_ID
{
  "subject": "June product update — revised",
  "html": "<p>The reviewed version...</p>"
}

Issues are listed at GET /api/v1/newsletters/{id}/issues and read individually at GET /api/v1/newsletters/{id}/issues/{issueId}. Only draft issues can be updated or sent.

Manage subscribers

POST /api/v1/newsletters/NEWSLETTER_ID/subscribers
{
  "email": "reader@example.com",
  "name": "Ada Lovelace",
  "tags": ["customer"]
}

PATCH /api/v1/newsletters/NEWSLETTER_ID/subscribers/SUBSCRIBER_ID
{
  "status": "unsubscribed"
}

Use GET /api/v1/newsletters/{id}/subscribers to list subscribers. The optional status query filters the result. Subscriber updates can change the name, tags, or status between subscribed and unsubscribed. API responses never return the private unsubscribe token.

Test and send explicitly

POST /api/v1/newsletters/NEWSLETTER_ID/issues/ISSUE_ID/send-test
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ai

POST /api/v1/newsletters/NEWSLETTER_ID/issues/ISSUE_ID/review
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
x-aamu-actor: ai

POST /api/v1/newsletters/NEWSLETTER_ID/issues/ISSUE_ID/send
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{ "review_token": "TOKEN_FROM_REVIEW" }

The test endpoint sends to the API actor when that address is an eligible active subscriber. Before production sending, request POST /api/v1/newsletters/{id}/issues/{issueId}/review and pass the returned review.token as review_token to /send. The send endpoint returns 202 with queued: true. Delivery runs in persistent, automatically growing batches; it is not complete when the request returns.

Schedule with POST /api/v1/newsletters/{id}/issues/{issueId}/schedule, supplying send_at, timezone and review_token. Cancel through /cancel-schedule. Public archive publication is separate from email sending: preview through /publication/preview, then pass its token to /publish.

For the product workflow behind these endpoints, see Newsletters in Aamu.app: from subscribers to sending.

Webhooks API

The Webhooks API manages team-level outbound webhooks. A webhook can subscribe to selected Aamu event types, optionally filter project-scoped events to specific projects, and send signed HTTP requests to an external endpoint.

Webhook management uses a team-level Webhooks API-key scope. It does not use x-project-id to choose the webhook owner; project filtering is configured with project_ids in the webhook definition. An empty project list accepts matching project-scoped events from all projects in the team. Private Chat events additionally require explicit room_ids and the subscription creator's room membership; project filters or wildcards never grant private-chat access.

POST: create a webhook

POST /api/v1/webhooks/
x-api-key: YOUR_API_KEY
Content-Type: application/json

{
  "name": "Form submissions",
  "url": "https://example.com/aamu/webhook",
  "events": [
    "form.created",
    "form.updated",
    "form.deleted",
    "form.submitted",
    "db.row.created",
    "db.row.updated"
  ],
  "project_ids": ["PROJECT_ID"],
  "enabled": true
}

The create response contains the signing secret. Store it securely when the webhook is created: later list and get responses expose only secret_last4, not the full secret.

For Forms integrations, form.created, form.updated, and form.deleted describe the Form lifecycle. A response emits form.submitted and also creates the backing database row, producing db.row.created. A later edit to that response row emits db.row.updated.

List, inspect, update, and delete webhooks

GET    /api/v1/webhooks/
GET    /api/v1/webhooks/WEBHOOK_ID
PATCH  /api/v1/webhooks/WEBHOOK_ID
DELETE /api/v1/webhooks/WEBHOOK_ID
x-api-key: YOUR_API_KEY

Use Webhooks read scope to list and inspect definitions. Creating, updating, enabling, disabling, and deleting webhooks requires Webhooks write scope.

GET: inspect delivery logs

GET /api/v1/webhooks/logs?limit=50
x-api-key: YOUR_API_KEY

The logs response contains recent webhook events and delivery attempts. Use it to distinguish an event that was never emitted, an event that did not match a webhook, and a delivery that reached the receiver but returned an error.

Each delivery includes an HMAC SHA-256 signature in x-aamu-signature, together with event and delivery ids. Verify the signature against the raw request body before trusting the payload. For a fuller event overview and signature example, see Outbound webhooks in Aamu.app.

Private Chat subscriptions

Select chat.message.created, chat.message.updated or chat.message.deleted and supply room_ids: ["ROOM_ID"]. Notifications from both browser and API actions contain metadata only, not message bodies. Membership is checked when configuring and delivering subscriptions; private Chat logs are restricted to viewers with current access and event-time participation. Chat events do not appear in project-wide activity reports.

Public forms

Public browser forms use the same URL for viewing and submitting. They do not use a Team API key. This is separate from the authenticated Forms API.

GET: render a public form

GET /shared/form/FORM_ID

Example response is HTML:

<form action="https://your-team.aamu.app/shared/form/FORM_ID" method="post" enctype="multipart/form-data">
  <input type="hidden" name="form_builder" value="1">
  ...
</form>

POST: submit a public form

POST /shared/form/FORM_ID
Content-Type: multipart/form-data

form_builder=1
email=person@example.com
message=I would like to hear more.

Example response is usually a redirect to the form thank-you page. For AJAX-style multipart submissions, the response can be JSON:

{
  "success": true,
  "id": "ROW_ID"
}

Files

Aamu files have two related identifiers: pointer identifies the logical file and fid identifies one stored file version. Database GraphQL media fields require both values. Obtain them from a successful Files API response; do not invent ids or store raw base64 data in a file column.

POST: upload a small file in one request

For small files, send one multipart/form-data request. Aamu receives the bytes, stores the object, creates the file and filepointer records, and returns a media object ready for GraphQL. The default limit is 10 MiB and can be configured by the server.

curl -X POST https://YOUR_AAMU_HOST/api/v1/files/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-project-id: YOUR_PROJECT_ID" \
  -F "file=@example.png;type=image/png"

The API key needs the project Files write scope.

POST: prepare a direct upload

For larger files, upload directly to object storage through a signed URL. Aamu keeps the bucket, object key, project, actor, file ids, and metadata in a short-lived server-side upload session. The client receives only an opaque uploadId plus the signed PUT request.

POST /api/v1/files/prepare-upload
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "name": "example.png",
  "type": "image/png",
  "size": 12345
}

Example response:

{
  "uploadId": "upl_OPAQUE_ID",
  "expiresAt": "2026-07-11T10:00:00.000Z",
  "upload": {
    "method": "PUT",
    "url": "SIGNED_UPLOAD_URL",
    "headers": { "Content-Type": "image/png" }
  },
  "file": {
    "file_id": "FILE_VERSION_ID",
    "file_version_id": "FILE_VERSION_ID",
    "name": "example.png",
    "type": "image/png",
    "size": 12345
  }
}

Send the bytes to upload.url with the returned method and headers. The signed URL contains the storage authorization; the client does not receive or send separate bucket or object-key fields.

POST: complete a direct upload

POST /api/v1/files/complete-upload
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "uploadId": "upl_OPAQUE_ID"
}

The upload session is project- and actor-bound, expires after a short time, and can be completed idempotently while the session remains available.

GET: read file metadata

GET /api/v1/files/FILEPOINTER_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

All three successful file operations return the same shape:

{
  "file": {
    "id": "FILEPOINTER_ID",
    "fid": "FILE_VERSION_ID",
    "pointer": "FILEPOINTER_ID",
    "filepointer_id": "FILEPOINTER_ID",
    "file_id": "FILE_VERSION_ID",
    "file_version_id": "FILE_VERSION_ID",
    "name": "example.png",
    "type": "image/png",
    "size": 12345,
    "browser_url": "/file/browser/FILEPOINTER_ID/FILE_VERSION_ID/example.png",
    "download_url": "/file/dl/FILEPOINTER_ID/FILE_VERSION_ID/example.png"
  }
}

Use a file in a GraphQL database mutation

Pass only the registered media identity to a generated GraphQL file or files field:

{
  "heroImage": {
    "fid": "FILE_VERSION_ID",
    "pointer": "FILEPOINTER_ID"
  }
}

Before inserting or updating the row, Aamu checks that both records exist, the file version belongs to the pointer, neither record is deleted, and the pointer belongs to the same company and project as the database table. A URL-only, base64-only, missing, cross-project, or mismatched media value is rejected.

The browser_url can also be embedded in Docs, Tasks, or other editor HTML. The old item-level files field is deprecated and is not returned by the current API.

GET: list and download project files

GET /api/v1/files?limit=50
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Requires Files Read and current creator access to the project. The bounded list accepts cursor, updated_since and optional folder_id, returning files, has_more and next_cursor. Deleted pointers, private-chat attachments and mismatched versions are excluded. File metadata includes api_download_url for authenticated API access and an encrypted flag.

GET /api/v1/files/FILEPOINTER_ID/download
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

The endpoint streams the stored object without browser cookies. Optional version_id must belong to that pointer and project. The API download URL is different from a browser URL and uses the same API-key/project headers. Encrypted files return 409 by default: explicitly request format=ciphertext and decrypt client-side with your workspace key. Ciphertext responses include X-Aamu-Encrypted and, when available, X-Aamu-Encryption-IV. The endpoint never receives or returns the workspace key. External pointers without stored objects cannot be downloaded this way.

Databases and GraphQL

Database rows can be read and written through GraphQL or the REST Database API. REST also manages databases, tables, columns, views, transfers and related operations.

Use GraphQL for queries and mutations against the generated typed schema. REST provides resource endpoints, including row listing, individual writes and batches.

REST row endpoints live under /api/v1/databases/{dbId}/tables/{tableId}/rows: GET lists rows, POST creates one, and GET, PATCH or DELETE with /{rowId} operates on one row. POST /rows/batch supports batch operations. Inspect OpenAPI for filters, cursors, idempotency and atomic batch options.

POST: create a database

Creating a database also creates its first table. The response returns both the database id and the initial table id.

POST /api/v1/databases/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "name": "Small-team CRM"
}

Example response:

{
  "database": {
    "id": "DB_ID",
    "pid": "YOUR_PROJECT_ID",
    "name": "Small-team CRM",
    "tables": ["DEALS_TABLE_ID"],
    "table_id": "DEALS_TABLE_ID"
  }
}

POST: add a table

Use the table endpoint when the integration needs a related table, for example companies next to deals, contacts next to accounts, or products next to orders.

POST /api/v1/databases/DB_ID/tables
x-api-key: YOUR_API_KEY
Content-Type: application/json

{
  "name": "Companies",
  "gtype": "Companies"
}

Example response:

{
  "table": {
    "id": "COMPANIES_TABLE_ID",
    "name": "Companies",
    "gtype": "Companies",
    "columns": []
  },
  "database": {
    "id": "DB_ID",
    "tables": ["DEALS_TABLE_ID", "COMPANIES_TABLE_ID"]
  }
}

POST: add columns

Columns are added to one table at a time. A reference column links rows within the same Database, including rows in the same table.

The public column types are text, longtext, link, document, documents, number, status, checkbox, timedate, timeline, tags, file, files, reference, contact, user, task, tasks, email, emails, meeting, and meetings. List-like fields such as documents, tags, tasks, emails, and meetings are written as arrays in GraphQL.

POST /api/v1/databases/DB_ID/tables/COMPANIES_TABLE_ID/columns
x-api-key: YOUR_API_KEY
Content-Type: application/json

{
  "columns": [
    { "name": "Company name", "type": "text", "gtype": "companyName" },
    { "name": "Website", "type": "link", "gtype": "website" },
    { "name": "Customer docs", "type": "documents", "gtype": "customerDocs" }
  ]
}

Then add fields to the first table and link each deal to a company:

POST /api/v1/databases/DB_ID/tables/DEALS_TABLE_ID/columns
x-api-key: YOUR_API_KEY
Content-Type: application/json

{
  "columns": [
    { "name": "Deal name", "type": "text", "gtype": "dealName" },
    { "name": "Stage", "type": "status", "gtype": "stage" },
    {
      "name": "Company",
      "type": "reference",
      "gtype": "company",
      "options": {
        "type": "one_to_one",
        "table": "COMPANIES_TABLE_ID",
        "column_show": "companyName"
      }
    }
  ]
}

options.type can be one_to_one or one_to_many. options.table points to the target table, and options.column_show points to the target column shown as the reference label. The target can be either the column id or its gtype.

document receives one existing Aamu Doc id. documents receives an array of existing Doc ids. The API links those Doc cells back to the database row, so create or resolve the Docs first instead of sending placeholder ids.

References can target the same table, and the generated GraphQL schema supports cycles between tables. automatic_reference is not a public writable column type.

POST: inspect the generated GraphQL schema

The exact query and mutation names come from the generated schema. Use introspection after creating or changing schema, especially when an integration creates its own table gtype values.

POST /api/v1/graphql/
x-api-key: YOUR_API_KEY
x-db-id: DB_ID
Content-Type: application/json

{
  "query": "{ __schema { mutationType { fields { name } } queryType { fields { name } } } }"
}

POST: add row data with GraphQL

Create target rows first, then store the referenced row id in the reference field. A one_to_one reference receives one row id. A one_to_many reference receives an array of row ids.

A GraphQL insert uses the same row-inserted automation path as Forms API and UI-created rows. If the table has a public row_inserted automation, its task or email actions run after the row is stored.

POST /api/v1/graphql/
x-api-key: YOUR_API_KEY
x-db-id: DB_ID
Content-Type: application/json

{
  "query": "mutation CreateCompany($companyName: String, $website: String, $customerDocs: [String]) { Companies(companyName: $companyName, website: $website, customerDocs: $customerDocs) { id companyName website customerDocs } }",
  "variables": {
    "companyName": "Acme Ltd",
    "website": "https://example.com",
    "customerDocs": ["DOC_ID_1", "DOC_ID_2"]
  }
}

Then create a deal and pass the company row id to the reference column:

POST /api/v1/graphql/
x-api-key: YOUR_API_KEY
x-db-id: DB_ID
Content-Type: application/json

{
  "query": "mutation CreateDeal($dealName: String, $stage: String, $company: String) { Sheet1(dealName: $dealName, stage: $stage, company: $company) { id dealName stage company { id companyName website } } }",
  "variables": {
    "dealName": "Website redesign",
    "stage": "new",
    "company": "COMPANY_ROW_ID"
  }
}

Example response:

{
  "data": {
    "Sheet1": {
      "id": "DEAL_ROW_ID",
      "dealName": "Website redesign",
      "stage": "new",
      "company": {
        "id": "COMPANY_ROW_ID",
        "companyName": "Acme Ltd",
        "website": "https://example.com",
        "customerDocs": ["DOC_ID_1", "DOC_ID_2"]
      }
    }
  }
}

GET: read row data with GraphQL

GraphQL reads are sent as HTTP POST requests to /api/v1/graphql/. This is normal GraphQL behavior: the operation is a read, even though the HTTP method is POST.

POST /api/v1/graphql/
x-api-key: YOUR_API_KEY
x-db-id: DB_ID
Content-Type: application/json

{
  "query": "query { Sheet1Rows { id dealName stage company { id companyName website } } }"
}

POST: update row data with GraphQL

GraphQL updates are also sent to /api/v1/graphql/ with HTTP POST. The mutation name depends on the generated schema, so use introspection to confirm the exact name and input type.

POST /api/v1/graphql/
x-api-key: YOUR_API_KEY
x-db-id: DB_ID
Content-Type: application/json

{
  "query": "mutation UpdateDeal($id: ID!, $dealName: String, $stage: String, $company: String) { updateSheet1(id: $id, dealName: $dealName, stage: $stage, company: $company) { id dealName stage company { id companyName } } }",
  "variables": {
    "id": "DEAL_ROW_ID",
    "dealName": "Website redesign",
    "stage": "won",
    "company": "COMPANY_ROW_ID"
  }
}

GET: read database activity

Database activity records describe cell-level row changes. They are useful for row detail timelines, audit views, CRM history, and integrations that need to react to field changes after data has been written through the UI or API.

GET /api/v1/databases/DB_ID/activity?table_id=DEALS_TABLE_ID&row_id=DEAL_ROW_ID&limit=20
x-api-key: YOUR_API_KEY

Example response:

{
  "activity": [
    {
      "id": "ACTIVITY_ID",
      "dbid": "DB_ID",
      "tableid": "DEALS_TABLE_ID",
      "rowid": "DEAL_ROW_ID",
      "dataid": "CELL_DATA_ID",
      "colid": "stage",
      "op": "update",
      "oldvalue": "new",
      "newvalue": "won",
      "userId": "USER_ID",
      "created": 1780000000000,
      "render": {
        "field": { "id": "stage", "name": "Stage", "type": "status" },
        "old_display": "New",
        "new_display": "Won",
        "summary": "Stage changed from New to Won"
      }
    }
  ]
}

op is insert, update or delete. oldvalue and newvalue keep the stored value shape for the column: strings for text/status/reference values, arrays for list-like columns, and objects for structured columns such as timeline and file. The optional render object is a convenience summary for display.

Database Automations API

The Automations API manages workflow definitions attached to a database. It supports listing and creating automations under a database, then reading, updating, or deleting an individual automation by id.

GET    /api/v1/databases/DB_ID/automations
POST   /api/v1/databases/DB_ID/automations
GET    /api/v1/automations/AUTOMATION_ID
PATCH  /api/v1/automations/AUTOMATION_ID
DELETE /api/v1/automations/AUTOMATION_ID

These endpoints use x-api-key. They resolve the project from the database, so they do not need x-project-id. Use Automations read permission for list/get and Automations write permission for create/update/delete.

POST: create an automation

POST /api/v1/databases/DB_ID/automations
x-api-key: YOUR_API_KEY
Content-Type: application/json

{
  "name": "Create follow-up task",
  "status": "public",
  "trigger": {
    "type": "row_inserted",
    "tableId": "TABLE_ID"
  },
  "actions": [
    {
      "type": "create_task",
      "pid": "PROJECT_ID",
      "users": ["USER_ID"],
      "dbFieldTitle": "TITLE_COLUMN_ID",
      "dbFieldBody": "BODY_COLUMN_ID"
    }
  ]
}

Supported trigger types are row_inserted and row_updated. A row-updated trigger also specifies columnId and the target value:

{
  "name": "Start delivery when deal is won",
  "status": "public",
  "trigger": {
    "type": "row_updated",
    "tableId": "DEALS_TABLE_ID",
    "columnId": "STAGE_COLUMN_ID",
    "value": "won"
  },
  "actions": [
    {
      "type": "create_task",
      "pid": "DELIVERY_PROJECT_ID",
      "dbFieldTitle": "DEAL_NAME_COLUMN_ID"
    }
  ]
}

A row-updated trigger runs when the selected field changes to the configured value. Actions include create_task, send_email, create_row, update_row, subscribe_to_newsletter and unsubscribe_from_newsletter. Each action requires the appropriate destination permissions; see OpenAPI for its configuration.

GET and PATCH an automation

GET /api/v1/automations/AUTOMATION_ID
x-api-key: YOUR_API_KEY

PATCH /api/v1/automations/AUTOMATION_ID
x-api-key: YOUR_API_KEY
Content-Type: application/json

{
  "status": "draft",
  "name": "Paused follow-up task"
}

A partial PATCH keeps fields that are not included. Switching the status to draft pauses execution without deleting the definition. DELETE removes the automation from API and UI listings.

Forms, GraphQL, automations, and Tasks together

These APIs can form one end-to-end workflow:

  1. Create a database and columns with the Database REST API.

  2. Create a public automation with Automations write scope.

  3. Submit a form through /api/v1/forms/{id}/submissions or insert a row through GraphQL.

  4. The row is stored in the database and the matching automation runs.

  5. A task is created in its configured project or an email action runs.

Using Databases and Docs together

Docs and Databases work well together. A database can hold structured state, while Docs can hold rich long-form context. The bridge between the two is the document column type: its value is the id of a Docs document.

GET: fetch a linked Doc after querying a row

GET /api/v1/docs/DOC_ID
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID

Example response:

{
  "doc": {
    "id": "DOC_ID",
    "title": "Interview notes: Ada Lovelace",
    "html": "<h1>Interview notes</h1><p>Ada liked the onboarding flow.</p>"
  }
}

POST: create a Doc and store its id in a row

First create the document:

POST /api/v1/docs/
x-api-key: YOUR_API_KEY
x-project-id: YOUR_PROJECT_ID
Content-Type: application/json

{
  "title": "Interview notes: Ada Lovelace",
  "html": "<h1>Interview notes</h1><p>Ada liked the onboarding flow and asked for more examples.</p>"
}

Example response:

{
  "doc": {
    "id": "DOC_ID",
    "title": "Interview notes: Ada Lovelace"
  }
}

Then use that DOC_ID in a database row whose column type is document:

POST /api/v1/graphql/
x-api-key: YOUR_API_KEY
x-db-id: DB_ID
Content-Type: application/json

{
  "query": "mutation CreateFeedback($input: FeedbackInput!) { createFeedback(input: $input) { id customer sourceDocument } }",
  "variables": {
    "input": {
      "customer": "Ada Lovelace",
      "sourceDocument": "DOC_ID"
    }
  }
}

Example response:

{
  "data": {
    "createFeedback": {
      "id": "ROW_ID",
      "customer": "Ada Lovelace",
      "sourceDocument": "DOC_ID"
    }
  }
}

Legacy direct table submit

Older generated database form snippets can submit directly to a table endpoint. This creates database rows, but it is best treated as a legacy/browser form integration.

GET: no row listing endpoint here

This legacy submit path does not list rows. Use GraphQL or GET /api/v1/databases/{dbId}/tables/{tableId}/rows instead.

POST /api/v1/graphql/
x-api-key: YOUR_API_KEY
x-db-id: DB_ID
Content-Type: application/json

{
  "query": "query { feedbackRows { id customer message } }"
}

Example response:

{
  "data": {
    "feedbackRows": [
      { "id": "ROW_ID", "customer": "Ada Lovelace", "message": "The onboarding flow was clear." }
    ]
  }
}

POST: legacy table submit

POST /api/v1/db/TABLE_ID
Content-Type: application/x-www-form-urlencoded

customer=Ada%20Lovelace&message=The%20onboarding%20flow%20was%20clear.

Example response:

{
  "success": true,
  "id": "ROW_ID"
}

New integrations should use the authenticated Forms API for Form submissions and GraphQL or REST row endpoints for direct database operations.

Update support summary

Chat supports room creation, participant management, history, unread/read state, own-message editing and soft deletion, and private-room webhooks. Posts supports publication, threaded replies and author-only post/comment changes. Docs and Meetings support threaded discussion comments and author-only comment changes in addition to their existing item updates.

Projects supports team-admin creation, metadata updates, archive/restore and membership management. Files supports upload, metadata, bounded listing and authenticated downloads. Workspace Search brings readable content together using existing feature scopes.

Tasks, Docs, Meetings, Forms, Newsletters, issues, subscribers and Database resources retain their REST update operations; database rows also support GraphQL mutations. Consult each OpenAPI operation for permissions, pagination, retries and preconditions rather than assuming every feature shares the same mutation contract.

A practical starting point

Create a Team API key with the project and feature scopes your integration needs. Read the host’s OpenAPI description and start with one workflow. See Database automations with Aamu.app for an example of turning row events into follow-up work.

How the API operation surface guides Aamu's internal AI

The public API remains the integration surface for external systems, API keys, and project-scoped automation. Aamu's internal AI can now work with the same described operation set when a user asks it to act inside the workspace.

The OpenAPI operation names and request schemas help the AI select an operation and prepare its path, query, and body arguments. The internal action does not need an API key and does not have to call Aamu over HTTP: it reuses the existing server-side handlers and product functions with the signed-in user's session and accessible projects.

This keeps the public API and internal AI capabilities aligned across areas such as Forms, databases, GraphQL rows, database automations, files, users, Helpdesk, Email, Newsletters, Docs, Tasks, and Meetings. External integrations still use API key scopes; internal AI follows the current user's workspace permissions. Higher-risk actions retain explicit confirmation boundaries.

Related articles