Before you start
Accounts are set up with you during onboarding. For the DKIM record and the rest of your sending setup, see transactional email deliverability.
Send email API with cURL, step by step
The example sends a password reset email. Swap the subject, body and metadata for your own message.
1. Store your API key
Keep the key out of your code. Set it as an environment variable on the server that calls tinyRelay.
export TINYRELAY_API_KEY="your-api-key"$env:TINYRELAY_API_KEY = "your-api-key"2. Build the request body
The request body is JSON. subject, body (HTML), sender.from and recipients.to carry the email; attributes fill merge tags such as [FIRST_NAME]; metadata carries your own IDs back on every webhook.
{
"subject": "Reset your password",
"body": "<p>Hi [FIRST_NAME], use the link below to reset your Acme password.</p><p><a href=\"https://acme.com/reset?t=8f1c\">Reset password</a></p>",
"sender": {
"from": { "name": "Acme", "email": "no-reply@acme.com" },
"replyTo": { "name": "Acme Support", "email": "support@acme.com" }
},
"recipients": {
"to": [
{
"name": "Ann Lee",
"email": "ann@example.com",
"attributes": { "FIRST_NAME": "Ann" }
}
]
},
"metadata": { "user_id": "u_1042", "flow": "password_reset" }
}3. POST it to /email/campaign
Pick inline JSON for a quick test, or keep the body in a file. Single quotes stop the shell touching the JSON; the API key sits in double quotes so the variable expands.
curl -X POST "https://api.tinyemail.com/v1/relay/email/campaign" \
-H "Content-Type: application/json" \
-H "X-RELAY-ACCESS-TOKEN: $TINYRELAY_API_KEY" \
-d '{
"subject": "Reset your password",
"body": "<p>Hi [FIRST_NAME], use the link below to reset your Acme password.</p>",
"sender": { "from": { "name": "Acme", "email": "no-reply@acme.com" } },
"recipients": {
"to": [
{ "name": "Ann Lee", "email": "ann@example.com", "attributes": { "FIRST_NAME": "Ann" } }
]
},
"metadata": { "user_id": "u_1042", "flow": "password_reset" }
}'# Save the body from step 2 as reset.json, then:
curl -i -X POST "https://api.tinyemail.com/v1/relay/email/campaign" \
-H "Content-Type: application/json" \
-H "X-RELAY-ACCESS-TOKEN: $TINYRELAY_API_KEY" \
--data-binary @reset.json# Optional: hide tracked destinations inside an opaque, tamper-proof token
curl -X POST "https://api.tinyemail.com/v1/relay/email/campaign" \
-H "Content-Type: application/json" \
-H "X-RELAY-ACCESS-TOKEN: $TINYRELAY_API_KEY" \
-H "X-Link-Wrapping: true" \
--data-binary @reset.jsonBase URL from current site samples; confirm in the docs.
The command, flag by flag
| Flag | What it does here |
|---|---|
-X POST | What it does hereUses the POST method. Any other method returns 405. |
-H "Content-Type: application/json" | What it does hereTells tinyRelay the body is JSON. |
-H "X-RELAY-ACCESS-TOKEN: …" | What it does hereYour API key. The shell expands $TINYRELAY_API_KEY inside double quotes. |
-d '…' / --data-binary @file | What it does hereSends the body inline or from a file. --data-binary sends the file exactly as written. |
-i | What it does herePrints the response status line and headers, so you can see the 202. |
-H "X-Link-Wrapping: true" | What it does hereOptional. Tracked links hide the destination in an opaque, tamper-proof token. Off unless you send it. |
4. Check for a 202
Run the file variant with -i to see the status line. 202 plus a JSON body with requestId and "message": "SUCCESS" means the send was accepted. Anything else is covered under troubleshooting.
The 202 response
A request that passes validation returns 202. That means tinyRelay accepted the send; what happens to each email afterwards arrives as webhook events.
{
"requestId": "833c5151-5a98-4bad-a30f-7cc816352b7a",
"message": "SUCCESS"
}requestIdidentifies this send. Store it next to your own record.- If you did not set
campaignId, therequestIdis used as the campaign ID, and every webhook event for the send carries it. - A
202is not delivery. Watch forDELIVEREDorBOUNCEDevents.
Personalize many recipients in one cURL request
One request can send the same email to many people. Give each recipient their own attributes for the body and their own metadata for your webhooks. Here Ann's events carry {"tenant_id": "acme", "flow": "password_reset", "user_id": "u_1042"} and Bob's the same with u_2077.
{
"subject": "Reset your password",
"body": "<p>Hi [FIRST_NAME], use the link below to reset your Acme password.</p>",
"sender": { "from": { "name": "Acme", "email": "no-reply@acme.com" } },
"campaignId": "password_reset_batch_0142",
"metadata": { "tenant_id": "acme", "flow": "password_reset" },
"recipients": {
"to": [
{ "email": "ann@example.com", "attributes": { "FIRST_NAME": "Ann" }, "metadata": { "user_id": "u_1042" } },
{ "email": "bob@example.com", "attributes": { "FIRST_NAME": "Bob" }, "metadata": { "user_id": "u_2077" } }
]
}
}The rules, from the Send Email docs:
Request-level and recipient-level maps merge. The cap is on the merged map.
At most 500 recipients in one request may carry their own metadata.
Each metadata key can be up to 64 characters.
Values up to 256 characters: flat strings, numbers or booleans.
attributes are merge tags rendered into the body, such as [FIRST_NAME], and never echoed on webhooks. metadata is never rendered into the email and is always echoed on webhooks.
"attributes": { "FIRST_NAME": "Ann" } // in the email: "Hi Ann"
"metadata": { "user_id": "u_1042" } // on every webhook eventRequest-level and recipient-level maps merge, and the recipient's keys win.
// request { "tenant_id": "acme", "plan": "pro" }
// recipient { "plan": "team" }
// merged { "tenant_id": "acme", "plan": "team" }Values must be flat strings, numbers or booleans.
{ "order_id": "SO-1", "total": 42, "gift": true } // flat: fine
{ "order": { "id": "SO-1" } } // nested object: not flatMetadata is scoped to campaignId. Reusing a campaignId across batches replaces the earlier metadata, so use a unique one per batch or leave it out.
"campaignId": "password_reset_batch_0142" // unique per batch
// or leave it out: the requestId is used as the campaign IDHandle delivery webhooks
tinyRelay posts an event to your webhook endpoint for each thing that happens to an email. There are seven event types:
- SENDSent to the receiving server
- DELIVEREDEmail delivered
- BOUNCEDHARD_BOUNCED or SOFT_BOUNCED · adds bounceCategory
- OPENOpened by the recipient
- CLICKA tracked link was clicked · adds url
- UNSUBSCRIBEUnsubscribe link clicked
- SPAMMarked as spam
SEND: the email was sent. Apps usually mark the message as sent against their record.
Each event carries eventType, receiver, requestId, campaignId and your merged metadata. BOUNCED events add bounceCategory, and CLICK events add the url that was clicked.
svix-id, svix-timestamp and svix-signature headers and delivered at least once, so verify each request and deduplicate on svix-id.Read email webhooks and delivery events, then verify webhook signatures. Bounce categories are explained in the email bounce codes reference; field-level detail is in the webhook docs.
Troubleshooting
The Send Email endpoint documents four error statuses. Match the status you get to the row below.
| Status | Meaning | What to check |
|---|---|---|
400 | MeaningBad Request | What to checkThe body failed validation. The response carries a message field; read it, fix the field it names and resend. |
401 | MeaningAPI key missing or invalid | What to checkCheck the header is named X-RELAY-ACCESS-TOKEN and holds the key issued at onboarding. If the key is right, confirm the server you are calling from has an IP on your allowlist. |
405 | MeaningMethod Not Allowed | What to checkSend a POST to /email/campaign. Other methods are refused. |
412 | MeaningMetadata limits broken | What to checkThe merged metadata map has more than 10 keys, or more than 500 recipients carry their own metadata. Nothing is sent; trim the metadata and resend. |
Full error schemas are in the Send Email API reference.
FAQ
Status 202 with a JSON body containing a requestId and the message SUCCESS. It means tinyRelay accepted the send; delivery results arrive later as webhook events.
In the X-RELAY-ACCESS-TOKEN request header. Keep it in an environment variable rather than typing it into commands that end up in shell history.
Yes. Add more objects to recipients.to. Each can have its own attributes for merge tags and its own metadata, and up to 500 recipients per request can carry their own metadata.
Set disableTrackingLinks to true in the body. It switches off both link rewriting and the open pixel for the whole request; there is no way to turn off only one of them.
Next steps
- Transactional email API overview
- Send email with Node.js
- Send email with Python
- Verify webhook signatures
- Send Email API reference (docs)