# MailerDash Docs — full text > Complete MailerDash API documentation (https://docs.mailerdash.com). API base URL: https://api.mailerdash.com. Generated for AI agents/LLMs from the docs source. # MailerDash — Documentation URL: https://docs.mailerdash.com/en/ Welcome to the MailerDash documentation. (Pages without an English translation fall back to Spanish.) --- # Campaigns URL: https://docs.mailerdash.com/en/bulk/campaigns/ A campaign sends an email to all active contacts in a list. The process is: create the campaign (in `draft` status) → optional: test it → send or schedule → analytics update in real time as the send progresses. ## Operations ### Management **List campaigns** ``` GET /v1/bulk/campaigns ``` Optional parameters: `status`, `limit` (default `100`), `offset`, `key_id`. **Create campaign** ``` POST /v1/bulk/campaigns ``` Required body: | Field | Type | Description | |---|---|---| | `name` | string | Internal name of the campaign. | | `template_id` | string | ID of the template to send. | | `list_id` | string | ID of the target contact list. | | `from_email` | string | Sender address. Must be a verified domain. | Optional body: `from_name`, `reply_to`. Response: **201** with the campaign object in `status: "draft"`. **Get campaign** ``` GET /v1/bulk/campaigns/{id} ``` **Update campaign** ``` PATCH /v1/bulk/campaigns/{id} ``` Only available when `status` is `draft` or `scheduled`. Updatable fields: `name`, `template_id`, `list_id`, `from_email`, `from_name`, `reply_to`, `scheduled_at`. ### Sending and control **Send or schedule** ``` POST /v1/bulk/campaigns/{id}/send ``` Optional body: `{ "scheduled_at": "2026-07-01T09:00:00Z" }`. Without a body (or without `scheduled_at`) the send is immediate. Response: **200**. **Send test email** ``` POST /v1/bulk/campaigns/{id}/test ``` Required body: `{ "to": "yo@miempresa.com" }`. Optional body: `{ "sample_data": {} }`. Renders the template with sample data and sends it to the specified address. Response: **200**. **Pause** ``` POST /v1/bulk/campaigns/{id}/pause ``` Pauses a send in progress. Response: **200**. Returns **409** if the campaign is not in `sending`. **Resume** ``` POST /v1/bulk/campaigns/{id}/resume ``` Resumes a paused campaign. Response: **200**. Returns **409** if the campaign is not in `paused`. **Cancel** ``` POST /v1/bulk/campaigns/{id}/cancel ``` Cancels the campaign. Available from `draft`, `scheduled`, `sending`, or `paused`. Returns **409** if already in `sent` or `cancelled`. **Duplicate** ``` POST /v1/bulk/campaigns/{id}/duplicate ``` Creates a new `draft` with the same parameters. Response: **201** with the new campaign object. ### Analytics **Detailed analytics** ``` GET /v1/bulk/campaigns/{id}/analytics ``` Returns engagement metrics: opens (unique, total, rate), clicks (unique, total, rate, ctor), bounces, complaints, unsubs, rates (open/ctr/ctor/bounce/complaint/unsub), `top_links[]` and `timeline[]`. Optional parameter: `format=csv` to export. **Delivery report** ``` GET /v1/bulk/campaigns/{id}/report ``` Delivery statistics for the campaign. **Deliverability** ``` GET /v1/bulk/campaigns/{id}/deliverability ``` Crosses sends + bounces + complaints. Includes `delivery_rate`, `bounce_rate`, `complaint_rate` and a breakdown by destination domain. **Cross-campaign performance** ``` GET /v1/bulk/campaigns/performance ``` Optional parameter: `key_id`. Returns an array of objects with `id`, `name`, `status`, `started_at`, `delivered`, `opens_unique`, `clicks_unique`, `bounced`, `open_rate`, `click_rate`, and `bounce_rate`. **Aggregated rates** ``` GET /v1/bulk/analytics/rates ``` Optional parameters: `period` (`24h` | `7d` | `30d`, default `7d`), `key_id`, `format=csv`. Returns `period`, `granularity`, `window_start`, `totals`, `rates`, and `series[]`. ## Lifecycle A campaign's status follows this flow: ``` draft │ ├─ /test (test send, does not change status) │ └─ /send ──► sending ──► sent │ ├─ /pause ──► paused │ │ └───────────────┘ /resume │ └─ /cancel ──► cancelled /cancel also available from: draft, scheduled, paused ``` The possible states are: `draft`, `scheduled`, `sending`, `sent`, `paused`, `cancelled`. A campaign in `sent` is immutable — it cannot be resent. ## Full example ```bash # 1. Create the campaign curl -X POST https://api.mailerdash.com/v1/bulk/campaigns \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Newsletter Junio 2026", "template_id": "bienvenida-fc9674", "list_id": "newsletter-q3-2026-abc123", "from_email": "noticias@miempresa.com", "from_name": "Mi Empresa", "reply_to": "contacto@miempresa.com" }' # 2. Send a test email to your inbox curl -X POST https://api.mailerdash.com/v1/bulk/campaigns/cmp-d894cd3d626d9d11/test \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"to": "yo@miempresa.com"}' # 3. Launch the bulk send curl -X POST https://api.mailerdash.com/v1/bulk/campaigns/cmp-d894cd3d626d9d11/send \ -H "Authorization: Bearer $MAILERDASH_API_KEY" ``` ## Reference For the full request/response schema and error codes, see the [bulk API reference](/reference/bulk/). --- # Contacts URL: https://docs.mailerdash.com/en/bulk/contacts/ Contacts are the foundation of the entire bulk system. Each contact has an `email` as its unique identifier within your API key, plus optional fields: `name` and `metadata` (a free-form JSON object for any segmentation data you need). A contact can belong to multiple lists and can have one of four statuses: `active`, `unsubscribed`, `bounced`, or `complained`. ## Schema | Field | Type | Description | |---|---|---| | `id` | integer | Internal identifier. | | `email` | string | Contact's email. Unique per key. | | `name` | string | Optional name. | | `metadata` | object | Free-form JSON object for custom attributes. | | `status` | string | `active` \| `unsubscribed` \| `bounced` \| `complained` | | `key_id` | string | API key the contact belongs to. | | `created_at` | string | Creation date (ISO 8601). | | `updated_at` | string | Last updated date (ISO 8601). | ## Operations ### List contacts ``` GET /v1/bulk/contacts ``` Query parameters: | Parameter | Description | |---|---| | `limit` | Maximum number of results (default: `100`). | | `offset` | Offset for pagination. | | `status` | Filter by status: `active`, `unsubscribed`, `bounced`, `complained`. | | `q` / `search` | Search by email or name. | | `key_id` | Filter by API key (admin only). | | `format` | `csv` to export in CSV format. | --- ### Create or update a contact ``` POST /v1/bulk/contacts ``` Body: ```json { "email": "ana@empresa.com", "name": "Ana García", "metadata": { "plan": "pro" } } ``` If a contact with that `email` already exists, their data is updated. Returns **201** with the full contact. --- ### Get a contact ``` GET /v1/bulk/contacts/{email} ``` Returns the contact with that email, including all their fields. --- ### Update a contact ``` PATCH /v1/bulk/contacts/{email} ``` Body (all fields are optional): ```json { "name": "Ana García López", "status": "unsubscribed", "metadata": { "plan": "enterprise" } } ``` --- ### Delete a contact ``` DELETE /v1/bulk/contacts/{email} ``` Permanently deletes the contact. Returns **204 No Content**. --- ### Engagement history ``` GET /v1/bulk/contacts/{email}/engagement ``` Returns the history of campaigns received by that contact and their engagement score. Response: ```json { "email": "ana@empresa.com", "sends": [ { "campaign_id": "camp_abc123", "campaign_name": "Newsletter Q2", "sent_at": "2026-04-15T10:00:00Z", "opened": true, "clicked": false } ], "totals": { "sends": 5, "opens": 3, "clicks": 1 }, "score": "active", "last_engagement_at": "2026-04-15T14:22:00Z" } ``` The `score` field can be `active`, `passive`, or `dormant` based on the contact's recent activity. --- ### Bulk import contacts ``` POST /v1/bulk/contacts/import ``` Body: array of objects with `email` (required), `name` and `metadata` optional. ```json [ { "email": "ana@empresa.com", "name": "Ana García", "metadata": { "plan": "pro" } }, { "email": "pedro@cliente.io", "name": "Pedro Sánchez" }, { "email": "maria@otro.com" } ] ``` Returns **207 Multi-Status** with the result for each entry: ```json { "created": 2, "updated": 1, "failed": 0, "errors": [] } ``` --- ### Import preview ``` POST /v1/bulk/contacts/import-preview ``` Checks how many emails already exist in your account before importing. Does not modify any data. Body: ```json { "emails": ["ana@empresa.com", "nuevo@dominio.com"] } ``` Response: ```json { "existing": ["ana@empresa.com"], "total_unique": 2, "total_received": 2 } ``` --- ### Bulk delete contacts ``` POST /v1/bulk/contacts/delete ``` Atomic deletion of multiple contacts. Body: ```json { "emails": ["ana@empresa.com", "pedro@cliente.io"] } ``` Response: ```json { "requested": 2, "deleted": 2, "not_found": [], "forbidden": [] } ``` ## Examples ### Create a contact with metadata ```bash curl -X POST https://api.mailerdash.com/v1/bulk/contacts \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "ana@empresa.com", "name": "Ana García", "metadata": {"plan": "pro", "signup_source": "landing"}}' ``` ### Bulk import contacts ```bash curl -X POST https://api.mailerdash.com/v1/bulk/contacts/import \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '[ {"email": "ana@empresa.com", "name": "Ana García", "metadata": {"plan": "pro"}}, {"email": "pedro@cliente.io", "name": "Pedro Sánchez", "metadata": {"region": "CDMX"}}, {"email": "maria@otro.com"} ]' ``` ## Reference For the full request/response schema and error codes, see the [bulk API reference](/reference/bulk/). --- # Lists URL: https://docs.mailerdash.com/en/bulk/lists/ A list is a group of contacts you can target with a campaign. Each campaign points to exactly one list, and a list can contain any number of active contacts. Lists are independent of each other: a contact can belong to multiple lists at the same time, and removing them from one list does not affect them in others or delete them from the system. ## Schema | Field | Type | Description | |---|---|---| | `id` | string | Slug identifier for the list. | | `name` | string | Descriptive name. | | `key_id` | string | API key the list belongs to. | | `contact_count` | integer | Number of contacts in the list. | | `created_at` | string | Creation date (ISO 8601). | ## Operations ### List all lists ``` GET /v1/bulk/lists ``` Returns an array with all lists in your account, including the `contact_count` for each one. --- ### Create a list ``` POST /v1/bulk/lists ``` Body: ```json { "name": "Newsletter Q3 2026" } ``` Returns **201** with the created list, including its automatically generated `id`. --- ### Get a list with its contacts ``` GET /v1/bulk/lists/{id} ``` Returns the list details along with its paginated contacts. Query parameters: | Parameter | Description | |---|---| | `limit` | Maximum number of contacts to return. | | `offset` | Offset for pagination. | | `status` | Filter contacts by status. | --- ### Rename a list ``` PATCH /v1/bulk/lists/{id} ``` Body: ```json { "name": "Newsletter Q4 2026" } ``` --- ### Delete a list ``` DELETE /v1/bulk/lists/{id} ``` Deletes the list. Returns **204 No Content**. If the list is referenced by an active campaign, returns **409 Conflict**. --- ### Add contacts to a list ``` POST /v1/bulk/lists/{id}/contacts ``` Body: ```json { "emails": ["ana@empresa.com", "pedro@cliente.io"] } ``` The emails must correspond to contacts that already exist in your account. Returns **200** with the number of contacts added. --- ### Remove a contact from a list ``` DELETE /v1/bulk/lists/{id}/contacts/{email} ``` Removes the contact from this list. Returns **204 No Content**. The contact continues to exist in the system and in any other list it belongs to. --- ### Clone a list ``` POST /v1/bulk/lists/{id}/duplicate ``` Creates a copy of the list with all its contacts. Useful for creating derived segments without starting from scratch. Returns **201** with the new list. --- ### Bulk delete lists ``` POST /v1/bulk/lists/delete ``` Atomic deletion of multiple lists. Body: ```json { "ids": ["newsletter-q3-abc123", "promo-verano-xyz"] } ``` Response: ```json { "requested": 2, "deleted": 1, "not_found": [], "forbidden": [], "in_use": ["promo-verano-xyz"] } ``` Lists referenced by a campaign are reported in `in_use` and are not deleted. The remaining ones are deleted, even within the same request. ## Examples ### Create a list and add contacts ```bash # 1. Create the list curl -X POST https://api.mailerdash.com/v1/bulk/lists \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Newsletter Q3 2026"}' # 2. Add contacts (use emails already registered as contacts) curl -X POST https://api.mailerdash.com/v1/bulk/lists/newsletter-q3-2026-abc123/contacts \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"emails": ["ana@empresa.com", "pedro@cliente.io"]}' ``` ## Reference For the full request/response schema and error codes, see the [bulk API reference](/reference/bulk/). --- # Overview (Bulk / Marketing) URL: https://docs.mailerdash.com/en/bulk/overview/ The bulk (marketing) module is independent from the transactional module. It uses the same authentication — `Authorization: Bearer $MAILERDASH_API_KEY` — but operates on a different channel (`X-Md-Channel: bulk`). You do not need to add that header to your REST calls: the bulk-worker adds it automatically when dispatching campaigns. Your interaction with the API is always to manage data (contacts, lists, templates, campaigns, sequences), not to trigger sends directly. ## Data model The general flow is: **Contacts** → added to **Lists** → **Campaigns** (or **Sequences**) point to a List + a Template → the bulk-worker dispatches the sends. ## Resources ### Contacts Individual records with `email`, `name`, and arbitrary metadata. Each contact has a `status` that reflects their deliverability state: - `active` — can receive emails. - `unsubscribed` — unsubscribed; will not receive further messages. - `bounced` — a permanent bounce (5.x.x) occurred; excluded from future sends. - `complained` — marked an email as spam via FBL; excluded from future sends. Contacts are the source of truth for your subscribers. ### Lists Groups of contacts. A campaign points to exactly one list. You can have separate lists per product, segment, or channel (newsletter, onboarding, reactivation). Contacts can belong to multiple lists. ### Templates Reusable HTML and/or plain-text content, with its own `subject`. Campaigns and sequences reference them by ID, letting you update the content without modifying already-configured campaigns. ### Campaigns One-shot bulk send to a list. Lifecycle: - `draft` — draft state, editable. - `send` — queued for immediate dispatch. - `sent` — the bulk-worker finished processing all contacts. Once sent, a campaign is not modifiable. ### Sequences Drip automations. A sequence defines steps (`steps`) with a delay between each one (hours or days), and has `subscribers` — contacts that advance through the steps at the configured intervals. Useful for automated onboarding, nurturing, and follow-ups. ## Suppressions All bulk routes respect the account's global suppression list. Contacts with status `bounced`, `complained`, or `unsubscribed` are automatically skipped at send time — you don't need to filter them manually. See the [suppressions](/en/suppressions/) guide to learn how to manage the list and what happens when a contact is suppressed from different sources. ## Guides by resource | Resource | Guide | |---|---| | Contacts | [Create, import, and manage contacts](/en/bulk/contacts/) | | Lists | [Organize contacts into lists](/en/bulk/lists/) | | Templates | [Create reusable templates](/en/bulk/templates/) | | Campaigns | [Send bulk campaigns](/en/bulk/campaigns/) | | Sequences | [Automate with sequences (drip)](/en/bulk/sequences/) | --- # Sequences (drip) URL: https://docs.mailerdash.com/en/bulk/sequences/ A sequence is a drip automation: you define a series of emails (steps) with delays between them, then subscribe contacts. The system sends each step at the right time without manual intervention. They are ideal for onboarding, nurturing, and post-conversion follow-ups. ## Key concepts - **Sequence** — the container. Has a name and `status` (`draft` / `active` / `archived`). Must be in `active` for the worker to process sends. - **Step** — an email within the sequence. Has `position` (execution order), `offset_seconds` (delay from subscription or from the previous step), and the email content: `subject` + `html` / `body_text`, or a `template_id`. At least one of the two content schemes must be present. - **Subscriber** — a contact subscribed to the sequence. Advances through the steps over the configured time, and can be in active, paused, completed, or cancelled state. ## Operations ### Sequences **List** ``` GET /v1/bulk/sequences ``` **Create** ``` POST /v1/bulk/sequences ``` Required body: `{ "name": "Sequence name" }`. Response: **201** with `status: "draft"`. **Get with its steps** ``` GET /v1/bulk/sequences/{id} ``` **Update** ``` PATCH /v1/bulk/sequences/{id} ``` Body: `{ "name"?: string, "status"?: "draft" | "active" | "archived" }`. **Delete** ``` DELETE /v1/bulk/sequences/{id} ``` Returns **409** if the sequence has active or paused subscribers. ### Steps **Add step** ``` POST /v1/bulk/sequences/{id}/steps ``` Required body: | Field | Type | Description | |---|---|---| | `position` | integer | Order of the step within the sequence (1, 2, 3…). | | `offset_seconds` | integer | Seconds to wait before sending this step. | Optional body: `subject`, `html`, `body_text`, `from_email`, `from_name`, `reply_to`, `template_id`. Response: **201** with the step object. Returns **403** if `from_email` uses a domain not authorized on the account. **Update step** ``` PATCH /v1/bulk/sequences/{id}/steps/{step_id} ``` Accepts the same optional fields as the POST. Returns **403** if `from_email` uses an unauthorized domain. **Delete step** ``` DELETE /v1/bulk/sequences/{id}/steps/{step_id} ``` Response: **204**. ### Subscribers **List subscribers** ``` GET /v1/bulk/sequences/{id}/subscribers ``` **Subscribe a contact** ``` POST /v1/bulk/sequences/{id}/subscribers ``` The operation is idempotent: if the contact already has an active or paused subscription, it returns **200** without creating a duplicate. Required body: `{ "email": "contacto@example.com" }`. Optional body: `{ "started_at"?: string (ISO 8601), "variables"?: object }`. Response: **201** (new subscription) or **200** (already existed as active/paused). Returns **409** if the subscription exists in a terminal state (`completed` or `cancelled`). **Get subscriber status** ``` GET /v1/bulk/sequences/{id}/subscribers/{email} ``` **Cancel subscription** ``` DELETE /v1/bulk/sequences/{id}/subscribers/{email} ``` Response: **200**. **Pause** ``` POST /v1/bulk/sequences/{id}/subscribers/{email}/pause ``` Response: **200**. Pending steps are held until resumed. **Resume** ``` POST /v1/bulk/sequences/{id}/subscribers/{email}/resume ``` Response: **200**. ## Example: create a welcome sequence ```bash # 1. Create the sequence curl -X POST https://api.mailerdash.com/v1/bulk/sequences \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Onboarding - Bienvenida"}' # Response: { "id": "onboarding-bienvenida-a1b2c3", "status": "draft", ... } # 2. Add the first step (immediate, offset_seconds=0) curl -X POST https://api.mailerdash.com/v1/bulk/sequences/onboarding-bienvenida-a1b2c3/steps \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "position": 1, "offset_seconds": 0, "subject": "Bienvenido a {{name}}, aquí empieza todo", "from_email": "hola@miempresa.com", "template_id": "bienvenida-fc9674" }' # 3. Add the second step (3 days later = 259200 seconds) curl -X POST https://api.mailerdash.com/v1/bulk/sequences/onboarding-bienvenida-a1b2c3/steps \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "position": 2, "offset_seconds": 259200, "subject": "¿Cómo va todo?", "from_email": "hola@miempresa.com", "template_id": "followup-fc9675" }' # 4. Activate the sequence curl -X PATCH https://api.mailerdash.com/v1/bulk/sequences/onboarding-bienvenida-a1b2c3 \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status": "active"}' # 5. Subscribe a contact curl -X POST https://api.mailerdash.com/v1/bulk/sequences/onboarding-bienvenida-a1b2c3/subscribers \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "ana@empresa.com"}' ``` ## Sequence webhooks If you have webhooks configured on your API key, you will receive events for each moment in a subscription's lifecycle: | Event | When it fires | |---|---| | `sequence.subscribed` | A new contact subscribes. | | `sequence.step.sent` | A step is sent successfully. | | `sequence.step.failed` | A step fails to send. | | `sequence.completed` | The contact completed all steps. | | `sequence.cancelled` | The subscription is cancelled. Includes a `reason` field: `unsubscribed`, `manual`, `bounced`, `complained`, or `suppressed`. | ## Reference For the full request/response schema and error codes, see the [bulk API reference](/reference/bulk/). --- # Templates URL: https://docs.mailerdash.com/en/bulk/templates/ A template defines the subject and body (HTML and/or plain text) of emails. Campaigns and sequences reference it by `id` — you can update the template without touching the campaign, and change the content before sending. This lets you iterate on copy or design without having to edit each campaign separately. ## Schema | Field | Type | Description | |---|---|---| | `id` | string | Slug identifier for the template. | | `name` | string | Descriptive name to identify it in the dashboard. | | `subject` | string | Email subject. Supports merge tags. | | `html` | string | HTML body. Supports merge tags. | | `body_text` | string | Plain-text body. Supports merge tags. | | `key_id` | string | API key this template belongs to. | | `created_at` | string | Creation date (ISO 8601). | | `updated_at` | string | Last updated date (ISO 8601). | ## Merge tags You can personalize the subject and body using `{{name}}` to insert the contact's name. MailerDash replaces the tag at send time with the value of the `name` field on the recipient contact. Example: ``` subject: "Hi {{name}}, your report is ready" html: "

