Transactional email API

Send, batch, schedule, reschedule, cancel and look up transactional email through the Reggio Digital API, with attachments and test addresses.

Every call on this page needs a key with the email:send ability, on an account with a transactional email plan and at least one verified sending domain. The dashboard's Send API page shows the same examples with the account's own key and sender filled in.

Sending one email

POST /api/v1/emails

curl -X POST https://reggiodigital.com/api/v1/emails \
  -H "Authorization: Bearer $REGGIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "from": "hello@yourdomain.com",
      "to": [
          "success@simulator.amazonses.com"
      ],
      "subject": "Your receipt",
      "html": "<p>Thanks for your order.</p>",
      "text": "Thanks for your order.",
      "tags": {
          "kind": "receipt",
          "order": "1042"
      }
  }'

A message we accept answers 202:

{
  "id": "0100019a1b2c3d4e-5f6a7b8c-1234-5678-9abc-def012345678-000000",
  "to": ["success@simulator.amazonses.com"],
  "suppressed": []
}

Store the id. It is how you look the message up later and how our webhooks refer to it. Treat it as an opaque string: a message sent now and a message held for later have ids of different shapes.

to is the recipients we are sending to. suppressed is any recipient we skipped because we have stopped sending to that address after a hard bounce, a spam complaint or an unsubscribe. A skipped address is not an error, so check the list rather than treating the send as failed. If every recipient is suppressed, the send is refused with a 422.

The fields

Field Required What it is
from Yes An address on one of the account's verified sending domains, on its own. A name in front of it, as in Acme <hello@yourdomain.com>, is refused with a 422.
to Yes A list of up to 50 addresses. Always a list, even for one recipient.
subject Without a template The subject line, up to 998 characters.
html Without a template, html or text The formatted body.
text Without a template, html or text The plain body. Send both when you can.
template_id Instead of a body A published template. Leave out subject, html and text.
variables With a template The values the template fills in, as name and value pairs.
reply_to No Where replies go, if not the from address.
cc, bcc No Lists of up to 50 addresses each.
tags No Up to 20 name and value pairs for your own use, such as {"kind": "receipt", "order": "1042"}. Names use letters, numbers, dashes, underscores, colons and dots; values are strings up to 128 characters. They come back when you look a message up and in webhooks.
headers No Up to 10 extra email headers. Any name starting with X-, plus List-Unsubscribe and List-Unsubscribe-Post. Values cannot contain line breaks.
attachments No Up to 20 files. See attachments.
scheduled_at No A time up to 30 days ahead to send it instead of now. See sending later.

If Gmail threads a run of similar receipts into one conversation, set X-Entity-Ref-ID in headers to something unique per message.

Attachments

Give each attachment a filename and either content, the file as base64, or path, an https link we download it from. A link saves your server holding a large file in memory.

curl -X POST https://reggiodigital.com/api/v1/emails \
  -H "Authorization: Bearer $REGGIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "from": "hello@yourdomain.com",
      "to": [
          "success@simulator.amazonses.com"
      ],
      "subject": "Your receipt",
      "html": "<p>Thanks for your order.</p><img src=\"cid:logo\" alt=\"\">",
      "text": "Thanks for your order.",
      "tags": {
          "kind": "receipt",
          "order": "1042"
      },
      "attachments": [
          {
              "filename": "receipt-1042.txt",
              "content": "T3JkZXIgMTA0MgpUb3RhbDogJDQyLjAwCg=="
          },
          {
              "filename": "logo.png",
              "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAMAASsJTYQAAAAASUVORK5CYII=",
              "content_id": "logo"
          }
      ]
  }'
  • A path must be https and reachable from the public internet. Links into private networks are refused.
  • Add a content_id and the file becomes an inline image. Point at it from your HTML with <img src="cid:logo">.
  • Set content_type to the file's type, such as application/pdf. Without it the file is sent as application/octet-stream, which some mail apps will not preview.
  • Keep the whole message under 28MB. Anything bigger is refused with a 413; send a link to the file instead.
  • Batches cannot carry attachments.

Sending later

Give scheduled_at a time with a timezone, up to 30 days ahead:

curl -X POST https://reggiodigital.com/api/v1/emails \
  -H "Authorization: Bearer $REGGIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "from": "hello@yourdomain.com",
      "to": [
          "success@simulator.amazonses.com"
      ],
      "subject": "Your receipt",
      "html": "<p>Thanks for your order.</p>",
      "text": "Thanks for your order.",
      "tags": {
          "kind": "receipt",
          "order": "1042"
      },
      "scheduled_at": "2026-09-21T09:00:00-04:00"
  }'

We check everything about the message straight away, so a problem comes back on this request rather than at send time. The response has an id starting with sched_ and the scheduled_at we recorded, in UTC. That id keeps working after the message has gone. A scheduled message does not count against the monthly allowance until it is sent.

Changing the time

PATCH /api/v1/emails/{id} with a new scheduled_at. The message keeps its id, and only one copy ever goes out.

