---
title: "Send a batch of emails (from an app)"
method: POST
path: "/app-emails/batch"
tags: ["Email"]
---

# Send a batch of emails (from an app)

`POST /app-emails/batch`

## Request

- Base URL: `https://api.hypamoon.com/api/v1`
- URL: `https://api.hypamoon.com/api/v1/app-emails/batch`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://app.hypamoon.com/oauth/authorize`, token URL `https://api.hypamoon.com/oauth/token`, scopes: `data:write`)

For an app's own server code only: await env.HYPAMOON.sendEmailBatch([{ to, subject, text }, ...]) does this call without an Authorization header; Hypamoon adds the app's key, and any other key gets 403. The rules of POST /emails/batch for the app's project; from is optional per email, as in sendEmail.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `emails` object[], required
    - `from` string: Optional, as in sendEmail.
    - `to` union, required: One address or a list (at most 50 recipients in to, cc and bcc together). Everyone in to and cc sees the others, as in any email; use bcc, or a batch with one email per person, when they must not. Every address must appear in this project (a form submission, a document, a chat conversation, a user of one of your apps) or be confirmed by the owner.
      - string
      - string[]
    - `cc` string[]: Optional. Copy recipients; they see the to and cc addresses. The same rules as to.
    - `bcc` string[]: Optional. Hidden recipients; nobody sees them. The same rules as to.
    - `subject` string, required: The subject line.
    - `text` string: The plain text body. Send text, html or both.
    - `html` string: The HTML body. No scripts, no forms; links must show where they go.
    - `reply_to` string: Optional. Where replies go, like the user's own mailbox.
    - `category` 'transactional' | 'other': transactional: a reply to something the recipients did (an order, a booking, a form they sent). other (default): anything else; it gets an unsubscribe link with the project's postal address (needs a business address in the company details) and a List-Unsubscribe header, is not sent to people who unsubscribed, and goes to exactly one recipient (use a batch for more people).
    - `in_reply_to` string, uuid: The id of the form submission (a document from a public form) or the chat conversation this email answers. Required when the recipient appears in the project only through a form or a chat; the address must be in that submission or conversation.
    - `check_recipients` boolean: Optional, default true: before the first email to an address, check that its domain exists and accepts email, and refuse likely typos (like gmial.com) and, for category other, throwaway addresses. false skips those checks; an address that bounced before is always refused.
  - `idempotency_key` string

## Response `200`

The same Idempotency-Key as before: the original emails.

- object
  - `data` object[], required: The emails in the order of the request.
    - `id` string, uuid, required: The id of the email. sendEmail and sendEmailBatch in an app return it; getEmail(id) reads it back.
    - `app_id` string, uuid, nullable, required: The app that sent it with env.HYPAMOON.sendEmail; null when it was sent with the API, by an agent or from the portal.
    - `from` string, required
    - `to` string, required: The first address in to. All recipients are in recipients.
    - `recipients` object, required: Everyone this email went to. After 90 days the addresses are masked, like s***@example.org.
      - `to` string[], required
      - `cc` string[], required
      - `bcc` string[], required
    - `recipient_count` integer, required: to, cc and bcc together; each counts toward the limits and the bundle.
    - `attachments` object[], required: Name, type and size of each attachment. The content is never stored.
      - `filename` string, required
      - `content_type` string, required
      - `size` integer, required: In bytes.
    - `reply_to` string, nullable, required
    - `subject` string, required
    - `category` 'transactional' | 'other', required
    - `status` 'queued' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'simulated', required: queued, sent (accepted for delivery), delivered, bounced, complained (marked as spam), failed, or simulated (test mode).
    - `recipient_source` 'confirmed' | 'member' | 'form' | 'document' | 'conversation' | 'app_user' | 'buyer' | 'pass_holder' | 'null', nullable, required: Where the recipient appears in the project.
    - `test` boolean, required
    - `error` string, nullable, required
    - `reason` string, nullable, required: Why the email bounced, was marked as spam, is delayed or was not sent, in plain words. null otherwise.
    - `recipient_status` object[], required: The status per recipient. With several recipients one can bounce while the others are delivered.
      - `email` string, required: The address. Masked after 90 days, like s***@example.org.
      - `field` 'to' | 'cc' | 'bcc', required
      - `status` 'queued' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'simulated', required
      - `reason` string, nullable, required: Why it bounced, was marked as spam, is delayed or was not sent.
      - `updated_at` string, nullable, required: When this recipient's status last changed.
    - `timeline` object[], required: Every step, oldest first: queued, then sent (or simulated, or failed), then what the recipient's mail server reported.
      - `status` 'queued' | 'sent' | 'delivered' | 'delayed' | 'bounced' | 'complained' | 'failed' | 'simulated', required: queued, sent, delivered, delayed (the recipient's server has not accepted it yet; retried automatically), bounced, complained, failed or simulated.
      - `at` string, required: When it happened (ISO 8601).
      - `recipient` string, nullable, required: The recipient this step is about; null for every recipient.
      - `reason` string, nullable, required: A short reason in plain words, for delayed, bounced, complained and failed.
    - `created_at` string, required
    - `sent_at` string, nullable, required
    - `delivered_at` string, nullable, required
    - `bounced_at` string, nullable, required
    - `complained_at` string, nullable, required
    - `sender` 'hypamoon_fallback': Only when the project has no verified sending domain of its own: the email went from Hypamoon's address ("<app or project> via Hypamoon"), in a neutral layout without the project's images or design. Tell the user so.
    - `next_step` string: With sender hypamoon_fallback: how to send from the project's own address with its own design.

## Other responses

- `201`: Accepted for delivery (or simulated with a test key).
- `400`: Bad Request: invalid_json, idempotency_key_invalid.
- `401`: Unauthorized: unauthorized, missing_api_key, invalid_api_key, api_key_revoked, project_deleted.
- `403`: Forbidden: project_not_enabled, key_mode_mismatch, missing_scope, verification_required, forbidden, mail_not_enabled, email_paused, recipient_not_allowed.
- `409`: Conflict: idempotency_key_reused, email_domain_not_verified.
- `422`: Unprocessable Content: validation_failed, email_address_invalid, company_details_missing, email_blocked, attachment_not_allowed.
- `429`: Too Many Requests: rate_limited, email_limit_reached.
- `500`: Internal Server Error: internal_error.
- `502`: Bad Gateway: upstream_error.
- `503`: Service Unavailable: service_unavailable.

---

[API](https://skmtc.dev/hypamoon/apis/hypamoon-api.md) · [All operations](https://skmtc.dev/hypamoon/apis/hypamoon-api/llms.txt) · [OpenAPI document](https://skmtc.dev/hypamoon/apis/hypamoon-api/revisions/9f0b90004c06?raw)