Hi {{name}},

Your monthly report is available.

" ``` If the contact has no `name`, the tag is replaced with an empty string. ## Operations ### List templates ``` GET /v1/bulk/templates ``` Returns an array with all templates in your account. --- ### Create a template ``` POST /v1/bulk/templates ``` Body: ```json { "name": "Bienvenida onboarding", "subject": "Bienvenido a bordo, {{name}}", "html": "

Hola {{name}},

Tu cuenta está lista.

", "body_text": "Hola {{name}},\n\nTu cuenta está lista." } ``` `name` and `subject` are required. Returns **201** with the created template. --- ### Get a template ``` GET /v1/bulk/templates/{id} ``` Returns all fields of the template, including `html` and `body_text`. --- ### Update a template ``` PATCH /v1/bulk/templates/{id} ``` Body (all fields are optional): ```json { "subject": "¡Bienvenido, {{name}}!", "html": "

Hola {{name}},

Tu cuenta está lista. Empieza ahora.

" } ``` Returns **200** with the updated template. Fields you don't send are not modified. --- ### Delete a template ``` DELETE /v1/bulk/templates/{id} ``` Deletes the template. Returns **204 No Content**. If it is referenced by an active campaign, returns **409 Conflict**. --- ### Bulk delete templates ``` POST /v1/bulk/templates/delete ``` Atomic deletion of multiple templates. Body: ```json { "ids": ["bienvenida-onboarding-abc", "promo-verano-xyz"] } ``` Response: ```json { "requested": 2, "deleted": 1, "not_found": [], "forbidden": [], "in_use": ["promo-verano-xyz"] } ``` Templates in use by active campaigns are reported in `in_use` and are not deleted. The rest are deleted in the same request. ## Example ### Create a template with HTML and plain text ```bash curl -X POST https://api.mailerdash.com/v1/bulk/templates \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Bienvenida onboarding", "subject": "Bienvenido a bordo, {{name}}", "html": "

