# MailKaka API Documentation

MailKaka is a familiar transactional email API, plus custom-domain team mailboxes, built on India (Mumbai-region) infrastructure.

## Base URL

```
https://www.mailkaka.in/api
```

Self-hosted deployments: replace with `https://<your-deployment-host>/api`.

Use the `www.mailkaka.in` host shown above for MailKaka Cloud examples. If you copied an older `https://mailkaka.in/api` example, update it to `https://www.mailkaka.in/api`.

## Authentication

Every request needs an API key created from the MailKaka dashboard, sent as a Bearer token:

```
Authorization: Bearer mk_xxxxxxxx...
Content-Type: application/json
```

Keys are scoped to one sending domain and a set of permissions (e.g. `send`). A key that fails to parse or doesn't match a stored key returns `401`.

## Validate a key (no email sent)

```bash
curl 'https://www.mailkaka.in/api/api-keys/validate' \
  -H 'Authorization: Bearer mk_xxxxxxxx...'
```

```json
{
  "valid": true,
  "key": { "name": "Production", "permissions": ["send"] },
  "domain": { "domain": "yourcompany.in", "status": "verified" },
  "organization": { "name": "Your Company" }
}
```

Any key that authenticates reports `valid: true` even if it lacks `send` permission or its domain isn't verified -- those are returned as data so you can see what's wrong, not as a reason this check itself fails. A bad/missing key returns `401` with `{ "valid": false, "error": "..." }`.

## Send an email

```
POST https://www.mailkaka.in/api/emails
Authorization: Bearer mk_xxxxxxxx...
Content-Type: application/json
```

### cURL

```bash
curl -X POST 'https://www.mailkaka.in/api/emails' \
  -H 'Authorization: Bearer mk_xxxxxxxx...' \
  -H 'Content-Type: application/json' \
  -d '{
    "from": "hello@yourcompany.in",
    "to": ["customer@example.com"],
    "subject": "Your order is confirmed",
    "html": "<strong>Shipped from India.</strong>"
  }'
```

### Node.js

```js
await fetch('https://www.mailkaka.in/api/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MAILKAKA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'hello@yourcompany.in',
    to: ['customer@example.com'],
    subject: 'Your order is confirmed',
    html: '<strong>Shipped from India.</strong>',
  }),
});
```

### Request fields

| Field | Type | Notes |
|---|---|---|
| `from` | string, required | Accepts `email@domain.com` or `"Name <email@domain.com>"`. The inner email domain must match the verified sending domain on the key. |
| `to` | string or string[], required | One or more recipients. A message supports at most 50 total recipients across `to`, `cc`, and `bcc`. |
| `cc`, `bcc` | string or string[], optional | Same shape as `to`; included in the 50-recipient message limit. |
| `subject` | string, required | |
| `html`, `text` | string, optional | At least one of the two is required. |
| `attachments` | array, optional | `{ filename, content (base64), contentType }`. Combined base64 content is capped at 8MB; executable extensions (`.exe`, `.js`, `.msi`, etc.) are rejected. |
| `reply_to` | string or string[], optional | |
| `tags` | object or array, optional | Flat string map, or Resend-style `[{ name, value }]`. |
| `unsubscribe` | boolean, optional, default `false` | Adds a one-click List-Unsubscribe header. Requires exactly one `to` recipient -- reject with `400` otherwise. Opt-in only; never set this on OTP/receipt-style transactional mail. |

Send an `Idempotency-Key` header to make retries safe: a repeated key returns the original result instead of sending again.

## Send a batch

`POST https://www.mailkaka.in/api/emails/batch` accepts an array of 1-100 messages. Authentication and domain scope are shared by the request, while each message can have different recipients, content, and sender display name.

```js
const response = await fetch('https://www.mailkaka.in/api/emails/batch', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MAILKAKA_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-notifications-2026-08-03',
  },
  body: JSON.stringify([
    {
      from: 'Acme <hello@yourcompany.in>',
      to: ['first@example.com'],
      subject: 'Your first update',
      html: '<strong>First message</strong>',
    },
    {
      from: 'Acme <hello@yourcompany.in>',
      to: ['second@example.com'],
      subject: 'Your second update',
      html: '<strong>Second message</strong>',
    },
  ]),
});
```

Successful response:

```json
{ "data": [{ "id": "..." }, { "id": "..." }] }
```

The entire payload is validated before queueing. Quota reservation and inserts are atomic, so an invalid item, exhausted quota, or database failure does not leave a partially queued batch. Suppressed recipients are filtered per message. Pass one `Idempotency-Key` for safe whole-batch retries.

### Migrating from Resend

MailKaka is built as India-first email infrastructure, not a Resend clone. If you are moving an existing Resend integration over, the single-message send shape at `POST /emails` is intentionally familiar, including array-shaped `to`, `cc`, and `bcc`. A few Resend-specific features need explicit changes:

- Display-name senders such as `from: "Name <email@domain.com>"` are supported.
- `resend.batch.send(...)` can use MailKaka's `POST /emails/batch`, capped at 100 messages per request.
- Open and click tracking events are not emitted.

## Logs

Delivery and request logs are visible in the dashboard. An authenticated logs API is also available:

