# Mailer by satz — API documentation

Four steps: register an app, get its key, post a submission, read the response.

## 1. Register your app

On the [dashboard](https://mail.satz.co.in/dashboard), **Register an app** is seven steps. The first three are required, the next three are optional and can be switched on later, and the last one issues the key.

1. **Basics** — the website name, and the email address submissions should be delivered to. Any inbox works.
2. **Form fields** — the fields you need from your form. Only these are emailed; anything else your form posts is refused with `400 unknown_field` (Max: 25 fields).
3. **Mail design** — one of 5 designs for how a submission looks in the inbox. Switchable any time.
4. **Auto-reply** *(optional)* — a message emailed back to whoever submitted the form.
5. **Spam guard** *(optional)* — a bot fills every input it finds and submits instantly, while a person leaves a hidden field alone and takes a few seconds. Name a **honeypot field** and a **timing field** your form posts and either signal refuses the submission with `422`.
6. **Attachments** *(optional)* — let submissions carry files.
7. **Generate key** — creates the app and shows the secret key, once.

## 2. Post the submission

**Without files — JSON**

```
const res = await fetch("https://mail.satz.co.in/api/v1/send", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Jane Doe",
    email: "jane@example.com",
    message: "Hello!",
  }),
});
```

**With files — multipart**

```
const res = await fetch("https://mail.satz.co.in/api/v1/send", {
  method: "POST",
  headers: { "Authorization": `Bearer ${SECRET_KEY}` },
  body: new FormData(form),
});

if (res.ok) form.success(); // your function
```

With attachments on, the whole request may total **5.0 MB** — fields and every file together, not per file — across at most 5 files. `.doc`/`.xls` are not accepted.**Supported formats:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.pdf`, `.txt`, `.csv`, `.docx`, `.xlsx`, `.pptx`.

## 3. Read the response

Always JSON plus a status, never a redirect — so what a visitor sees next is your decision. Success is `{ "ok": true }`; every failure carries a machine-readable `error` code, and the two field errors name the field.

| Status | Meaning |
| --- | --- |
| `200` | The email was sent to the configured address. |
| `200` | `duplicate: true` — an identical submission within 60s; no second email sent. |
| `400` | Empty or invalid body. |
| `400` | `unknown_field` — the submission contained a field the app doesn't declare. |
| `400` | `body_too_deep` — the body nests more than 5 levels. |
| `401` | Secret key missing or invalid. |
| `403` | `destination_unverified` — the destination address hasn't confirmed yet. |
| `413` | `payload_too_large` — the request body exceeded 500KB, or 5.0 MB for an app with attachments switched on. |
| `422` | `honeypot_filled` — the app's honeypot field arrived non-empty. |
| `422` | `too_fast` — submitted faster than the app's minimum fill time. |
| `422` | `timing_missing` — the timing field held nothing usable. |
| `422` | `spam_rejected` — the content scored past the spam threshold. |
| `422` | `attachments_not_enabled` — a file was posted but the app doesn't accept them. |
| `422` | `too_many_files` — more files than the app's limit. |
| `422` | `unsupported_file_type` — the file's type isn't accepted, or its contents don't match its name. |
| `422` | `empty_file` — one of the files was zero bytes. |
| `422` | `invalid_filename` — a file arrived with no extension. |
| `429` | `daily_limit_exceeded` — the app has used its 500 sends for the day. |
| `502` | The mail server failed to send. |

## Important notes

- **500 emails a day per app**, on the UTC calendar day. Past it, submissions are refused with `429` rather than dropped silently — ask us at [contact@satz.co.in](mailto:contact@satz.co.in) if your form needs more.
- **Repeats are collapsed.** An identical submission within 60 seconds answers `200` with `"duplicate": true` and sends no second email, so a double-clicked submit is safe. A failed send is not a repeat, so retrying after a `502` does deliver.
- **Body size** is capped at 500KB, or 5.0 MB with attachments on, and nesting at 5 levels.
- **Content is scored for spam** on every submission — link volume, anchor or BBCode markup and mail-header probes → `422 spam_rejected`, recorded in the app's activity with the reason. A **honeypot** field and a **minimum fill time** are available per app on the [dashboard](https://mail.satz.co.in/dashboard); neither belongs in your field list, since both are stripped before the field check runs.
- **A blocked submission costs nothing** from the daily allowance. Anything that reaches the mail server counts, including a send that fails there.
- **`From:` is always our address** and the submitter goes in `Reply-To:`, so replying from your inbox reaches them. Sending as their address would fail SPF and DMARC.
- **An optional automatic reply** to the submitter can be switched on per app. It is a second email and uses a second send; if the day's allowance runs out, the submission still goes through and the reply is the part skipped.
- **Rotating the key invalidates the old one immediately.** Only a hash is stored, so a lost key cannot be recovered — rotate and update your integration.
- **Delivered submissions are not stored.** The activity log keeps the status, the time and the mail server's reply, not the fields you received. Only a blocked submission records what triggered the block.
- **Every attempt is logged** against the app, successes and failures alike, so a missing email is diagnosable from the [dashboard](https://mail.satz.co.in/dashboard).

## Try it

A live tester that sends a real email using one of your app's secret keys is available on the HTML version of this page at https://mail.satz.co.in/docs after signing in.