Hola {{name}},

Tu cuenta está lista.

", "body_text": "Hola {{name}},\n\nTu cuenta está lista." }' ``` ## Reference For the full request/response schema and error codes, see the [bulk API reference](/reference/bulk/). --- # SPF, DKIM, and DMARC URL: https://docs.mailerdash.com/en/domains/spf-dkim-dmarc/ The three email authentication protocols (SPF, DKIM, and DMARC) tell receiving servers that your emails are legitimate. Without them, Gmail, Outlook, and Yahoo may send them to spam or reject them outright. --- ## SPF — who can send on your behalf SPF (Sender Policy Framework) lists the servers authorized to send email from your domain. Receiving servers verify that the sender's IP is on that list. ### Record to publish Add (or modify) the TXT record at the root of your domain: ``` _Name_: tuempresa.com (or @) _Type_: TXT _Value_: v=spf1 include:mailerdash.com ~all ``` If you already have an existing SPF record (for example from Google Workspace or another provider), **do not create a second TXT** — add the `include` to your existing record: ``` v=spf1 include:_spf.google.com include:mailerdash.com ~all ``` --- ## DKIM — cryptographic signature of content DKIM (DomainKeys Identified Mail) signs each email with a private key. The receiving server verifies the signature using the public key published in your DNS. ### CNAME delegation (one-time setup, forever) MailerDash uses **CNAME delegation**: you publish a CNAME once and the platform rotates the internal key without you ever having to touch your DNS again. ``` _Name_: ._domainkey.tuempresa.com _Type_: CNAME _Value_: ``` When you [verify your domain](/en/domains/verify-domain/), the dashboard shows you the exact `selector` and CNAME value. Publish that record once and you're done — key rotations are transparent to you. --- ## DMARC — alignment policy DMARC (Domain-based Message Authentication, Reporting and Conformance) defines what the receiving server should do when an email fails SPF or DKIM, and sends you usage reports. ### Recommended record to start ``` _Name_: _dmarc.tuempresa.com _Type_: TXT _Value_: v=DMARC1; p=none; rua=mailto:dmarc-reports@tuempresa.com ``` | Field | Description | |-------|-------------| | `p=none` | Monitor without action — emails that fail still arrive. Ideal for getting started. | | `p=quarantine` | Emails that fail go to spam. Enable once you are confident in your configuration. | | `p=reject` | Emails that fail are rejected. Maximum protection; enable after reviewing reports. | | `rua=mailto:...` | Address where you will receive daily XML reports from receiving servers. | ### Recommended progression 1. **Weeks 1–2**: publish `p=none` with `rua` to receive reports without any impact. 2. **Weeks 3–4**: review the reports and confirm that your legitimate emails pass SPF and DKIM. 3. **Month 2**: move up to `p=quarantine`. 4. **Month 3+**: move up to `p=reject` if the reports show correct alignment. --- ## Verify your configuration Once the records are published, you can verify with online tools: - **MXToolbox** (mxtoolbox.com) — SPF, DKIM, and DMARC lookups - **mail-tester.com** — send a test email and receive a configuration score - **Google Postmaster Tools** — domain reputation monitoring for traffic to Gmail --- ## Summary of records to publish | Record | DNS Name | Type | Value | |--------|-----------|------|-------| | SPF | `tuempresa.com` | TXT | `v=spf1 include:mailerdash.com ~all` | | DKIM | `._domainkey.tuempresa.com` | CNAME | the exact target shown in your dashboard | | DMARC | `_dmarc.tuempresa.com` | TXT | `v=DMARC1; p=none; rua=mailto:...` | You get the exact selector and CNAME from the dashboard when you verify your domain, or by querying `GET /v1/domains`. --- **Next step**: [Verify your domain](/en/domains/verify-domain/) to complete registration on the platform. **API reference**: [Platform — Domains](/reference/platform/) --- # Verify a domain URL: https://docs.mailerdash.com/en/domains/verify-domain/ To send emails from a domain (for example, `notificaciones@tuempresa.com`) you must prove that you own that domain. Verification is done by publishing a TXT record in your DNS — a standard process that does not affect your email or any other services on the domain. ## Verification flow The process has three steps: 1. **Register** the domain via API → you receive a unique token. 2. **Publish** that token as a TXT record in your DNS. 3. **Verify** by calling the `/verify` endpoint → the system performs the DNS lookup and, if found, marks the domain as verified. ## Operations **List registered domains** ``` GET /v1/domains ``` Returns all domains on the account with their status: `verified` (boolean), `verified_at` (timestamp or null), and the associated `token`. **Register a domain** ``` POST /v1/domains ``` Body: `{ "domain": "tuempresa.com" }`. Response: **201** with the token and the exact instructions for publishing in DNS. Returns **409** if the domain is already registered on the account. **Verify** ``` POST /v1/domains/{domain}/verify ``` Performs a DNS TXT lookup. If found, sets `verified: true` and records `verified_at`. Response: `{ domain, verified, verified_at }`. Returns **400** if the TXT record is not found yet. **Delete a domain** ``` DELETE /v1/domains/{domain} ``` Requires an admin API key. Response: **204**. ## Step-by-step example ```bash # Step 1: register the domain curl -X POST https://api.mailerdash.com/v1/domains \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "tuempresa.com"}' ``` The response includes the exact instructions you need: ```json { "domain": "tuempresa.com", "token": "md-verify=abc123xyz", "instructions": { "record_type": "TXT", "record_name": "_md-verify.tuempresa.com", "record_value": "md-verify=abc123xyz", "next_step": "Add this TXT record to your DNS, then call POST /v1/domains/tuempresa.com/verify" } } ``` **Step 2:** in your DNS panel (Cloudflare, Route 53, Namecheap, etc.) add the following record: | Field | Value | |---|---| | Name / Host | `_md-verify.tuempresa.com` | | Type | `TXT` | | Value | `md-verify=abc123xyz` | | TTL | Any (300 seconds recommended) | Wait a few minutes for the record to propagate, then: ```bash # Step 3: verify curl -X POST https://api.mailerdash.com/v1/domains/tuempresa.com/verify \ -H "Authorization: Bearer $MAILERDASH_API_KEY" ``` Successful response: ```json { "domain": "tuempresa.com", "verified": true, "verified_at": "2026-06-22T14:30:00.000Z" } ``` ## Without a verified domain you cannot send Attempting to send an email (transactional or campaign) with a `from_email` whose domain is not verified results in **403 Forbidden** with code `domain_not_authorized`. The error is returned at request time, before the message is queued. Once verified, the domain is automatically available for all API keys on your account — there is no additional activation step. ## Next step With your domain verified, the next step is to configure the email authentication records: SPF, DKIM, and DMARC. These records improve deliverability and protect your domain from spoofing. See the [SPF, DKIM, and DMARC](/en/domains/spf-dkim-dmarc/) guide. --- # Open and click tracking URL: https://docs.mailerdash.com/en/events/tracking/ Open and click tracking is an automatic feature available in **campaigns** (bulk). When you send a campaign with HTML content, MailerDash injects a tracking pixel and rewrites links to capture recipient interactions. Tracking **does not apply to transactional sends** (`POST /v1/mail/send`). --- ## How it works ### Open tracking (pixel) At send time, MailerDash inserts an invisible `1×1px` pixel at the end of the HTML `` (or at the end of the content if there is no ``): ```html ``` When the email client loads the image, an open is recorded and associated with that send and contact. ### Click tracking (link rewriting) All `` elements in the HTML that point to `http://` or `https://` URLs are rewritten to pass through the tracking domain: ```html Ver oferta Ver oferta ``` The redirect captures the click and sends the recipient to the original URL in under a second. **Links that are not rewritten:** - `mailto:`, `tel:`, `javascript:`, anchors (`#sección`) - Unresolved placeholders (`{{variable}}`) - The List-Unsubscribe and `One-Click` unsubscribe link - URLs that already point to the tracking domain --- ## Where to view analytics ### Campaign analytics ```bash GET /v1/bulk/campaigns/{id}/analytics Authorization: Bearer $MAILERDASH_API_KEY ``` Response (summary): ```json { "campaign_id": "camp_abc123", "delivered": 1250, "opens_unique": 342, "opens_total": 489, "clicks_unique": 118, "clicks_total": 201, "bounces": 12, "complaints": 2, "unsubs": 5, "rates": { "open": 0.2736, "ctr": 0.0944, "ctor": 0.3450, "bounce": 0.0096, "complaint": 0.0016, "unsub": 0.0040 }, "top_links": [...], "timeline": [...], "detail_window_days": 90 } ``` Also append `?format=csv` to export per-recipient detail with columns `email`, `status`, `sent_at`, `first_opened_at`, `first_clicked_at`. ### Aggregated rates across campaigns ```bash GET /v1/bulk/analytics/rates?period=7d ``` `period` accepts `24h`, `7d`, or `30d`. Returns consolidated metrics for all campaigns on your key within that window. ### Contact engagement ```bash GET /v1/bulk/contacts/{email}/engagement ``` Returns the interaction history (opens, clicks, bounces, complaints, unsubscribes) for a specific contact, useful for segmentation and deliverability diagnostics. --- ## Unique vs. total | Metric | Meaning | |---|---| | `opens_unique` | Number of **distinct contacts** who opened at least once | | `opens_total` | Total opens (includes multiple opens from the same contact) | | `clicks_unique` | Distinct contacts who clicked at least one link | | `clicks_total` | Total clicks (includes repeated clicks and clicks on multiple links) | For calculating open rates, always use `opens_unique / delivered`; `opens_total` is useful for measuring deep engagement (if a contact opens the email multiple times). --- ## Data retention The `detail_window_days: 90` field indicates that individual open/click events (detailed per-recipient timeline) are kept for **90 days**. Aggregated campaign counters are retained indefinitely. --- ## Privacy and GDPR See the full bulk endpoint reference at [/reference/bulk/](/reference/bulk/). --- # Webhooks URL: https://docs.mailerdash.com/en/events/webhooks/ Webhooks let you receive real-time notifications when events occur in your account: delivered emails, failures, unsubscribes, and campaign and sequence events. Each API key can have its own `webhook_url`. When an event fires, MailerDash sends a `POST` to that URL with the event payload signed with HMAC-SHA256. --- ## Configure your webhook The webhook is configured per API key using `PATCH /v1/client/keys/:id` with the fields `webhook_url` (and optionally a `webhook_secret` you can rotate later). If no secret exists when you set the URL, the system generates one automatically. ```bash curl -X PATCH https://api.mailerdash.com/v1/client/keys/mi-key \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_url": "https://mi-app.com/webhooks/mailerdash" }' ``` To **view the current webhook configuration** (without exposing the secret): ```bash GET /v1/client/keys/:id/webhook ``` Response: ```json { "webhook_url": "https://mi-app.com/webhooks/mailerdash", "secret_set": true, "secret_masked": "whsec_••••••••••••3a9f" } ``` To **disable** the webhook (URL + secret are set to `null`): ```bash DELETE /v1/client/keys/:id/webhook ``` --- ## `WebhookEvent` payload Each event arrives as a `POST` with `Content-Type: application/json`. The body always contains the fields `event`, `ts`, and `app`, plus additional fields specific to the event type. ```json { "event": "email.sent", "ts": "2026-06-22T15:30:00.000Z", "app": "mi-app", "request_id": "req_abc123", "to": ["usuario@ejemplo.com"], "subject": "Tu factura de junio", "message_id": "" } ``` ### Available events | Event | Description | |---|---| | `email.sent` | Transactional email sent successfully | | `email.failed` | Transactional email failed to send | | `contact.unsubscribed` | A contact unsubscribed | | `campaign.email.sent` | Campaign email sent to a contact | | `campaign.email.failed` | Campaign email failed for a contact | | `campaign.completed` | Campaign finished sending to all recipients | | `sequence.subscribed` | Contact subscribed to a sequence | | `sequence.step.sent` | Sequence step sent | | `sequence.step.failed` | Sequence step failed | | `sequence.completed` | Sequence completed for a contact | | `sequence.cancelled` | Sequence cancelled (field `reason` with cause) | ### Fields per event - **`email.sent` / `email.failed`**: `request_id`, `to` (array), `subject`, `message_id`. `failed` also includes `error`. - **`contact.unsubscribed`**: `email`. - **`campaign.email.sent` / `campaign.email.failed`**: `email`, `campaign_id`. `failed` also includes `error`. - **`campaign.completed`**: `campaign_id`, `name`, `sent_count`, `failed_count`, `delivery_rate`. - **`sequence.*`**: `email`, `campaign_id` (sequence id). `sequence.cancelled` also includes `reason` (`unsubscribed`, `manual`, `bounced`, `complained`, `suppressed`). --- ## Verifying the HMAC-SHA256 signature Each request includes two security headers: - `X-Md-Signature`: `sha256=` - `X-Md-Timestamp`: ISO timestamp at the time of delivery The signature is computed over the string `.` using your `webhook_secret`. ```js const crypto = require('crypto'); function verifyWebhook(req, webhookSecret) { const signature = req.headers['x-md-signature']; // "sha256=abc123..." const timestamp = req.headers['x-md-timestamp']; // "2026-06-22T15:30:00.000Z" const rawBody = req.body; // Buffer or string — must be the raw unparsed body if (!signature || !timestamp) return false; // Check that the timestamp is not too old (anti-replay) const diff = Math.abs(Date.now() - new Date(timestamp).getTime()); if (diff > 5 * 60 * 1000) return false; // more than 5 minutes → reject // Compute HMAC over "." const signedPayload = `${timestamp}.${rawBody}`; const expected = 'sha256=' + crypto .createHmac('sha256', webhookSecret) .update(signedPayload) .digest('hex'); // Compare in constant time to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // Example with Express — parse body as Buffer to preserve the raw body app.post('/webhooks/mailerdash', express.raw({ type: 'application/json' }), (req, res) => { const valid = verifyWebhook(req, process.env.MAILERDASH_WEBHOOK_SECRET); if (!valid) return res.status(401).send('Invalid signature'); const event = JSON.parse(req.body); console.log('Evento recibido:', event.event); res.sendStatus(200); }); ``` ```python from datetime import datetime, timezone def verify_webhook(signature: str, timestamp: str, raw_body: bytes, secret: str) -> bool: # Check that the timestamp is not too old (anti-replay) ts = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) diff = abs((datetime.now(timezone.utc) - ts).total_seconds()) if diff > 300: # more than 5 minutes → reject return False signed_payload = f"{timestamp}.{raw_body.decode('utf-8')}".encode('utf-8') expected = 'sha256=' + hmac.new( secret.encode('utf-8'), signed_payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) ``` --- ## Retries with backoff If your endpoint does not respond with a `2xx`, MailerDash retries automatically: 1. **Initial burst** (in-process, max 3 attempts): after the first failure, retries at 1s, 5s, and 25s. 2. **Queue with progressive backoff** (if the burst fails completely): 5 min → 30 min → 2 h → 6 h → 24 h (×3). 3. If all attempts are exhausted, the delivery is marked as `failed` (DLQ) and visible in the delivery history. The timeout per attempt is **10 seconds**. Your endpoint must respond within that time. --- ## Management endpoints ### Rotate the secret Generates a new `webhook_secret` and invalidates the previous one. Use this if you believe your secret was compromised. ```bash POST /v1/client/keys/:id/webhook/rotate-secret ``` ### Send a test event Fires a `webhook.test` event to the configured URL to verify that your endpoint receives it correctly. ```bash POST /v1/client/keys/:id/webhook/test ``` ### Delivery history Lists the most recent deliveries with their status, HTTP code, and timestamp. ```bash GET /v1/client/keys/:id/webhook/deliveries ``` Query parameters: `limit` (default 50, max 200), `offset`. ### Delivery detail Includes the full payload and the response body from your endpoint (up to 2 KB). ```bash GET /v1/client/keys/:id/webhook/deliveries/:dId ``` ### Manually retry a delivery Re-fires a specific event using the key's current URL and secret. ```bash POST /v1/client/keys/:id/webhook/deliveries/:dId/retry ``` --- ## Additional security - In production, MailerDash **only delivers to HTTPS URLs**. - URLs that resolve to private or reserved IPs (loopback, RFC1918, cloud metadata) are blocked to prevent SSRF. - The `webhook_secret` is only shown masked in API responses; it is never exposed in full after creation. See the full reference at [/reference/platform/](/reference/platform/). --- # Authentication URL: https://docs.mailerdash.com/en/get-started/authentication/ MailerDash has two authentication methods: ## API keys (server-to-server) For your backend integrations. Send the key in the `Authorization` header: ```http Authorization: Bearer md_xxxxxxxxxxxxxxxxxxxx ``` ## JWT (dashboard) The dashboard uses session JWTs. Each JWT user resolves to their linked API key for send operations. Do not use JWT for server-to-server integrations; use an API key. ## Channels Mark the traffic type with `X-Md-Channel`: - `trans` — transactional (high priority, transactional transport). - `bulk` — marketing / campaigns. --- # Concepts URL: https://docs.mailerdash.com/en/get-started/concepts/ ## Transactional vs marketing - **Transactional** (`X-Md-Channel: trans`): 1:1 email triggered by user actions (confirmations, OTP, receipts). High deliverability expected. - **Marketing / bulk** (`X-Md-Channel: bulk`): list campaigns, drip (sequences). Subject to suppressions and unsubscribes. ## Sending domains You can only send from **verified** domains associated with your account. Verification publishes SPF/DKIM/DMARC; DKIM signing is handled by the platform via **CNAME delegation** (you publish a CNAME once and we rotate the key without you touching your DNS). ## Reputation and deliverability Reputation is built per domain + IP. Start with moderate volumes (warmup), keep your bounce/complaint rate low, and respect suppressions. --- # Introduction URL: https://docs.mailerdash.com/en/get-started/introduction/ MailerDash is a platform for sending **transactional email** (confirmations, password recovery, notifications) and **marketing** (list campaigns, drip automations) via a simple HTTPS API. ## How it works - You authenticate each request with an **API key** (`Authorization: Bearer` header). - You send over two channels: **transactional** and **bulk** (marketing), routed with the `X-Md-Channel` header. - Your emails are signed with **DKIM** from your verified domains for maximum deliverability. ## The `/v1` contract All endpoints live under `/v1`, the stable public contract. The version of the running software is exposed at `GET /version` (changes with each release; the `/v1` contract does not). ## Next step Follow the [Quickstart](/en/get-started/quickstart/) to send your first email. --- # Quickstart URL: https://docs.mailerdash.com/en/get-started/quickstart/ ## 1. Get your API key In the dashboard, create an API key. Store it securely: **the token is only shown once when you create it**. ## 2. Verify your domain To send signed email you need a verified domain. Publish the SPF/DKIM/DMARC records provided by the API (see [Concepts](/en/get-started/concepts/)). ## 3. Send an email The payload follows SendGrid style: `personalizations[].to[].email`, `from`, `subject` and `content[]`. ```bash curl -X POST https://api.mailerdash.com/v1/mail/send \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Md-Channel: trans" \ -d '{ "personalizations": [{ "to": [{ "email": "recipient@example.com" }] }], "from": { "email": "no-reply@tu-dominio.com", "name": "Tu Servicio" }, "subject": "Hola desde MailerDash", "content": [{ "type": "text/html", "value": "

