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
| Field | Required |
|---|---|
to | yes |
subject | yes |
text | yes |
html | no |
project_id | no |
Notes on the two optional-looking fields:
html— plain string, sent as-is; omit it and onlytextrenders.project_id— required 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
| Status | Body error | Cause |
|---|---|---|
| 401 | missing or malformed API key | No Authorization header, or it isn't Bearer <key>. |
| 401 | invalid API key | The key doesn't match any stored hash. |
| 401 | API key has been revoked | revoked_at is set. |
| 403 | API key has no owner; create a new one | The key's creator account no longer exists. |
| 403 | API key is not authorised for this project | The key's scope doesn't cover project_id. |
| 400 | to must be a valid email address | to missing or not address-shaped. |
| 400 | subject is required | Empty or missing. |
| 400 | text is required | Empty or missing. |
| 400 | project_id is required for this key | Org/scoped key with no project_id and no key-bound project. |
| 404 | the project this key belongs to no longer exists | Project was deleted after the key was minted. |
| 409 | sending is not configured for this project | The project has no send identity yet. |
| 429 | send quota exceeded (...) | Org's daily or monthly cap reached — see Limits below. remaining: 0 in the body. |
| 500 | send failed | Key lookup or send-identity resolution failed unexpectedly. |
| 500 | quota check failed | The quota RPC itself errored. |
| 502 | send failed | The 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).