Kontacts docs

Send an email

POST /api/v1/send — the one way to send outbound mail through the API.

POST /api/v1/send

Sends one email as the project's identity. This is the only send endpoint — there is no scheduled-send, no idempotency key, no sandbox/test mode, and no bulk or batch variant. One call, one message.

Auth

Authorization: Bearer <key> — see Authentication. A project key always sends as its own project. An org/scoped key must also send project_id in the body, and must be authorized for that project.

Request body

FieldRequired
toyes
subjectyes
textyes
htmlno
project_idno

Notes on the two optional-looking fields:

  • html — plain string, sent as-is; omit it and only text renders.
  • project_idrequired for org/scoped keys, and silently ignored for project keys (a project key always sends as its own project, so passing this is harmless).

There is no `from` field

The project's own identity decides the From address. This is intentional — a from field would let one project send as another's address — and it means a request that includes one is simply ignored, not rejected.

Response

201 Created on success:

{
  "id": "email-uuid",
  "from": "you@yourproject.example",
  "to": "teammate@example.com",
  "providerId": "resend-message-id",
  "persisted": true,
  "remaining": 9
}

id can be null even on a 201. The message still sent — persisted: false means the audit-log row failed to write after a successful provider call. That failure is logged server-side and never turned into an error response, so a client retry logic keyed only on HTTP status will not double-send. remaining is what's left in the org's current quota window after this send.

Errors

StatusBody errorCause
401missing or malformed API keyNo Authorization header, or it isn't Bearer <key>.
401invalid API keyThe key doesn't match any stored hash.
401API key has been revokedrevoked_at is set.
403API key has no owner; create a new oneThe key's creator account no longer exists.
403API key is not authorised for this projectThe key's scope doesn't cover project_id.
400to must be a valid email addressto missing or not address-shaped.
400subject is requiredEmpty or missing.
400text is requiredEmpty or missing.
400project_id is required for this keyOrg/scoped key with no project_id and no key-bound project.
404the project this key belongs to no longer existsProject was deleted after the key was minted.
409sending is not configured for this projectThe project has no send identity yet.
429send quota exceeded (...)Org's daily or monthly cap reached — see Limits below. remaining: 0 in the body.
500send failedKey lookup or send-identity resolution failed unexpectedly.
500quota check failedThe quota RPC itself errored.
502send failedThe mail provider rejected or could not be reached.

Error strings are exact, read from apps/web/app/api/v1/send/route.ts at the time this page was written — if you're matching on message text rather than status code, treat it as a snapshot.

curl

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"}'

Limits

Sending draws from the organisation's plan quota — the same pool the dashboard compose window spends, and the same pool the MCP send_email and reply_email tools spend. Free is capped at 10 sends per organisation per UTC day, shared across every project in the org, not 10 per project. Indie Hacker raises that to 200/day and 1,000/month (apps/web/lib/plans.ts).

The credit is spent before the call to the mail provider, and refunded only when the message demonstrably never left (a provider-side error, not a successful send that failed to record) — see isRefundableSendError in apps/web/lib/outbound.ts if you need the exact boundary.

Keys are minted under Settings → API keys (/dashboard/settings/api-keys).

On this page