Skip to content

Explore CallMissed

Transactional Email API

Transactional email on your own letterhead

Verify a domain you own once, then send, schedule and receive DKIM-signed mail with the cm_ API key you already have — logged end to end and honest about what accepted means.

Authorization: Bearer cm_… → POST https://api.callmissed.com/api/v1/email/send → 202 Accepted

The essentials

One cm_ key
The key you already hold. Toggle the email permission on it; no second credential.
202 + suppressed[]
Accepted sends answer 202 and name the recipients your suppression list dropped.
DKIM on your domain
Your domain signs every message once its two delegated keys verify.
₹30 per 1,000
Per recipient, from the shared credit balance. Rejected sends cost nothing.

Four records to publish, one call to verify

POST /domains answers with the exact DNS set: an ownership token, SPF, and two CNAMEs that delegate DKIM signing to us — rotation included. Publish them, then call verify. All four checks must pass before the domain sends.

  • TXT

    @→cmv-verify=9f3ac2…

    A one-off token proving you control the domain

  • TXT

    @→v=spf1 include:… ~all

    Authorises our sending infrastructure for the domain

  • CNAME

    cm1._domainkey→…dkim1…

    Delegates the DKIM signing key for the domain

  • CNAME

    cm2._domainkey→…dkim2…

    A second delegated DKIM key, used for rotation

  • MX

    @→…inbound…

    Routes inbound mail — only needed to receive · optional

Simulated example — record values are abbreviated; yours are generated per domain and re-fetchable with GET /domains/{id}/records.

POST /api/v1/email/domains/{id}/verify

  • Ownership token found
  • SPF authorises sending
  • DKIM key delegated
  • Rotation key delegated

Simulated example. A send from any other local part registers it and retries; that can briefly surface as 503 sender_propagating.

Verification registers donotreply@your-domain as a sender, so it works the moment the checks pass. Other local parts are registered via the senders endpoint — or automatically on first use, which can briefly answer 503 sender_propagating while it propagates.

The same four records, in the console

Add the domain you send from, publish the records it hands you, verify, and follow each email in the Sends log as it is delivered.

Demo: adding and verifying a sending domain from its DNS records, then the Sends log filling in

Compose once, substitute at send time

Save a subject and body with {{ params.KEY }} placeholders, then pass the values per send — or per recipient set in a batch. Try it: edit a value and the letter re-inks.

Substitution values

params

"params": {
  "name": "Ada",
  "order_id": "1043",
  "amount": "₹1,240"
}

Simulated example. A key with no value renders empty; a {{ contact.* }} reference is refused outright as 422 unresolvable_template_vars.

Rendered at send time

Subject

Your receipt, Ada

Hi Ada, your order 1043 is confirmed. Total: ₹1,240.

  • name
  • order_id
  • amount

Substituted values are HTML-escaped, inserted once and never re-scanned — a param that itself contains {{ … }} stays literal. Explicit subject, body or sender fields on the send override the template.

From POST to postmark

Walk one send down the rail: the request, the 202, the DKIM signature, and the feedback that comes back — including the recipient who never makes it past the suppression list.

Simulated example — the full sequence is on the page below; play re-walks it in order.

  1. Request

    POST /api/v1/email/send

    Three recipients on one send. [email protected] is already on your suppression list.

    request body
    {
      "from": "Acme Ops <[email protected]>",
      "to": [
        "Ada <[email protected]>",
        "Bo <[email protected]>",
        "[email protected]"
      ],
      "subject": "Your receipt",
      "text": "Thanks for your order.",
      "reply_to": "[email protected]"
    }
  2. Response

    202 Accepted

    status: sent means accepted for delivery — not delivered. suppressed[] names who was dropped before anything was attempted.

    response · 202 Accepted
    {
      "id": "9d0f8b3a-1c2e-4a5b-…",
      "message_id": "<[email protected]>",
      "messageId": "<[email protected]>",
      "messageIds": ["<[email protected]>"],
      "status": "sent",
      "suppressed": ["[email protected]"]
    }

    Accepted is not delivered

    202 means the message passed validation, suppression and quota checks and was accepted for delivery. What the recipient's server does with it comes back later — as send-log statuses and webhook events, not in this response.

  3. Signing

    DKIM-signed, handed to delivery

    The message is signed with the verified From domain’s key, then handed off.

    DKIM-Signature: v=1; d=acme.com; s=cm1; bh=…; b=…

  4. Feedback

    Delivery reports back

    ada@ is marked delivered in the send log. bo@ bounces — a signed email.bounced webhook fires and the address joins your suppression list.

    • [email protected]delivered

      Send log row shows status: delivered, set from delivery feedback.

    • [email protected]bounced

      email.bounced webhook fires; the address is auto-added to suppressions.

    • [email protected]suppressed

      Dropped before sending, returned in suppressed[], never billed.

    webhook delivery — signed, logged per attempt
    POST https://your-app.com/hooks/email
    X-CallMissed-Signature: sha256=…
    X-CallMissed-Event: email.bounced
    
    {
      "type": "email.bounced",
      "data": {
        "message_id": "<[email protected]>",
        "recipient": "[email protected]",
        "detail": "550 5.1.1 recipient address rejected"
      }
    }

