Send Transactional Email with curl (2026): Raw HTTP to the Rovela API
A curl reference for Rovela’s Email API: POST /emails, Idempotency-Key, error shapes, GET status, POST /emails/batch, scheduled_at, and cancel.
Send transactional email with curl
curl is the fastest way to see Rovela’s Email API without opening an IDE: one POST to https://api.rovela.dev/emails, a Bearer key, and a JSON body. This post is a command-line reference — headers, idempotency, errors, reading status, batch, schedule, and cancel — for people who want to script or debug sends from a terminal.
It is deeper than Send Your First Email in 5 Minutes (the onboarding path) and it is not a language tutorial like How to Send Email in Node.js. The HTTP contract is the same one described in HTTP Email API. Start from the Email API product page, the Quick Start, the Send Email API, and Authentication.
Related product guides in this series:
- Transactional Email API
- HTTP Email API
- Migrate SMTP to an Email API
- Email API for SaaS Apps
- Transactional Email API + Hosted Mailboxes
- Inbound and Outbound Email API
- Email Sending API
- Email Notification API
- Email API Integration
- Batch Transactional Email API
- Transactional Email Webhooks
- A Simple Transactional Email API
- Send Transactional Email from Your App
- Verified Domain Email API
Prerequisites
- A verified sending domain. Unverified
fromvalues are refused with400(Quick Start, Verified Domain Email API). - An API key shaped like
re_…, created in the dashboard (shown once). Prefersending_accessfor send-only commands; you will needfull_accessforGET, reschedule, and cancel (Authentication). - curl 7.x+ with TLS. Store the key in an environment variable, not in your shell history if you can avoid it:
export ROVELA_API_KEY='re_your_sending_key' # do not commit this
export MAIL_FROM='Acme <hello@yourdomain.com>'
The basic send
curl -X POST https://api.rovela.dev/emails \
-H "Authorization: Bearer re_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@yourdomain.com>",
"to": "you@example.com",
"subject": "Hello from Rovela",
"html": "<p>It worked.</p>",
"text": "It worked."
}'
On success you get {"object":"email","id":"…"}. That is queued, not delivered. Add both html and text when you can — many clients preview the text part.
Idempotency, tags, and verification-style mail
Anything you might re-run (a script, a CI job, a flaky laptop Wi-Fi) should send Idempotency-Key (up to 256 characters, 24-hour window):
curl -X POST https://api.rovela.dev/emails \
-H "Authorization: Bearer re_your_sending_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: verify-signup-8f3a2c" \
-d '{
"from": "Acme <no-reply@yourdomain.com>",
"to": "alice@example.com",
"subject": "Your Acme code: 482913",
"html": "<p>Your verification code is</p><p style=\"font-size:28px;letter-spacing:6px\"><b>482913</b></p><p>It expires in 10 minutes.</p>",
"text": "Your verification code is 482913. It expires in 10 minutes."
}'
A retry with the same key returns the original response instead of a second code (Idempotency Keys for Email APIs, verification example).
Inspect delivery (needs full_access)
# current state
curl https://api.rovela.dev/emails/<email_id> -H "Authorization: Bearer re_your_full_access_key"
# timeline: accepted, delivered, opened, clicked, bounced, failed, …
curl https://api.rovela.dev/emails/<email_id>/events -H "Authorization: Bearer re_your_full_access_key"
A sending_access key used here gets 401. For systematic monitoring, prefer webhooks over a curl poll loop (Transactional Email Webhooks).
Batch and scheduled sends
Up to 100 personalized transactional messages:
curl -X POST https://api.rovela.dev/emails/batch \
-H "Authorization: Bearer re_your_sending_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reminders-2026-11-10" \
-d '[
{
"from": "Acme <reminders@mail.acme.com>",
"to": "alice@example.com",
"subject": "Your trial ends in 3 days",
"html": "<p>Hi Alice, your trial ends on Nov 13.</p>",
"text": "Hi Alice, your trial ends on Nov 13."
},
{
"from": "Acme <reminders@mail.acme.com>",
"to": "bob@example.com",
"subject": "Your trial ends in 3 days",
"html": "<p>Hi Bob, your trial ends on Nov 13.</p>",
"text": "Hi Bob, your trial ends on Nov 13."
}
]'
The batch example in the docs notes that the request is checked before anything is written or charged — one bad entry (unverified domain, empty body) rejects the whole batch with a 400. After acceptance, entries deliver independently; suppressed addresses in a batch are skipped (status suppressed) rather than failing the request. The batch endpoint rejects template and topic_id. See Batch and scheduled sends and Batch Transactional Email API.
Schedule with a future ISO 8601 scheduled_at including the offset:
curl -X POST https://api.rovela.dev/emails \
-H "Authorization: Bearer re_your_sending_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@mail.acme.com>",
"to": "alice@example.com",
"subject": "How is your first week going?",
"html": "<p>Hi Alice, any questions so far? Just reply to this email.</p>",
"text": "Hi Alice, any questions so far? Just reply to this email.",
"scheduled_at": "2026-11-17T09:00:00+08:00"
}'
Move or cancel while the message is still scheduled (cancel also works if it is queued but not yet handed to the mail server). Both need full_access:
curl -X PATCH https://api.rovela.dev/emails/<email_id> -H "Authorization: Bearer re_your_full_access_key" -H "Content-Type: application/json" -d '{"scheduled_at": "2026-11-18T09:00:00+08:00"}'
curl -X POST https://api.rovela.dev/emails/<email_id>/cancel -H "Authorization: Bearer re_your_full_access_key"
# {"object": "email", "id": "<email_id>", "cancelled": true}
Once the message is on its way, both return 400. More context: Scheduled Email Sending.
Errors you will actually see
Errors share a shape like {"statusCode":400,"name":"Bad Request","message":"html or text body is required"}. Surface message in your logs as-is (Quick Start).
| Status | Typical cause |
|---|---|
400 | Missing body, bad base64 attachment, from domain not in your organization, missing template, or more than 50 recipients |
401 | Missing/invalid key, or sending_access used to read, reschedule, or cancel |
429 | Rate limit. Each batch entry counts individually. Honour Retry-After if present |
FAQ
Can I pipe a file as the body? Yes — --data-binary @payload.json with Content-Type: application/json. Keep the key in a header (-H), not in the file if that file is committed.
Does curl need a special Rovela flag? No. It is ordinary HTTPS (Email API).
How do I attach a PDF? Add an attachments array item with filename, content_type, and base64 content. Building that on the CLI is awkward; the worked example is invoice attachment and Send an Invoice Email with a PDF Attachment.
Should CI use curl to mail the team? Yes, if the job is server-side and the key is a secret. Use a unique Idempotency-Key per pipeline run + purpose so retries do not double-send.
Keep curl in the toolbox
When something fails in production, a reduced curl of the same POST (with a throwaway recipient and a fresh or intentional idempotency key) is still the quickest reproduction. For application code, wrap the same contract as in Send Transactional Email from Your App.