ScheduleKaro Email
Send receipts, sign-in codes and campaigns from your own domain. One HTTP call, delivery and bounce tracking included, and a suppression list that is enforced for you rather than left to your code.
/api/email/v1/send with the key in an Authorization: Bearer header.curl https://schedulekaro.com/api/email/v1/send \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": "customer@example.com",
"subject": "Your order is on its way",
"html": "<p>Thanks for your order.</p>"
}'Every response is a JSON object with an ok field. On failure it carries a stable error.code as well as a sentence - write your code against the code, not the sentence, because the sentence is written for a human and may improve.
| Code | HTTP | What it means |
|---|---|---|
BAD_REQUEST | 400 | A field is missing or the wrong shape. The detail says which. |
UNAUTHORIZED | 401 | The API key is missing, revoked or expired. |
FORBIDDEN | 403 | The key is valid but does not have the scope this call needs. |
NOT_FOUND | 404 | No message, template or domain with that identifier. |
PAYLOAD_TOO_LARGE | 413 | The request body is over 256 KB. |
RATE_LIMITED | 429 | Too many calls. Retry after a short pause. |
QUOTA_EXCEEDED | 402 | The monthly allowance and the credit balance are both spent. |
SUPPRESSED | 403 | The recipient is on a suppression list and was not emailed. |
DOMAIN_NOT_VERIFIED | 403 | The From domain is not verified for this account. |
DOMAIN_PAUSED | 403 | Sending from that domain is stopped. The dashboard says why. |
DAILY_LIMIT | 429 | The domain has sent as much as its warm-up allows today. |
MISSING_VARIABLES | 400 | A template needed values that were not in `data`. |
SERVER_ERROR | 500 | Our fault. Safe to retry. |
SERVICE_UNAVAILABLE | 503 | The email service is not open yet. Retry once it launches. |
send — Send email and verification codes.read — Read message status, domains and suppressions, and check whether an address is deliverable.suppressions:write — Add and remove suppressed addresses.workflows:write — Put a contact into an automated workflow.Calls are limited per key and per IP address. A RATE_LIMITED reply is safe to retry after a pause. Separately, each verified domain has a daily sending limit that rises as its reputation holds - a new domain starts small on purpose.
Every send is checked against the suppression list, your quota and the verified From domain before it reaches Amazon SES.
/api/email/v1/sendscope: sendThe From address must be on a domain you have verified. Marketing mail obeys unsubscribes; transactional mail is never blocked by one.
fromstring · required | Address on a verified domain. "Name <hello@yourdomain.com>" is accepted. |
tostring · required | Recipient address. |
subjectstring · required | Subject line. |
htmlstring | HTML body. At least one of html or text is required. |
textstring | Plain text body. |
replyTostring | Where replies should go, if not the From address. |
kindstring | "transactional" (default) or "marketing". |
curl -X POST https://schedulekaro.com/api/email/v1/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": "customer@example.com",
"subject": "Your order is on its way",
"html": "<p>Thanks for your order.</p>"
}'{
"ok": true,
"data": {
"id": "6f2f5f0c-2d1e-4f6f-9f1a-6f9a2d3b4c5d",
"providerMessageId": "0100019...-000000"
}
}/api/email/v1/send-templatescope: sendRenders the published version of a template you saved in the dashboard. A template that needs a value you did not supply is refused rather than sent with a blank in it.
templatestring · required | The template's name, e.g. order-receipt. |
fromstring · required | Address on a verified domain. |
tostring · required | Recipient address. |
dataobject | Values for the template's variables. |
replyTostring | Where replies should go. |
kindstring | "transactional" (default) or "marketing". |
curl -X POST https://schedulekaro.com/api/email/v1/send-template \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "order-receipt",
"from": "billing@yourdomain.com",
"to": "customer@example.com",
"data": {
"first_name": "Asha",
"total": "₹1,240"
}
}'{
"ok": true,
"data": {
"id": "6f2f5f0c-2d1e-4f6f-9f1a-6f9a2d3b4c5d",
"providerMessageId": "0100019...-000000",
"template": "order-receipt",
"templateVersion": 3
}
}/api/email/v1/send-batchscope: sendEach entry is sent on its own merits and reported on its own line, so one suppressed address does not fail the rest. Always returns 200 - read the entries, not the status code.
fromstring | Default From for every entry that does not set its own. |
kindstring | Default kind for every entry. |
messagesarray · required | Up to 50 entries. Each takes the same fields as /send, or a template and data. |
curl -X POST https://schedulekaro.com/api/email/v1/send-batch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"messages": [
{
"to": "a@example.com",
"subject": "Hi",
"html": "<p>Hi</p>"
},
{
"to": "b@example.com",
"template": "welcome",
"data": {
"first_name": "Ravi"
}
}
]
}'{
"ok": true,
"data": {
"accepted": 1,
"rejected": 1,
"results": [
{
"index": 0,
"to": "a@example.com",
"ok": true,
"id": "6f2f5f0c-..."
},
{
"index": 1,
"to": "b@example.com",
"ok": false,
"error": {
"code": "SUPPRESSED",
"detail": "This address unsubscribed."
}
}
]
}
}A one-time code to prove somebody owns the address they typed. One live code per address: asking again replaces the last one rather than leaving several valid.
/api/email/v1/verification/startscope: sendSends a six-digit code and returns when it expires. The code itself is never returned - only the person holding the inbox learns it.
fromstring · required | Address on a verified domain. |
emailstring · required | Address to verify. |
brandstring | Your name, as it should read in the mail. |
subjectstring | Override the default subject. |
curl -X POST https://schedulekaro.com/api/email/v1/verification/start \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"email": "customer@example.com",
"brand": "Kiran Tea Co."
}'{
"ok": true,
"data": {
"email": "customer@example.com",
"expiresAt": "2026-09-01T09:25:00.000Z",
"messageId": "6f2f5f0c-..."
}
}/api/email/v1/verification/checkscope: sendA wrong code counts an attempt; the challenge is spent after five. A correct one can only be used once.
emailstring · required | The address the code was sent to. |
codestring · required | What the person typed. |
curl -X POST https://schedulekaro.com/api/email/v1/verification/check \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"code": "418302"
}'{
"ok": true,
"data": {
"email": "customer@example.com",
"verified": true
}
}What happened to everything you sent.
/api/email/v1/messagesscope: readNewest first, paged on a creation-time cursor. Pass the response's nextBefore as `before` to get the next page; it is null when there is nothing older.
limitnumber | 1-100. Defaults to 50. |
statusstring | queued, sent, delivered, bounced, complained, rejected or failed. |
tostring | Only messages to this address. |
beforestring | ISO 8601 timestamp - return messages older than this. |
curl -X GET https://schedulekaro.com/api/email/v1/messages \ -H "Authorization: Bearer YOUR_API_KEY"
{
"ok": true,
"data": {
"messages": [
{
"id": "6f2f5f0c-...",
"to": "customer@example.com",
"subject": "Your order is on its way",
"status": "delivered",
"createdAt": "2026-09-01T09:15:00.000Z"
}
],
"nextBefore": null
}
}/api/email/v1/messages/{id}scope: readThe whole story of one send - delivered, opened, clicked, bounced - in a single call, so you can answer "did it arrive?" without building a webhook first.
curl -X GET https://schedulekaro.com/api/email/v1/messages/{id} \
-H "Authorization: Bearer YOUR_API_KEY"{
"ok": true,
"data": {
"id": "6f2f5f0c-...",
"status": "delivered",
"openedAt": "2026-09-01T09:31:00.000Z",
"events": [
{
"type": "send",
"occurredAt": "2026-09-01T09:15:00.000Z"
},
{
"type": "delivery",
"occurredAt": "2026-09-01T09:15:04.000Z"
},
{
"type": "open",
"occurredAt": "2026-09-01T09:31:00.000Z"
}
]
}
}Delivery and engagement by day, read from a rollup that is recomputed every fifteen minutes. Cheap to poll, and the same numbers the dashboard shows.
/api/email/v1/statsscope: readTotals and a per-day series for a date range, optionally for one sending domain. Days are IST calendar days, the day the mail was sent - a bounce that arrives the next morning is counted against the day of the send. Rates are percentages: delivery and bounce against what Amazon SES accepted, the rest against what was delivered.
fromstring | First day, YYYY-MM-DD. Defaults to 29 days ago. |
tostring | Last day, YYYY-MM-DD. Defaults to today. At most 366 days in a range. |
domainstring | Only mail sent from this verified domain. |
curl -X GET https://schedulekaro.com/api/email/v1/stats \ -H "Authorization: Bearer YOUR_API_KEY"
{
"ok": true,
"data": {
"range": {
"from": "2026-08-05",
"to": "2026-09-03",
"days": 30
},
"domain": null,
"totals": {
"sent": 12480,
"accepted": 12475,
"delivered": 12310,
"opened": 5104,
"clicked": 918,
"bounced": 140,
"complained": 21,
"unsubscribed": 46,
"failed": 5,
"rejected": 0,
"rates": {
"delivery": 98.68,
"open": 41.46,
"click": 7.46,
"bounce": 1.12,
"complaint": 0.17,
"unsubscribe": 0.37
}
},
"daily": [
{
"day": "2026-09-03",
"sent": 420,
"accepted": 420,
"delivered": 415,
"opened": 190,
"clicked": 33,
"bounced": 5,
"complained": 0,
"unsubscribed": 2,
"failed": 0,
"rejected": 0,
"rates": {
"delivery": 98.81,
"open": 45.78,
"click": 7.95,
"bounce": 1.19,
"complaint": 0,
"unsubscribe": 0.48
}
}
]
}
}Addresses that must not be emailed. Scope matters: "marketing" stops campaigns but still lets a receipt or a sign-in code through, which is what an unsubscribe usually means.
/api/email/v1/suppressionsscope: readNewest first.
limitnumber | 1-500. Defaults to 100. |
curl -X GET https://schedulekaro.com/api/email/v1/suppressions \ -H "Authorization: Bearer YOUR_API_KEY"
{
"ok": true,
"data": {
"suppressions": [
{
"email": "gone@example.com",
"scope": "all",
"reason": "bounce",
"createdAt": "2026-08-30T11:02:00.000Z"
}
]
}
}/api/email/v1/suppressionsscope: suppressions:writeSuppressing an address again widens or refreshes it rather than failing.
emailstring · required | Address to stop emailing. |
scopestring | "marketing" (default) or "all". |
detailstring | Your own note about why. |
curl -X POST https://schedulekaro.com/api/email/v1/suppressions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "gone@example.com",
"scope": "all"
}'{
"ok": true,
"data": {
"email": "gone@example.com",
"scope": "all"
}
}/api/email/v1/suppressionsscope: suppressions:writeOnly removes an entry you added yourself. A bounce or a complaint is not yours to overrule, and an unsubscribe was the recipient's decision.
emailstring · required | Address to release. May also be passed as ?email=. |
curl -X DELETE https://schedulekaro.com/api/email/v1/suppressions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "asked-again@example.com"
}'{
"ok": true,
"data": {
"email": "asked-again@example.com",
"removed": true
}
}Check an address before you accept it. Catching a typo on a sign-up form saves an account that can never be confirmed, a support ticket, and a bounce against your sending domain.
/api/email/v1/validatescope: readFour checks: the shape of the address, whether the domain is a throwaway mailbox service, whether it is a shared desk address like info@, and whether the domain can receive mail at all. "invalid" means it cannot be delivered to and sending would bounce; "risky" means it will accept mail and probably is not worth sending; "pending" means DNS could not be reached and the answer is unknown - retry rather than reject. It does not connect to the mailbox, so "valid" means nothing is wrong with the address, not that somebody is reading it. Sends nothing and costs no quota.
emailstring | One address to check. Use this or `emails`. |
emailsarray | Up to fifty addresses. The response carries a `results` array in the same order. |
curl -X POST https://schedulekaro.com/api/email/v1/validate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "priya@acme.in"
}'{
"ok": true,
"data": {
"email": "priya@acme.in",
"result": "valid",
"reason": "ok",
"detail": "Nothing wrong with this address.",
"deliverable": true
}
}The domains you may send from, and the state of their DNS.
/api/email/v1/domainsscope: readIncludes the DKIM, SPF, DMARC and MAIL FROM state, the health score, and the DNS records we last read at your nameservers.
curl -X GET https://schedulekaro.com/api/email/v1/domains \ -H "Authorization: Bearer YOUR_API_KEY"
{
"ok": true,
"data": {
"domains": [
{
"domain": "yourdomain.com",
"verificationStatus": "verified",
"dkimStatus": "verified",
"spfStatus": "verified",
"dmarcStatus": "pending",
"healthScore": 85,
"dailyLimit": 1000,
"paused": false
}
]
}
}Automated sequences you design in the dashboard - a wait, an email, a condition - started by your own application.
/api/email/v1/workflowsscope: readEvery workflow on the account, with what starts it and whether it is live. Use it to find the id the enroll call needs.
limitnumber | 1-200. Defaults to 50. |
curl -X GET https://schedulekaro.com/api/email/v1/workflows \ -H "Authorization: Bearer YOUR_API_KEY"
{
"ok": true,
"data": {
"workflows": [
{
"id": "8f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"name": "Welcome series",
"status": "active",
"trigger": "api",
"enrolled": 412,
"completed": 297
}
]
}
}/api/email/v1/workflows/{id}/enrollscope: workflows:writeThe person must already be one of your contacts and must have confirmed their subscription - this is not a way to add somebody to a list. Enrolling the same person twice does nothing unless the workflow allows re-entry, in which case they start again from the first step.
emailstring · required | The contact to enroll. |
curl -X POST https://schedulekaro.com/api/email/v1/workflows/{id}/enroll \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "asha@example.com"
}'{
"ok": true,
"data": {
"workflowId": "8f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"email": "asha@example.com",
"outcome": "enrolled"
}
}Rather than polling, point a URL at your server in the dashboard and we post every delivery, bounce, complaint, open, click and unsubscribe to it as JSON. Each request is signed with your endpoint secret, and a delivery your server missed is retried rather than dropped.
Open the email dashboard