Send it at a time, not just at once

Add scheduledAt and the send waits up to 72 hours out — enqueued now, billed and delivered when it fires, cancellable while pending.

For many recipients at once, messageVersions carries up to 1,000 recipient sets in one call — each overriding subject, body or params — and the deduplicated union is billed once. An address claimed by an earlier version is simply not sent twice.

Simulated example — slots are computed from your clock. The API takes a UTC timestamp that is in the future and within 72 hours.

POST /api/v1/email/send · body excerpt
{
  "from": "Acme Ops <[email protected]>",
  "to": ["Ada <[email protected]>"],
  "subject": "Reminder",
  "text": "Your appointment is tomorrow.",
  "scheduledAt": "2026-07-28T09:00:00Z"
}
response · 202 Accepted · example timestamp
{
  "id": "3f1c9a2d-4b5e-6a7f-…",
  "status": "scheduled",
  "scheduledAt": "2026-07-28T09:00:00Z",
  "batchId": "7a2b1c0d-9e8f-4a5b-…",
  "suppressed": []
}

Nothing is billed or delivered at enqueue time — both happen when it fires. Inspect pending rows with GET /scheduled/{id-or-batchId} and cancel with DELETE before it fires; a cancelled send is never charged.

A worn wooden pigeonhole mail organiser with kraft envelopes slotted into its compartments

Claim a pigeonhole, read what arrives

Publish the MX record on a verified domain and addresses like support@your-domain start receiving. Mail to anything unclaimed is refused at the door — there is no catch-all.

Claim the addressPOST /api/v1/email/inbound/addresses
Pick a mailbox name on a verified domain — support, replies, anything up to 64 chars. Addresses are globally unique; an already-claimed name answers 409.
Forward to your appforward_url on the address
Every message that lands is POSTed to your endpoint as JSON — stored first, then forwarded, so a down endpoint never loses mail.
Read it backGET /api/v1/email/inbound/messages/{id}
Forwards are unsigned, so treat them as a knock on the door: confirm anything you act on by re-reading the message with your key.
An open ruled mail ledger with a fountain pen resting across the page and blank postage stamps beside it

Every send is a ledger line

What happened after the hand-off is written down: a send log with real statuses, a suppression list that maintains itself, engagement metrics per window, and signed webhooks for the events that matter.

GET /api/v1/email/sends · Simulated example

№RecipientSubjectStatusLogged
1043[email protected]Your receiptdelivered09:41
1042[email protected]Your receiptbounced09:41
1041list · 240 recipientsMarch invoice rundelivered09:12
1040[email protected]Appointment reminderqueuedscheduled
1039[email protected]Welcome notecomplained08:57
1038—Missing senderrejected08:30
Send logGET /api/v1/email/sends
One row per message — queued, sent, delivered, bounced, complained, rejected or failed — filterable by status, recipient, message id and a ≤90-day window.
SuppressionsGET /api/v1/email/suppressions
Hard bounces and complaints land here automatically, singly or in batches of 100. Suppressed addresses are dropped pre-send and returned in suppressed[].
Engagement metricsGET /api/v1/email/emails/metrics
One aggregate row per window: sent, delivered, bounced, complained, opens and clicks with the rates derived for you — tracked subsets handled honestly.
Webhook eventsPOST /api/v1/email/webhooks
Bounce and complaint events POST to your endpoint, HMAC-SHA256 signed over the raw body, retried 3 times, and every attempt is in the delivery log.

Three ceilings, four error shapes

Requests per minute, recipients per calendar month, and a per-domain daily quota that warms up on clean volume — a refusal always names which one you hit. Error bodies come in four shapes; branch on the HTTP status first.

PlanSend rateRecipients / monthDaily-quota ceiling
Free10 requests / min2,000200
Starter60 requests / min50,0005,000
Pro300 requests / min1,000,00050,000
Enterprise1,000 requests / minUnlimited500,000