curl -X PATCH https://reggiodigital.com/api/v1/emails/sched_123 \
  -H "Authorization: Bearer $REGGIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "scheduled_at": "2026-09-22T14:30:00-04:00"
  }'

Cancelling

DELETE /api/v1/emails/{id}. This is permanent, and the response has "status": "canceled".

curl -X DELETE https://reggiodigital.com/api/v1/emails/sched_123 \
  -H "Authorization: Bearer $REGGIO_API_KEY"

A message that has already gone out, or that we have started sending, can be neither moved nor cancelled. Moving it answers 409 with not_reschedulable, and cancelling it answers 409 with not_cancellable.

Sending a batch

POST /api/v1/emails/batch accepts up to 100 messages under emails. Each takes the same fields as a single send, except attachments and scheduled_at.

curl -X POST https://reggiodigital.com/api/v1/emails/batch \
  -H "Authorization: Bearer $REGGIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "emails": [
          {
              "from": "hello@yourdomain.com",
              "to": [
                  "success@simulator.amazonses.com"
              ],
              "subject": "Your table is ready",
              "text": "See you soon."
          },
          {
              "from": "hello@yourdomain.com",
              "to": [
                  "bounce@simulator.amazonses.com"
              ],
              "subject": "Your table is ready",
              "text": "See you soon."
          }
      ]
  }'

Every message is checked before any is accepted, so the whole batch is taken or none of it is. A problem names the message by position, as in emails.1.to. The response lists one result per message, in the order you sent them:

{
  "data": [
    { "id": "sched_981", "suppressed": [] },
    { "id": "sched_982", "suppressed": [] }
  ]
}

Each message goes out within moments. Its id starts as sched_ and works for lookups straight away.

Not sending twice

Add an Idempotency-Key header to any send you might retry. A repeat within 24 hours gets the first answer back and sends nothing. More in idempotency.

curl -X POST https://reggiodigital.com/api/v1/emails \
  -H "Authorization: Bearer $REGGIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: receipt-1042" \
  -d '{
      "from": "hello@yourdomain.com",
      "to": [
          "success@simulator.amazonses.com"
      ],
      "subject": "Your receipt",
      "html": "<p>Thanks for your order.</p>",
      "text": "Thanks for your order.",
      "tags": {
          "kind": "receipt",
          "order": "1042"
      }
  }'

Checking what happened

GET /api/v1/emails/{id} returns a message's current state for 30 days after it was sent.

curl https://reggiodigital.com/api/v1/emails/MESSAGE_ID \
  -H "Authorization: Bearer $REGGIO_API_KEY"
{
  "id": "0100019a1b2c3d4e-5f6a7b8c-1234-5678-9abc-def012345678-000000",
  "from": "hello@yourdomain.com",
  "to": ["customer@example.com"],
  "subject": "Your receipt",
  "tags": { "kind": "receipt", "order": "1042" },
  "status": "bounced",
  "status_description": "The receiving server refused this message and sent it back.",
  "scheduled_at": null,
  "sent_at": "2026-09-14T15:04:05+00:00",
  "delivered_at": null,
  "bounced_at": "2026-09-14T15:04:07+00:00",
  "complained_at": null,
  "bounce": {
    "type": "Transient",
    "subtype": "MailboxFull",
    "permanent": false,
    "summary": "Their mailbox is full.",
    "what_to_do": "Nothing is wrong with the address. It may work again once they clear space, so it is worth trying later.",
    "diagnostic": "552 5.2.2 Mailbox full"
  },
  "events": [
    { "type": "sent", "at": "2026-09-14T15:04:05+00:00", "detail": null },
    { "type": "bounced", "at": "2026-09-14T15:04:07+00:00", "detail": "552 5.2.2 Mailbox full" }
  ]
}

status is one of scheduled, queued, sent, delivered, delayed, bounced, complained, rejected, failed or canceled. Delivery is not instant: a message read back straight away usually says sent, and delivered follows within a minute or two. bounce is null unless the message bounced.

Rather than polling, most integrations use webhooks.

Testing without emailing anyone

These addresses behave in a fixed way and nothing sent to them leaves the mail network, so you can build bounce and complaint handling before a real customer is involved. They are metered and logged like any other send, and show up on the account's Sent page.

Send to What happens
success@simulator.amazonses.com Delivered.
bounce@simulator.amazonses.com A hard bounce, as if the mailbox did not exist.
complaint@simulator.amazonses.com Delivered, then marked as spam.
suppressionlist@simulator.amazonses.com Refused because the address is on a block list.

Limits

  • 120 requests a minute per account, shared with the newsletter API. See rate limits.
  • Each send and each batch returns X-Reggio-Monthly-Quota and X-Reggio-Monthly-Remaining. Every recipient counts, including cc and bcc.
  • An account that reaches its sending ceiling gets 429 with send_rejected until it is lifted.
  • 50 addresses per to, cc or bcc; 100 messages per batch; 20 attachments; 28MB per message; 30 days ahead for scheduling; 30 days of lookups.