Funciona 🎉

" }] }' ```
```js const res = await fetch('https://api.mailerdash.com/v1/mail/send', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MAILERDASH_API_KEY}`, 'Content-Type': 'application/json', 'X-Md-Channel': 'trans', }, body: JSON.stringify({ personalizations: [{ to: [{ email: 'recipient@example.com' }] }], from: { email: 'no-reply@tu-dominio.com', name: 'Tu Servicio' }, subject: 'Hola desde MailerDash', content: [{ type: 'text/html', value: '

Funciona 🎉

' }], }), }); console.log(res.status, await res.json()); ```
```python res = requests.post( "https://api.mailerdash.com/v1/mail/send", headers={ "Authorization": f"Bearer {os.environ['MAILERDASH_API_KEY']}", "X-Md-Channel": "trans", }, json={ "personalizations": [{"to": [{"email": "recipient@example.com"}]}], "from": {"email": "no-reply@tu-dominio.com", "name": "Tu Servicio"}, "subject": "Hola desde MailerDash", "content": [{"type": "text/html", "value": "

Funciona 🎉

"}], }, ) print(res.status_code, res.json()) ```
## Errors Error responses use a consistent format: ```json { "error": { "type": "validation_error", "code": "domain_unauthorized", "message": "..." } } ``` See the [error reference](/en/reference/errors/) for the full catalog of `type` and `code` values. --- # Errors URL: https://docs.mailerdash.com/en/reference/errors/ All error responses from the MailerDash API follow a unified format inspired by Stripe. This makes programmatic handling straightforward: you can tell what type of error occurred (and how to react) without parsing the text message. --- ## Error response structure ```json { "error": { "type": "validation_error", "code": "domain_unauthorized", "message": "The from domain is not authorized for this key.", "param": "from.email" } } ``` | Field | Type | Description | |---|---|---| | `type` | string | Error category (see table below). Determines the retry strategy. | | `code` | string | Machine-readable specific code. | | `message` | string | Human-readable description in English. | | `param` | string (optional) | Field or parameter that caused the error, when applicable. | | `details` | array (optional) | List of sub-errors for multiple-validation cases. | --- ## Error types (`type`) The `type` field is categorical and lets you decide on a strategy without reading `code` or `message`: | `type` | HTTP | When it occurs | |---|---|---| | `validation_error` | 400 / 422 | The request has invalid or missing fields. Do not retry without fixing. | | `authentication_error` | 401 | Token missing, malformed, or invalid. Check the API key. | | `authorization_error` | 403 | Authenticated but lacking permissions. The key does not have the required scope. | | `not_found_error` | 404 | The requested resource does not exist. | | `rate_limit_error` | 429 | Rate limit or monthly quota exceeded. Retry with backoff. | | `conflict_error` | 409 | Conflict with the current state (e.g. resource already exists). | | `payload_too_large_error` | 413 | The body exceeds the allowed size (e.g. attachments too large). | | `idempotency_error` | 409 | The idempotency key was already used with a different payload. | | `smtp_error` | 5xx | Error delivering the email via SMTP. | | `api_error` | 5xx | Internal server error. Retry with exponential backoff. | --- ## Specific codes (`code`) ### Transactional send errors | `code` | `type` | Description | |---|---|---| | `domain_unauthorized` | `authorization_error` | The `from` domain is not authorized for this API key. | | `email_not_verified` | `authorization_error` | The account has not verified its email; sends are blocked until it does. | | `free_requires_verified_domain` | `authorization_error` | The free plan requires a verified domain to send. | | `monthly_quota_exceeded` | `rate_limit_error` | The plan's monthly send quota has been reached. | | `rate_limit_exceeded` | `rate_limit_error` | The per-minute request limit (`rate_per_min`) has been exceeded. | ### Domain errors | `code` | `type` | Description | |---|---|---| | `max_domains_exceeded` | `validation_error` | You have reached the domain limit allowed by your plan. | | `domain_taken` | `conflict_error` | The domain is already registered under another account on the platform. | | `reserved_domain` | `validation_error` | The domain is reserved and cannot be registered by clients. | | `cname_delegation_disabled` | `validation_error` | CNAME delegation is not enabled for this account. | ### API key errors | `code` | `type` | Description | |---|---|---| | `max_api_keys_exceeded` | `validation_error` | You have reached the API key limit for your plan. | | `key_not_linked` | `authorization_error` | The user's JWT has no linked API key for the requested resource. | | `already_revoked` | `conflict_error` | The API key has already been revoked. | | `wrong_password` | `authentication_error` | The provided password is incorrect (operations requiring confirmation). | ### Billing and plan errors | `code` | `type` | Description | |---|---|---| | `billing_unavailable` | `api_error` | The billing system is not available at this time. | | `no_billing_account` | `validation_error` | The account does not have an active Stripe subscription. | | `no_active_subscription` | `validation_error` | There is no active subscription for this operation. | | `overage_not_offered` | `validation_error` | The current plan does not offer overage sends. | | `package_not_selectable` | `validation_error` | The requested plan is not available for selection. | | `already_subscribed` | `conflict_error` | You already have this plan active. | ### General errors | `code` | `type` | Description | |---|---|---| | `not_found` | `not_found_error` | The resource does not exist. | | `unauthorized` | `authentication_error` | Token missing or invalid. | | `forbidden` | `authorization_error` | No permission for this operation. | | `payload_too_large` | `payload_too_large_error` | Body or attachments too large. | | `internal_error` | `api_error` | Internal server error. | | `bad_request` | `validation_error` | Malformed request. | --- ## Multiple validation errors When a single request has several invalid fields, the response includes the `details` array: ```json { "error": { "type": "validation_error", "code": "invalid", "message": "Request validation failed", "details": [ { "code": "required", "message": "'subject' is required", "param": "subject" }, { "code": "invalid_format", "message": "'from.email' must be a valid email", "param": "from.email" } ] } } ``` --- ## Node.js handling example ```js const res = await fetch('https://api.mailerdash.com/v1/mail/send', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MAILERDASH_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: { email: 'noreply@mi-app.com' }, to: [...], subject: '...', text: '...' }), }); if (!res.ok) { const { error } = await res.json(); if (error.type === 'rate_limit_error') { // Retry after a backoff — the Retry-After header may indicate when console.warn('Rate limit alcanzado, reintentando en 60s...', error.code); } else if (error.type === 'validation_error') { // Do not retry — fix the request console.error('Error de validación:', error.code, error.param); } else if (error.type === 'authentication_error') { // Check the API key console.error('API key inválida o sin permisos'); } else { console.error('Error inesperado:', error); } } ``` See the full endpoint reference at [/reference/transactional/](/reference/transactional/) and [/reference/bulk/](/reference/bulk/). --- # Rate limits and quotas URL: https://docs.mailerdash.com/en/reference/rate-limits/ MailerDash enforces two types of limits on sends: a **per-minute rate limit** (`rate_per_min`) and a **monthly quota** (`monthly_quota`). Both are defined at the plan (package) level and can have per-account overrides configured by the MailerDash team. --- ## Rate limit (`rate_per_min`) Controls how many send requests your API key can make within a 60-second window. If you exceed this limit, the API responds with HTTP `429`. The default for accounts without an assigned plan is **30 requests/minute**. Your plan may grant a higher limit. ```json { "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Too many requests. Please slow down." } } ``` The response includes the standard rate limit headers: | Header | Description | |---|---| | `RateLimit-Limit` | Maximum number of requests in the window | | `RateLimit-Remaining` | Requests remaining in the current window | | `RateLimit-Reset` | Unix timestamp when the window resets | --- ## Monthly quota (`monthly_quota`) The maximum number of emails you can send in a billing period (month). The counter resets at the start of each new period. When you reach your monthly quota, additional sends are rejected with HTTP `429`: ```json { "error": { "type": "rate_limit_error", "code": "monthly_quota_exceeded", "message": "Monthly quota exceeded: 5000/5000 emails sent this month" } } ``` ### Overage If your plan includes it, you can continue sending after exhausting your base quota by paying per additional email (overage). If the plan does not offer overage, sends that exceed the quota will be blocked with the code `overage_not_offered`. --- ## Check your current usage ```bash GET /v1/client/usage Authorization: Bearer $MAILERDASH_API_KEY ``` This endpoint returns the usage for the current period for each API key linked to your account, along with the limits of the active plan: ```json { "subscriptions": [ { "package_id": "pro", "package_name": "Pro", "monthly_quota": 50000, "rate_per_min": 120, "billing_period": "monthly", "started_at": "2026-06-01T00:00:00.000Z" } ], "keys": [ { "id": "mi-key", "label": "Producción", "sent_this_period": 12340, "monthly_quota": 50000, "quota_remaining": 37660 } ], "next_reset": "2026-07-01T00:00:00.000Z" } ``` The `next_reset` field indicates when the monthly counter will reset. --- ## Payload limits In addition to rate and quota limits, there are limits on request size: - **Attachments**: if the total attachments exceed the allowed limit, you will receive a `413` with code `payload_too_large`. The `param` field will indicate `attachments`. --- ## Summary of related error codes | `code` | HTTP | Description | |---|---|---| | `rate_limit_exceeded` | 429 | You exceeded the per-minute request limit | | `monthly_quota_exceeded` | 429 | You exceeded your plan's monthly quota | | `overage_not_offered` | 400 | The plan does not allow extra sends after the quota is exhausted | | `payload_too_large` | 413 | The request body exceeds the maximum size | See the full reference at [/reference/platform/](/reference/platform/) and error handling at [/en/reference/errors/](/en/reference/errors/). --- # Versioning and compatibility URL: https://docs.mailerdash.com/en/reference/versioning/ ## The `/v1` contract All public MailerDash API endpoints live under the `/v1/` prefix. This prefix represents the **stable public contract**: once an endpoint is documented under `/v1/`, we commit to not introducing breaking changes without a deprecation cycle. Examples of changes that are **not** considered breaking (they are backward-compatible): - Adding new optional fields to existing responses. - Adding new endpoints. - Adding new possible values to enums in non-critical fields. - Changing human-readable error messages (the `message` field). Examples of changes that **are** breaking and would require `/v2/`: - Removing or renaming existing response fields. - Changing the type of a field (e.g. from string to array). - Changing the semantics of an existing parameter. - Modifying error codes (`type`, `code`) in an incompatible way. --- ## When `/v2` appears A new `/v2/` prefix is introduced only when breaking changes are necessary. There are no current plans to migrate to `/v2/`. When it happens, it will be announced in advance and both versions will coexist during the grace period. --- ## Deprecation policy When an endpoint needs to change its path or behavior in a non-backward-compatible way, we follow this process: 1. The new endpoint or behavior is published (with its new path or semantics). 2. The previous endpoint is kept as a **legacy alias** — it continues working exactly as before. 3. The alias is marked `deprecated: true` in the OpenAPI spec (`openapi.yaml`). 4. Clients are notified of the grace period before the alias is removed. --- ## Software version You can check the exact version of the software currently running in production at any time: ```bash GET https://api.mailerdash.com/version ``` Response: ```json { "version": "0.11.22" } ``` This endpoint is outside the `/v1/` prefix and does not require authentication. The version follows [SemVer](https://semver.org/): `MAJOR.MINOR.PATCH`. There is also a health check endpoint: ```bash GET https://api.mailerdash.com/health ``` Returns `200 OK` when the server is active and operating normally. --- ## OpenAPI spec versioning The `openapi.yaml` file is versioned alongside the software. You can access the interactive API reference at: - [/reference/transactional/](/reference/transactional/) — transactional send endpoints - [/reference/bulk/](/reference/bulk/) — campaign, contact, list, and sequence endpoints - [/reference/platform/](/reference/platform/) — keys, domains, webhooks, usage, suppressions --- ## Summary | Concept | Policy | |---|---| | Stable prefix | `/v1/` — does not change without a deprecation cycle | | Breaking changes | Only under a new `/v2/` prefix | | Deprecation | Legacy alias + `deprecated: true` + grace period | | Software version | `GET /version` (no auth) | | Health check | `GET /health` (no auth) | --- # Resources URL: https://docs.mailerdash.com/en/resources/ ## Quick start The following examples show how to send your first transactional email. Replace `$MAILERDASH_API_KEY` with your real API key. ### curl ```bash curl -X POST https://api.mailerdash.com/v1/mail/send \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "email": "noreply@tu-dominio.com", "name": "Tu Empresa" }, "to": [{ "email": "cliente@ejemplo.com" }], "subject": "Bienvenido", "text": "Hola, gracias por registrarte.", "html": "

Hola, gracias por registrarte.

" }' ``` ### Node.js ```js const response = await fetch('https://api.mailerdash.com/v1/mail/send', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MAILERDASH_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: { email: 'noreply@tu-dominio.com', name: 'Tu Empresa' }, to: [{ email: 'cliente@ejemplo.com' }], subject: 'Bienvenido', text: 'Hola, gracias por registrarte.', html: '

Hola, gracias por registrarte.

', }), }); if (!response.ok) { const { error } = await response.json(); console.error(error.type, error.code, error.message); } else { const data = await response.json(); console.log('Enviado, message_id:', data.message_id); } ``` ### Python ```python response = httpx.post( "https://api.mailerdash.com/v1/mail/send", headers={"Authorization": f"Bearer {os.environ['MAILERDASH_API_KEY']}"}, json={ "from": {"email": "noreply@tu-dominio.com", "name": "Tu Empresa"}, "to": [{"email": "cliente@ejemplo.com"}], "subject": "Bienvenido", "text": "Hola, gracias por registrarte.", "html": "

Hola, gracias por registrarte.

", }, ) if response.is_error: error = response.json()["error"] print(f"Error {error['type']}: {error['code']} — {error['message']}") else: print("Enviado:", response.json().get("message_id")) ``` ### PHP With [Guzzle](https://docs.guzzlephp.org/) (`composer require guzzlehttp/guzzle`): ```php post('https://api.mailerdash.com/v1/mail/send', [ 'headers' => ['Authorization' => 'Bearer ' . getenv('MAILERDASH_API_KEY')], 'json' => [ 'from' => ['email' => 'noreply@tu-dominio.com', 'name' => 'Tu Empresa'], 'to' => [['email' => 'cliente@ejemplo.com']], 'subject' => 'Bienvenido', 'text' => 'Hola, gracias por registrarte.', 'html' => '

Hola, gracias por registrarte.

', ], ]); $data = json_decode((string) $response->getBody(), true); echo 'Enviado, message_id: ' . $data['message_id'] . PHP_EOL; } catch (RequestException $e) { $error = json_decode((string) $e->getResponse()->getBody(), true)['error']; fprintf(STDERR, "Error %s: %s — %s\n", $error['type'], $error['code'], $error['message']); } ``` ### Laravel Using Laravel's HTTP client (`Illuminate\Support\Facades\Http`): ```php use Illuminate\Support\Facades\Http; $response = Http::withToken(env('MAILERDASH_API_KEY')) ->post('https://api.mailerdash.com/v1/mail/send', [ 'from' => ['email' => 'noreply@tu-dominio.com', 'name' => 'Tu Empresa'], 'to' => [['email' => 'cliente@ejemplo.com']], 'subject' => 'Bienvenido', 'text' => 'Hola, gracias por registrarte.', 'html' => '

Hola, gracias por registrarte.

', ]); if ($response->failed()) { $error = $response->json('error'); logger()->error("MailerDash {$error['type']}: {$error['code']} — {$error['message']}"); } else { logger()->info('Enviado, message_id: ' . $response->json('message_id')); } ``` ### WordPress With `wp_remote_post`. Define your API key as a constant in `wp-config.php` (`define('MAILERDASH_API_KEY', 'tu-api-key');`): ```php $response = wp_remote_post('https://api.mailerdash.com/v1/mail/send', array( 'headers' => array( 'Authorization' => 'Bearer ' . MAILERDASH_API_KEY, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode(array( 'from' => array('email' => 'noreply@tu-dominio.com', 'name' => 'Tu Empresa'), 'to' => array(array('email' => 'cliente@ejemplo.com')), 'subject' => 'Bienvenido', 'text' => 'Hola, gracias por registrarte.', 'html' => '

Hola, gracias por registrarte.

', )), 'timeout' => 15, )); if (is_wp_error($response)) { error_log('MailerDash: ' . $response->get_error_message()); } elseif (wp_remote_retrieve_response_code($response) >= 400) { $error = json_decode(wp_remote_retrieve_body($response), true)['error']; error_log('MailerDash error: ' . $error['type'] . ' — ' . $error['message']); } else { $data = json_decode(wp_remote_retrieve_body($response), true); error_log('MailerDash enviado, message_id: ' . $data['message_id']); } ``` --- ## API reference The interactive reference (Swagger UI) is available directly in the dashboard: | Section | URL | |---|---| | Transactional (individual sends) | [/reference/transactional/](/reference/transactional/) | | Bulk (campaigns, contacts, lists, sequences) | [/reference/bulk/](/reference/bulk/) | | Platform (keys, domains, webhooks, usage) | [/reference/platform/](/reference/platform/) | You can also explore the API interactively (Swagger UI) or download the public OpenAPI spec (client endpoints only) as JSON: ```bash # Public OpenAPI spec (JSON) — useful for generating SDKs or importing to Postman curl https://api.mailerdash.com/docs.json -o mailerdash-openapi.json ``` - **Interactive Swagger UI:** [https://api.mailerdash.com/docs](https://api.mailerdash.com/docs) - **JSON spec:** `https://api.mailerdash.com/docs.json` --- ## For agents and LLMs If you build with an AI agent or an LLM, these docs are *agent-friendly*: | Resource | URL | |---|---| | LLM index | [`/llms.txt`](https://docs.mailerdash.com/llms.txt) | | Full docs in plain text | [`/llms-full.txt`](https://docs.mailerdash.com/llms-full.txt) | | Raw OpenAPI — Transactional | [`/openapi/openapi.transactional.json`](https://docs.mailerdash.com/openapi/openapi.transactional.json) | | Raw OpenAPI — Bulk | [`/openapi/openapi.bulk.json`](https://docs.mailerdash.com/openapi/openapi.bulk.json) | | Raw OpenAPI — Platform | [`/openapi/openapi.platform.json`](https://docs.mailerdash.com/openapi/openapi.platform.json) | `llms-full.txt` bundles the entire documentation into a single file, ready to paste into a model's context. --- ## Generate your SDK We have not published an official library (`npm`/`pip`) yet, but the public OpenAPI spec (`docs.json`, above) lets you generate a **typed** client in your language with [openapi-generator](https://openapi-generator.tech/) — without waiting for an official package. ### TypeScript ```bash npx @openapitools/openapi-generator-cli generate \ -i https://api.mailerdash.com/docs.json \ -g typescript-fetch \ -o ./mailerdash-sdk ``` ### Python ```bash npx @openapitools/openapi-generator-cli generate \ -i https://api.mailerdash.com/docs.json \ -g python \ -o ./mailerdash-sdk-python ``` The CLI runs on the JVM, so you need **Java 11+** installed. There are generators for more than 50 languages (PHP, Ruby, Go, C#, etc.); see the full list with `npx @openapitools/openapi-generator-cli list`. Since the spec is kept in sync with the production API, regenerating your SDK after a version change is just a matter of running the command again. --- ## Support If you have questions, find a bug, or need help with your integration, open a ticket from the dashboard or write to us directly. The ticket system lets you track each case and attach logs or payload examples for faster diagnosis. - **From the dashboard**: sidebar menu → Support → New ticket - **Via API**: `POST /v1/tickets` with the `subject` and `body` fields We will respond within the business hours published in the dashboard. For critical production incidents, indicate it in the ticket subject. --- # Suppressions URL: https://docs.mailerdash.com/en/suppressions/ The suppression list is the mechanism that protects your sending reputation. Any address that generates a permanent bounce, a spam complaint, or unsubscribes is added to the list automatically, and **the platform skips that email on all future sends** — both transactional and bulk — without you having to do anything. --- ## How entries are added Suppressions are created in three ways: | Source | `reason` | Description | |--------|---------|-------------| | Permanent bounce (5xx) | `bounce` | The receiving server permanently rejected the address | | FBL / spam complaint | `complaint` | The recipient marked the email as spam | | Unsubscribe | `unsubscribed` | The contact clicked the unsubscribe link or called the unsubscribe endpoint | | Manual | `manual` | An admin added the address manually via API or dashboard | --- ## Operations - **`GET /v1/suppressions`** — list all suppressed addresses, paginated. Query params: `limit` (default 100, max 1000), `offset`, `reason` (`bounce`|`complaint`|`manual`), `email` (partial search). - **`POST /v1/suppressions`** — manually add an address (requires admin key). Body: `{ email: string, reason?: "manual"|"complaint" }`. Useful for importing your own opt-out lists. - **`DELETE /v1/suppressions/{email}`** — "remove" a suppression so the address can receive emails again (requires admin key). Returns 204. --- ## Example: list recent suppressions ```bash # List the last 20 bounce suppressions curl "https://api.mailerdash.com/v1/suppressions?reason=bounce&limit=20" \ -H "Authorization: Bearer $MAILERDASH_API_KEY" ``` Response: ```json { "total": 47, "limit": 20, "offset": 0, "items": [ { "email": "usuario@dominio.com", "reason": "bounce", "code": "5.1.1", "added_at": "2026-06-15T10:30:00.000Z" } ] } ``` ## Example: add a manual suppression ```bash curl -X POST https://api.mailerdash.com/v1/suppressions \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "optout@cliente.com", "reason": "manual"}' ``` ## Example: remove a suppression ```bash curl -X DELETE "https://api.mailerdash.com/v1/suppressions/usuario%40dominio.com" \ -H "Authorization: Bearer $MAILERDASH_API_KEY" ``` --- ## GDPR / privacy considerations If a user exercises their right to data erasure (right to be forgotten), the correct process in MailerDash is: 1. Delete the contact via `DELETE /v1/bulk/contacts/{email}` — this removes them from your lists. 2. **Do not** remove the suppression — keeping it protects the user from receiving accidental future emails. The user's account on the platform (if one exists) is anonymized (email/name replaced with placeholders) when deleted, but suppressions remain as protection, not as personally identifiable data. --- **API reference**: [Platform — Suppressions](/reference/platform/) --- # Statistics, bounces, and audit URL: https://docs.mailerdash.com/en/transactional/events-and-stats/ These three endpoints give you complete visibility into what happened with your emails: how many were sent, which ones bounced, and a detailed record of each event. Use them to monitor the health of your deliverability, debug delivery issues, and generate activity reports. ## Available operations ### GET /v1/mail/stats Returns aggregated server metrics for the specified period. ``` GET https://api.mailerdash.com/v1/mail/stats ``` **Query params:** | Parameter | Type | Default | Description | |---|---|---|---| | `hours` | integer | 24 | Lookback time window. Maximum 720 (30 days). | | `key_id` | string | — | Admin only. Filters metrics by a specific API key. | **Example:** ```bash curl https://api.mailerdash.com/v1/mail/stats?hours=48 \ -H "Authorization: Bearer $MAILERDASH_API_KEY" ``` --- ### GET /v1/mail/bounces Lists bounces processed by the system, from most recent to oldest. ``` GET https://api.mailerdash.com/v1/mail/bounces ``` **Query params:** | Parameter | Type | Default | Description | |---|---|---|---| | `limit` | integer | 100 | Number of results. Maximum 1000. | | `offset` | integer | 0 | Pagination cursor. | | `recipient` | string | — | Filters by recipient email address. | | `status` | string | — | Filters by DSN code (e.g. `5.1.1` for unknown user). | | `key_id` | string | — | Admin only. Filters by API key. | **Response:** ```json { "total": 42, "limit": 100, "offset": 0, "items": [ { "id": 17, "recipient": "inexistente@example.com", "status": "5.1.1", "raw": "User unknown", "created_at": "2026-06-21T14:32:00Z" } ] } ``` --- ### GET /v1/mail/audit Audit log of sends and account actions, with support for advanced filters and CSV export. ``` GET https://api.mailerdash.com/v1/mail/audit ``` **Query params:** | Parameter | Type | Default | Description | |---|---|---|---| | `limit` | integer | 100 | Number of results. Maximum 1000. | | `offset` | integer | 0 | Pagination cursor. | | `app` | string | — | Filters by key ID (`app` in the audit). | | `event` | string | — | Filters by event type: `sent`, `failed`, `queued`. Accepts CSV for multiple: `sent,failed`. | | `from` | string | — | ISO 8601 start timestamp for the range (e.g. `2026-06-01T00:00:00Z`). | | `to` | string | — | ISO 8601 end timestamp for the range. | | `search` | string | — | Substring search against the recipient or subject. | | `sort_by` | string | `ts` | Sort field: `ts`, `event`, `subject`. | | `sort_dir` | string | `desc` | Direction: `asc` or `desc`. | | `format` | string | `json` | `json` (paginated) or `csv` (full download, ignores `limit`/`offset`). | **JSON response:** ```json { "total": 1240, "limit": 100, "offset": 0, "items": [ { "id": 998, "ts": "2026-06-22T09:15:00Z", "event": "sent", "app": "", "recipient": "cliente@example.com", "subject": "Tu recibo #4567", "message_id": "" } ] } ``` ## Example: stats for the last 48 hours ```bash curl "https://api.mailerdash.com/v1/mail/stats?hours=48" \ -H "Authorization: Bearer $MAILERDASH_API_KEY" ``` ## Example: bounces for a specific recipient ```bash curl "https://api.mailerdash.com/v1/mail/bounces?recipient=usuario@example.com&limit=20" \ -H "Authorization: Bearer $MAILERDASH_API_KEY" ``` ## Example: audit of failed sends in a date range ```bash curl "https://api.mailerdash.com/v1/mail/audit?event=failed&from=2026-06-01T00:00:00Z&to=2026-06-22T23:59:59Z&sort_dir=asc" \ -H "Authorization: Bearer $MAILERDASH_API_KEY" ``` ## Reference For the complete response schema and error codes, see the [transactional API reference](/reference/transactional/). --- # Send email URL: https://docs.mailerdash.com/en/transactional/send-email/ Transactional email is the message triggered in response to a user action: account confirmation, password reset, purchase receipt, delivery notification. Unlike bulk marketing, each send is individual and tied to a specific event. Use this channel when you need to deliver a targeted message to a specific person. ## Endpoint ``` POST https://api.mailerdash.com/v1/mail/send ``` Successful response — **202 Accepted**: ```json { "message": "accepted", "app": "" } ``` The `202` indicates that the server accepted the message and queued it for delivery. It does not guarantee that the recipient will receive it (that depends on the MTA and the remote server), but it does mean the payload passed validation and is in the queue. ## Payload ```json { "personalizations": [ { "to": [ { "email": "destinatario@example.com", "name": "Nombre Opcional" } ] } ], "from": { "email": "no-reply@tu-dominio.com", "name": "Tu Servicio" }, "subject": "Confirma tu cuenta", "content": [ { "type": "text/html", "value": "

Haz clic aquí para confirmar.

" }, { "type": "text/plain", "value": "Visita https://example.com/verify para confirmar tu cuenta." } ] } ``` ### Fields | Field | Type | Required | Description | |---|---|---|---| | `personalizations` | array | yes | List of recipient groups. At least one element. | | `personalizations[].to` | array | yes | Recipients in the group. Each item: `{ email, name? }`. | | `from` | object | yes | Sender. `{ email, name? }`. Must be a verified domain on your account. | | `subject` | string | yes | Email subject line. | | `content` | array | yes | Message bodies. At least one. Each item: `{ type, value }`. | | `content[].type` | string | yes | `"text/plain"` or `"text/html"`. Including both is recommended. | | `content[].value` | string | yes | Body content in the specified type. | ## Multiple recipients To send the same message to several recipients in a single request, add more objects to the `to` array inside `personalizations[0]`: ```json { "personalizations": [ { "to": [ { "email": "ana@example.com", "name": "Ana" }, { "email": "beto@example.com", "name": "Beto" }, { "email": "carlos@example.com" } ] } ], "from": { "email": "no-reply@tu-dominio.com" }, "subject": "Actualización de tu cuenta", "content": [{ "type": "text/plain", "value": "Tu cuenta ha sido actualizada." }] } ``` Each address in `to` receives the message independently. ## Transactional channel Include the `X-Md-Channel: trans` header in all your requests. Although the transactional channel is the default behavior, declaring it explicitly is good practice: it makes the intent clear and makes debugging easier if the MTA routing changes in the future. ```bash -H "X-Md-Channel: trans" ``` ## Recipient validation Before queuing any message, MailerDash validates each recipient address in three layers: 1. **Syntax** — the address must have a valid RFC format. 2. **Disposable domains** — a list of more than 5,000 temporary email domains (mailinator, guerrillamail, etc.) is rejected. 3. **MX lookup** — it verifies that the domain has active MX records (result cached for 24 hours). If any address fails any of the three layers, the entire request is rejected with `400 Bad Request`. Validate your lists before sending to large volumes. ## Idempotency If you include the `Idempotency-Key` header with a unique UUID per attempt, the server automatically deduplicates retries: ```bash -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" ``` - Within the **24-hour** window, a second request with the same key returns exactly the same response without re-queuing the message. - If you reuse the same `Idempotency-Key` with a **different payload**, you will receive `422 Unprocessable Entity`. Use idempotency in any retry logic to avoid duplicates. ## Full example ```bash curl -X POST https://api.mailerdash.com/v1/mail/send \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Md-Channel: trans" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "personalizations": [ { "to": [ { "email": "cliente@example.com", "name": "Cliente Ejemplo" } ] } ], "from": { "email": "no-reply@tu-dominio.com", "name": "Tu Servicio" }, "subject": "Confirma tu cuenta en Tu Servicio", "content": [ { "type": "text/html", "value": "

Hola Cliente Ejemplo,

Haz clic aquí para confirmar tu cuenta.

El enlace expira en 24 horas.

" }, { "type": "text/plain", "value": "Hola Cliente Ejemplo,\n\nVisita el siguiente enlace para confirmar tu cuenta:\nhttps://tu-dominio.com/verify?token=abc123\n\nEl enlace expira en 24 horas." } ] }' ``` Expected response: ```json { "message": "accepted", "app": "" } ``` ## Reference For the complete request/response schema, error codes, and additional parameters, see the [transactional API reference](/reference/transactional/). ---