The enforced daily figure is the domain’s, not the plan’s: a newly verified domain starts at 200 sends a day and climbs 200 → 1,000 → 5,000 → 20,000 → 50,000, one step per clean-sending day, then doubling. Too many bounces or complaints pauses the domain itself — that breaker stays on until the list quality is fixed.

  • Send rejection

    reason nested under detail — domain_not_verified, quota_exceeded, sender_propagating and friends.

    {
      "detail": {
        "error": "acme.com has not completed DNS verification",
        "reason": "domain_not_verified"
      }
    }
  • 502 relay_failed

    Flat shape, not wrapped in detail, and it carries the send id for the log.

    {
      "error": "The message could not be accepted for delivery",
      "reason": "relay_failed",
      "id": "9d0f8b3a-1c2e-4a5b-…"
    }
  • Auth, not-found, conflict

    detail is a plain string with no reason — 401s, 404s, 409s, sender-route errors.

    { "detail": "Domain not found" }
  • Schema validation

    detail is an array — rejected before the send pipeline runs, before any charge.

    {
      "detail": [{
        "type": "value_error",
        "loc": ["body"],
        "msg": "invalid email address"
      }]
    }

Franking, by the thousand

Thirty credits — thirty rupees — per thousand recipients, from the same balance your model calls draw on. No per-mailbox fees, no sending-tier subscription, no separate invoice.

See the full pricing
Credits charged
750
In rupees
₹750
Approx. USD
$7.81

Simulated example at the published rate — 30 credits (₹30) per 1,000 emails, about 3 paise per recipient, drawn from the same credit balance as every other API. Only accepted sends are billed. The 1,000-credit signup bonus alone covers roughly 33,333 emails.

Three calls, end to end

The whole service answers to the cm_ key with the email permission on — the same header you send for model calls. Brevo-shaped payloads are accepted unchanged.

Register & verify a domain

POST /api/v1/email/domains → POST /domains/{id}/verify

# 1. Add the domain — the response carries the DNS records to publish
curl -X POST https://api.callmissed.com/api/v1/email/domains \
  -H "Authorization: Bearer cm_your_key" \
  -H "Content-Type: application/json" \
  -d '{"domain": "acme.com"}'

# 2. Publish the records, then verify (repeat until verified is true)
curl -X POST https://api.callmissed.com/api/v1/email/domains/{domain_id}/verify \
  -H "Authorization: Bearer cm_your_key"

Verification registers donotreply@your-domain as a sender and turns on DKIM signing.

Send from a template

POST /api/v1/email/send

curl -X POST https://api.callmissed.com/api/v1/email/send \
  -H "Authorization: Bearer cm_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "9d0f8b3a-1c2e-4a5b-8f7d-6e2a1b0c9d4e",
    "to": ["Ada <[email protected]>"],
    "params": { "name": "Ada", "order_id": "1043" },
    "reply_to": "[email protected]"
  }'

params fills {{ params.KEY }} placeholders in the saved subject and bodies; explicit send fields override the template.

Schedule or batch a send

POST /api/v1/email/send

curl -X POST https://api.callmissed.com/api/v1/email/send \
  -H "Authorization: Bearer cm_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme Ops <[email protected]>",
    "to": ["Ada <[email protected]>"],
    "subject": "Reminder",
    "text": "Your appointment is tomorrow.",
    "scheduledAt": "2026-07-28T09:00:00Z"
  }'

scheduledAt accepts a UTC timestamp up to 72 hours out. Cancel a pending send with DELETE /api/v1/email/scheduled/{id} — never charged.

Postmarked questions

Do I need a separate email API key?
No. The Email API uses the same cm_ key as every other CallMissed API — toggle the email permission on for the key in the console. No separate sender account, no second credential.
What does a 503 sender_propagating mean?
Verification registers exactly one sender, donotreply@your-domain. Any other local part has to be registered — up front via the senders endpoint, or automatically when the send path retries a refused send. Registration is eventually consistent, so a first send from a brand-new address can briefly return 503 sender_propagating. Retry in a minute or two; nothing else is needed.
Is a 202 the same as delivered?
No. 202 means accepted for delivery: the message was validated, suppression-filtered, DKIM-signed and handed off. Whether it actually reached a mailbox comes back later — as send-log statuses (delivered, bounced, complained) and as signed webhook events. We make no inbox-placement promise: delivery depends on recipient servers and your domain reputation.
What does email cost?
30 credits — ₹30 — per 1,000 emails, billed per recipient from the same credit balance as every other API. Only accepted sends are billed; rejections and failed relays cost nothing. New accounts get 1,000 free signup credits.
Can I schedule sends or send to a list?
Both. Add a scheduledAt timestamp (future, within 72 hours) and the send is enqueued — nothing is billed or delivered until it fires, and a pending send can be cancelled. For many recipient sets in one call, messageVersions carries up to 1,000 versions; the deduplicated union of recipients is billed once.
Can I receive email too?
Yes. Publish the MX record on a verified domain, claim addresses like support@your-domain, then read received mail over the API or POST it to your app with a per-address forward_url. Mail to unclaimed addresses is refused — there is no catch-all.
A neat stack of kraft envelopes tied with twine on a linen desk pad beside a brass paper knife

Put your domain on the envelope

Add the domain, publish four records, and the next receipt your app sends leaves under your own signature — DKIM-signed and logged. The first thousand sends come out of your signup credits.