```
GET https://www.mailkaka.in/api/emails/logs
Authorization: Bearer mk_xxxxxxxx...
```

Logs are scoped to the API key's organization and can be filtered by query params such as `days`, `status`, `domain_id`, `api_key_id`, and `search`. Log visibility requires a valid MailKaka API key; there is no public unauthenticated logs endpoint.

## Webhooks

Configure an endpoint URL and event subscriptions from the dashboard. MailKaka posts:

- `sent`
- `delivered`
- `bounced`
- `complained`

There is **no** `opened`/`clicked` open/click tracking, and **no** inbound-email webhook -- inbound mail is delivered into MailKaka's own hosted team mailbox rather than forwarded to your endpoint. If you need programmatic access to inbound mail today, that's a known gap in the current API; there is no workaround yet.

Each delivery is signed:

```
X-Mailkaka-Signature: <hex HMAC-SHA256 of the raw request body, keyed with your endpoint's whsec_ secret>
```

Verify by recomputing the HMAC over the raw body with your stored secret and comparing.

## Errors

| Status | Meaning |
|---|---|
| `400` | Request validation failed (e.g. missing `html`/`text`, invalid email, `from` domain not matching the key's domain, or a batch outside the 1-100 range). MailKaka uses `400` for this, not `422`. |
| `401` | Missing or invalid `Authorization` header / API key. |
| `403` | Key lacks `send` permission, the sending domain isn't verified, or the organization is suspended. |
| `409` | A batch `Idempotency-Key` was reused with a different payload. |
| `429` | Monthly send quota exhausted for the plan. |

Example:

```json
{ "error": "From email must be from domain: yourcompany.in" }
```

## Migrating your existing integration

MailKaka's send API is intentionally shaped like the send-email APIs most libraries and codebases already use, so many integrations only need a base URL and key swap. If you're moving over from an existing provider, for a full audit against your codebase hand an AI coding assistant (Claude Code, Cursor, etc.) this prompt:

```
You are migrating a codebase from Resend (https://resend.com) to MailKaka (https://www.mailkaka.in), an India-first transactional email and business mailbox platform. Full reference: https://www.mailkaka.in/docs.md

Do the following:

1. Find every use of the Resend SDK/API in this repo: `new Resend(...)`, `resend.emails.send(...)`, `resend.batch.send(...)`, the `RESEND_API_KEY` env var, and any raw HTTP calls to `api.resend.com`.

2. Base URL and auth:
   - Replace the Resend base URL with MailKaka's: https://www.mailkaka.in/api
   - Replace `RESEND_API_KEY` with a MailKaka key (starts with `mk_`) in an env var such as `MAILKAKA_API_KEY`.
   - Existing Resend Node SDK single-message sends can usually be migrated by passing `baseURL` in the constructor: `new Resend(MAILKAKA_API_KEY, { baseURL: 'https://www.mailkaka.in/api' })`. Raw HTTP/cURL callers just change the host and Bearer token.

3. Sender addresses: MailKaka accepts both `from: 'email@domain.com'` and `from: 'Display Name <email@domain.com>'`. The inner email address must belong to the verified domain scoped to the API key.

4. `to`/`cc`/`bcc` behavior matches Resend: pass an array of addresses in one request; do not fan a multi-recipient send out into one request per recipient. Exception: if the code opts into MailKaka's `unsubscribe: true` field, that requires exactly one `to` recipient per request -- Resend has no equivalent flag, so only handle this if you're adding new MailKaka-specific behavior, not as part of a like-for-like migration.

5. Batch sends: MailKaka implements `POST /emails/batch` for arrays of 1-100 messages and returns `{ data: [{ id }] }` in request order. Existing `resend.batch.send(...)` calls can use it after the same base URL and key change. The full batch is rejected before queueing if any item is invalid. Preserve any existing `Idempotency-Key` header; MailKaka uses it to deduplicate the whole batch.

6. Webhooks: MailKaka only emits `sent`, `delivered`, `bounced`, `complained` events, signed via HMAC-SHA256 in an `X-Mailkaka-Signature` header (verify the same way you'd verify Resend's `svix-signature`, just with a plain HMAC of the raw body instead of Svix). MailKaka has no `opened`/`clicked` tracking events and no customer-facing inbound-email webhook -- inbound mail is instead delivered into MailKaka's own hosted team mailbox, not forwarded to your endpoint. If the code has handlers for those Resend event types, flag them clearly as unsupported rather than silently dropping the logic; do not fabricate a workaround.

7. Error shapes differ: MailKaka returns HTTP 400 (not 422) for request-validation failures, 401 for a missing/invalid API key, 403 for a key without send permission (or an unverified/suspended sending domain), and 429 when the plan's monthly send quota is exhausted. Update any status-code-specific error handling accordingly.

8. Before cutting over traffic, call `GET https://www.mailkaka.in/api/api-keys/validate` with the new key (`Authorization: Bearer mk_...`) to confirm it's live and see its permissions/domain without sending a real email.

Ask the user before removing any Resend-specific code you're not fully certain has a MailKaka equivalent -- prefer flagging an unsupported feature over deleting working logic.
```
