# Kontacts > Project-owned email. Every project gets a real inbox. Publish a meeting type for a booking page. Send and read mail with an API key. Hosted at https://kontacts.dev. Kontacts is email, a booking page, a form, and a kanban board — owned by the **project**, not by whoever is on the team today. Mail to any address at a verified domain lands in one shared inbox. Agents use the same inbox over REST, signed webhooks, or hosted MCP. This file is for language models (and the people prompting them) that are about to add a project to Kontacts. Human docs: https://docs.kontacts.dev, https://kontacts.dev/help, https://kontacts.dev/email-for-ai-agents, https://kontacts.dev/mailbox. ## How to add a project 1. Sign up at https://kontacts.dev/signup (magic link or GitHub). That creates an organisation (a workspace). 2. Create a project. - **Dashboard:** Add project. You can pick a GitHub repository here, or skip GitHub and add one later under Settings → Integrations → GitHub. - **API:** `POST /api/v1/projects` with JSON `{ "name": "Acme" }` (optional `slug`, slugified from `name` when omitted). Success is 201. The key must hold `projects: write` **and** reach all projects in the workspace — a key pinned to a specific project list gets 403, because it has no say over a project that does not exist yet. `GET /api/v1/projects` needs `projects: read` and lists the projects the key's reach resolves to. 3. Every project gets a real email address at create time, and exactly one kanban board (private until you publish it — see Kanban). Receiving on **your** domain still needs DNS: Settings → Domains, then point MX, SPF, and DKIM at Kontacts. Mail to any address at that domain lands in the project inbox. A contact form does not need a domain — create one under Settings → Forms and enable it when the fields are right (a new form starts as a closed draft). 4. Mint an API key under Settings → API keys (`/dashboard/settings/api-keys`). Pick its project reach and a permission level per tool (see API keys). The plaintext is shown once. Prefix `gm_live_`. Put it on `Authorization: Bearer …`. 5. Optional: publish a meeting type (booking), create a form, publish the kanban board, turn on GitHub issue routing, register a webhook. Project slugs are unique across all of kontacts.dev, not just within your organisation, so `POST /api/v1/projects` answers 409 `slug "…" is already taken` when another workspace already holds it. A slug that shadows a top-level route (`api`, `dashboard`, `login`, …) is refused with 400 `that slug is reserved`. The public v1 create path does not attach a GitHub repo. Linking a repository is the dashboard / GitHub App install flow. ## API keys There is one key format and one settings page. A key is `gm_live_` followed by 43 base64url characters; it is opaque, looked up by a sha256 hash, and never decoded. Only the hash is stored — the plaintext is returned exactly once, at mint time. The same key authenticates REST and MCP; there is no separate MCP credential and no session-cookie path on `/api/v1/*`. A key belongs to one organisation and carries two independent axes: - **Project reach** — either every project in the workspace (including projects created after the key was minted), or exactly the list of projects picked when it was minted. There is nothing in between. - **Permissions** — a level per product surface. Tools: `inbox`, `booking`, `forms`, `newsletters`, `kanban`, `contacts`, `domains`, `projects`, `webhooks`. Levels are ordered `none` < `read` < `write` < `admin`, and each implies the ones below it, so a check is a rank comparison and an `admin` key satisfies a `read` check. A tool absent from the key's permissions is `none`. Naming a project on a request: a key scoped to **exactly one** project has that project as its default and may omit `project_id`. A key reaching all projects, or several, has no default and must name `project_id` on every request that acts on one. Naming a project outside the key's reach is 403 `API key is not authorised for this project`, whatever the permissions say; a missing permission is 403 `this key lacks :`. Keys are revocable (a revoked key is 401 `API key has been revoked`), have no expiry, and have no rotation endpoint — mint a new key and revoke the old one. Keys minted before September 2026 no longer work at all: the change that unified the key model deleted every existing key, so an older token answers 401 `invalid API key`. ## Inbox Point a domain — or a subdomain like `mail.yourproject.dev` — at Kontacts. Mail to any address at the domain is received, stored, and shown in the inbox. Declaring local-parts does **not** gate receiving; it gates whether an **issue** opens (next section). Reply from the shared inbox — the dashboard ships a composer, and agents reply over MCP `reply_email`. Archive and spam paths exist. This is project communication infrastructure, not a full mail client. A free address on kontacts.dev for receiving is planned, not built. Until it ships, receiving means pointing a domain you control. ## GitHub issue routing GitHub issue routing is optional and off by default. When you turn it on, mail to the addresses on your list opens a GitHub issue. Mail that fails sender authentication is held in the inbox and marked quarantined instead. The optional catch-all counts every address as declared for that gate — it does not decide whether mail is received. Turn it on under Settings → Integrations → GitHub after you link a repository. Kontacts asks GitHub for read access to your email address and repository metadata, and read and write access to issues — mail to the addresses on your list opens an issue in the repository you connected. It never reads your code or your commit history, and it never pushes commits. When the linked repository is public: Emails sent to this project's address will be published as public GitHub issues — including the sender's address and the full message body. Anyone can read them. Form submissions do not open a GitHub issue on their own: the address on the form is self-reported, so a submission sits labelled Quarantined until you release it. Private repos work. The App only sees the repositories you pick at install time. ## Booking A booking page is not live at project creation. Publish a meeting type under Settings → Calendar settings — or over MCP with `create_meeting_type` then `set_meeting_type_active` (see the MCP section); until then the public `/book/{project}/{type}` URL 404s. The page belongs to the project, not to a person. Set team hours once. An optional Google Calendar connect can hide times a member is already busy; it does not replace those project hours. Optional Zoom for the conference link. This is a booking page, not a full calendar. ## Kanban Every project has exactly one board, created with the project, and it is **private until you publish it**. Publishing puts the roadmap at `/p/{project}/kanban` — no login, no key in the URL. That is the only public board URL; the older `/k/{public_key}` links answer 308 to the pretty path (and still 404 while a board is unpublished, so taking a board down does not disclose its project slug). By default, anyone with the public link can add a card to the intake column ("Idea"); moving and editing stay with project admins. Deleting a card is a dashboard action — the public board never offers it. The owner can turn public create off — that is a second, separate decision from publishing — see the MCP tools below. A visitor who is not signed in must provide a name and a valid email address with the card: the name becomes the card's public creator label, and the email is stored on the card but never exposed by the public read API. A signed-in creator keeps their session identity — guest-typed fields never override it — and their address is auto-captured into the project's contact book (see Contacts). ## Forms Every project can mint a public form endpoint under Settings → Forms (`/dashboard/settings/forms`). Visitors POST JSON, urlencoded, or multipart to `/api/v1/f/{public_key}`. The key is meant to live in page source — it is not a secret. Success is 201. Live URL shape: `https://kontacts.dev/api/v1/f/{public_key}`. Hosted page: `/f/{public_key}` or pretty `/p/{project}/f/{form}`. Embed in an iframe; add Allowed origins if you POST from your own HTML. Recommended fields: `name`, `email`, `message` (optional `subject`). Extra keys are kept. Optional honeypots `_gotcha` / `_hp`. Rate limit is 10 submissions per IP per hour. Submissions land in the project inbox, and a submission carrying a valid email address also lands the sender in the project's contact book (see Contacts). ## Contacts Every project has a contact book (Dashboard → Contacts): one row per (project, email), captured automatically server-side. Two capture paths feed it: a form submission whose email validated (an email-typed field in the form schema, or the email/from/sender key heuristic on legacy schema-less endpoints), and a signed-in creator of a public kanban card (a guest-typed card email stays on the card row and is not captured). A project admin can also create a contact by hand or import a CSV from the same page. First capture wins: a repeat capture never overwrites a row the owner may have edited. A contact's name is never derived from the email local part, and gender is never inferred. Agents get full CRUD over the book through the hosted MCP contact tools (see Hosted MCP below); there is no REST endpoint and no export today — outside MCP, the dashboard is the whole surface. ## For agents — one key, every surface Mint the key yourself at `/dashboard/settings/api-keys`. Same Bearer token on send, list, projects, and MCP; what a given key may do is its reach and its permissions, above. There is no dashboard scraping to do. There is no published SDK — the contract is HTTP (https://kontacts.dev/email-for-ai-agents). There is no IMAP (https://kontacts.dev/mailbox). ### REST send `/api/v1/send` — https://kontacts.dev/api/v1/send JSON `{ to, subject, text, html?, project_id? }`. There is no `from` field — the project identity decides. Needs `inbox: write`. `project_id` is required unless the key is scoped to exactly one project, in which case that project is assumed. Success is 201. A custom-domain From requires a verified domain with a declared address; otherwise send uses the platform identity. Do not assume the caller's own domain. A reply in an existing thread goes through the inbox or MCP `reply_email`, not this endpoint. ``` curl -X POST https://kontacts.dev/api/v1/send \ -H "Authorization: Bearer gm_live_…" \ -H "Content-Type: application/json" \ -d '{"to":"teammate@example.com","subject":"Hello","text":"Sent from the API"}' ``` ### REST list `/api/v1/emails` — https://kontacts.dev/api/v1/emails Needs `inbox: read`. With no `project_id`, it lists mail across every project the key reaches; pass `?project_id=` to narrow to one. This is a list, not a single-message GET — read one message with MCP `get_email`. ``` curl https://kontacts.dev/api/v1/emails \ -H "Authorization: Bearer gm_live_…" ``` ### REST projects `/api/v1/projects` — https://kontacts.dev/api/v1/projects - `GET` — projects the key reaches. Needs `projects: read`. - `POST` — create a project. Needs `projects: write` and a key that reaches all projects in the workspace. Body `{ name, slug? }`. ### Hosted MCP `/api/v1/mcp` — https://kontacts.dev/api/v1/mcp POST JSON-RPC, protocol `2025-03-26`. Read tools: `list_projects`, `key_info`, `list_emails`, `get_email`, `list_domains`, `list_bookings`, `list_meeting_types`, `list_contacts`, `get_board`, `list_cards`, `list_comments`, `list_audiences`, `list_subscribers`, `list_issues`, `get_issue`, `get_issue_stats`, `preview_issue`, `list_forms`, `get_form`, `list_submissions`, `get_submission`. Write tools: `send_email`, `reply_email`, `add_domain`, `verify_domain`, `create_meeting_type`, `set_meeting_type_active`, `set_availability`, `create_contact`, `update_contact`, `delete_contact`, `publish_kanban`, `set_kanban_public_editing`, `update_board`, `add_column`, `rename_column`, `move_column`, `delete_column`, `create_card`, `update_card`, `move_card`, `delete_card`, `delete_comment`, `create_audience`, `set_audience_enabled`, `create_draft`, `update_draft`, `send_issue`, `create_form`, `update_form`, `set_form_fields`, `set_form_enabled`, `set_form_origins`, `archive_form`, `duplicate_form`, `release_submission`. Each tool checks a permission on the key before it runs — `inbox` for mail, `domains`, `booking`, `kanban` for the board tools, and `contacts` for the contact-book tools — and every tool's description ends by naming the grant it needs (`Requires booking:admin.`), so a task can be planned against the key before the first write. `tools/list` advertises every tool whatever the key holds; the gate runs on `tools/call`, and a denial comes back as a **successful** JSON-RPC result with `isError: true` naming the missing grant, not as an HTTP error. Check `isError`, not the status code. Project-scoped tools declare `project_id` in their `inputSchema`, so strictly-validating clients can send it. Start with the discovery pair: `list_projects` returns the projects the key reaches (id, name, slug), and `key_info` describes the key itself — grants, scope, name and key prefix — and needs no grant at all. `project_id` accepts a project's UUID **or slug** on every project-scoped tool, and may be omitted only when the key is scoped to exactly one project; every tool except `list_emails` otherwise fails asking for it and names `list_projects` as the exit. `list_emails` is the exception: with no `project_id` it spans every project the key reaches. `send_email` re-enters the same stack as `/api/v1/send` — same validation, same quota, same "no `from` field" rule. `reply_email` uses the inbox reply path and is two-phase: one call previews, a second with `confirm: true` sends. `publish_kanban` uses that same two-phase shape and returns the resulting public URL, so you never build the link yourself; taking a board back down needs no `confirm`. `set_kanban_public_editing` is deliberately a separate tool — "anyone can read my roadmap" and "anyone on the internet can write to it" are different decisions. Cards created through that door land in the intake column, and a creator who is not signed in must send a name and a valid email (see Kanban). `create_meeting_type` and `set_meeting_type_active` apply the same split to the booking page: the create is a private draft (`is_active` keeps its false default, so nothing goes public), and activation is the two-phase, confirm-gated act that returns the public `/book/…` URL — deactivating needs no `confirm` and is the only remedy, since meeting types have no delete. Availability lives in `set_availability` — weekly windows in minutes since local midnight plus the IANA timezone they are read in; it replaces the whole set, so read `list_meeting_types` first (that read also returns each type's current windows). Publishing a type with zero windows is refused, naming `set_availability` as the exit, unless you pass `allow_empty: true` — a page that loads and offers no slots is almost never what anyone meant. `add_domain` applies the same two-phase, confirm-gated shape to domain setup — success means a row exists, not a working mailbox: publish the returned `dns_records` at the DNS host, then poll `verify_domain` (a write, but safe to poll) until it reports `verified`. There is no `delete_domain`; removing a domain stays a dashboard action. The contact tools are CRUD over the project's contact book — the same rows the dashboard's Contacts view shows and auto-capture feeds (see Contacts). `list_contacts` returns full rows (there is no `get_contact`). One contact per email per project: `create_contact` reports a duplicate rather than overwriting, `update_contact` patches only the fields you name and merges `options` keys over the stored bag, and `delete_contact` is two-phase like `reply_email` — one call previews the row, a second with `confirm: true` permanently deletes it. Nothing infers `gender`; only set what you were told. Rate limit is 60 requests per minute per key, counted in-process per instance, so treat it as a floor rather than an exact global cap. Cursor: ``` { "mcpServers": { "kontacts": { "url": "https://kontacts.dev/api/v1/mcp", "headers": { "Authorization": "Bearer gm_live_…" } } } } ``` Claude: ``` claude mcp add --transport http kontacts https://kontacts.dev/api/v1/mcp \ --header "Authorization: Bearer gm_live_…" ``` ### Signed webhooks Register an HTTPS URL under Settings → Webhooks (`/dashboard/settings/webhooks`). Events: `email.received`, `email.sent`, `email.bounced`, `form.submitted`, `booking.created`, `booking.cancelled`, `webhook.test`. Verify `X-Kontacts-Signature` as HMAC-SHA256 hex of `${timestamp}.${rawBody}`. Mail payloads are envelope metadata — the inbox holds the body. `form.submitted` includes the sanitized field map. Answer 2xx. Contract: https://kontacts.dev/help#webhooks. ## Also shipped - Newsletters: Newsletters in the dashboard Overview (`/dashboard/newsletters`). Public subscribe is `POST /api/v1/subscribe/{public_key}` — a `{public_key}` in the URL, not an `Authorization` header; no API key involved. Full reference: https://docs.kontacts.dev/docs/newsletters. Every sent issue carries `List-Unsubscribe` / `List-Unsubscribe-Post: List-Unsubscribe=One-Click` headers, so a compliant mail client offers one-click unsubscribe and we handle its POST. The URL those headers carry is keyed on a per-recipient token from the delivered message, so there is no endpoint that unsubscribes an address you name; removing someone otherwise is a dashboard action. - Email forwarding overlay: Settings → Integrations → Email forwarding. Still a project inbox, not an alias service. - Settings → General per project: rename, change slug, delete. Changing a slug breaks the public links that carry it — there is no redirect from the old one. ## Plans Billed per organisation, not per seat. Signing up does not enrol you in a paid plan. Current numbers: - Free: 2 projects, 1 member, 10 sends per organisation per UTC day. - Indie Hacker: 10 projects, 3 members, 200 sends/day and 1000/month, $8/month. - Enterprise: custom. Free and Indie send caps apply to dashboard compose, `/api/v1/send`, and MCP `send_email` / `reply_email` — one organisation pool. No uptime guarantee and no SLA (https://kontacts.dev/terms). ## What Kontacts is not - Not a full mail client. There is no IMAP today (https://kontacts.dev/mailbox), so you cannot point Thunderbird or Apple Mail at it. - Not a published SDK (https://kontacts.dev/email-for-ai-agents). Use curl, fetch, or MCP. - Not a personal calendar. The Calendar shows the project's confirmed bookings, plus cached teammate busy time if someone connected a Google Calendar — nothing else. Most bookings arrive from a booking page you publish; a project admin can also add an event from the calendar toolbar, which mails the guest a confirmation with a calendar invite. There is no reschedule and no delete — cancelling is the only way a booking leaves the grid, and it tells the guest. ## Docs - API reference (machine and human): https://docs.kontacts.dev - How it works (human): https://kontacts.dev/#product - Mailbox: https://kontacts.dev/mailbox - Booking page: https://kontacts.dev/booking-page - Calendar: https://kontacts.dev/calendar - Email for AI agents: https://kontacts.dev/email-for-ai-agents - Help (send API, forms, webhooks, MCP): https://kontacts.dev/help - Pricing: https://kontacts.dev/pricing - Privacy: https://kontacts.dev/privacy - Terms: https://kontacts.dev/terms ## Support support@kontacts.dev — that address runs on Kontacts itself. Do not send passwords, API keys, or GitHub tokens.