Skip to main content

Features

Scheduled Sending

Add a timestamp to a normal send and the message waits. Group a campaign under one batch id, then reschedule or cancel the whole thing before it goes out.

A timestamp on a normal send

Add scheduled_at to the send request you already make, and the message is stored instead of transmitted. There is no second endpoint to learn.

curl -X POST https://api.mailkube.com/mta/v1/emails \
  -H "Authorization: Bearer mk_<key_id>_<secret>" \
  -H "Content-Type: application/json" \
  -H "User-Agent: acme-billing/1.4" \
  -d '{
    "from": "Acme <hello@yourdomain.com>",
    "to": "customer@example.com",
    "subject": "Your weekly digest",
    "html": "<h1>This week at Acme</h1><p>Here is what you missed...</p>",
    "scheduled_at": "2026-08-20T09:00:00+02:00",
    "batch_id": "weekly-digest"
  }'

The timestamp is ISO-8601 and has to carry an offset. A naive one is refused rather than guessed at. The same 09:00 is two different instants depending on who wrote it. Any valid offset is accepted and normalized to UTC on the way in, and the time has to be in the future and within 30 days.

What comes back says scheduled rather than sent, and carries the id you manage it by.

Moving it, or calling it off

PATCH /scheduled-emails/{id} changes the time. DELETE on the same path cancels. Both work on a whole campaign through PATCH and DELETE /scheduled-emails/batches/{batch_id}. That is the call a slipped launch actually needs, and the response tells you how many rows moved.

Content is immutable once scheduled. To change what the message says, cancel it and schedule a new one.

Batches and listing

A batch_id is a grouping label. Each message is still its own request. The label gives cancel-all and reschedule-all something to address, and it is only accepted alongside scheduled_at.

Listing covers pending, canceled and failed sends inside a rolling 31-day window. Filtering by status=sent is rejected outright as a validation_error. A sent message has already moved to your delivery logs.

Webhooks

email.scheduled fires when the send is queued, email.sent at the moment of transmission, and email.failed if a due-time check dropped it. Subscribe to all three and a scheduled message’s whole life arrives on your endpoint as it happens.

stateDiagram-v2
    accTitle: The states a scheduled message moves through
    accDescr: A send carrying scheduled_at enters the scheduled state and emits email.scheduled. From there the due time is reached and it is transmitted, emitting email.sent; or you cancel it before the due time and it becomes canceled; or a due-time check drops it and it emits email.failed.
    [*] --> scheduled: POST /emails with scheduled_at (email.scheduled)
    scheduled --> sent: due time reached (email.sent)
    scheduled --> canceled: DELETE before the due time
    scheduled --> failed: dropped at the due time (email.failed)
    sent --> [*]
    canceled --> [*]
    failed --> [*]

Setting an endpoint up, the signature headers and the retry behaviour are all on the webhooks page.

Plans and the scheduling guide

Scheduled sending is on the paid plans, and a send carrying scheduled_at without the entitlement comes back as scheduling_not_included. Pricing has the plan list, and the scheduled sending guide(opens in a new tab) documents every filter, endpoint and error name.