Cresca's transactional email API is a REST API at https://api.cresca.xyz/api/v1 that sends one-to-one email, such as password resets, receipts and verification codes, from your own verified domain. You authenticate with an API key in an Authorization: Bearer header, POST JSON to /emails, and get back an id you can use to check delivery. This guide covers every v1 endpoint, with a full worked example and code you can copy.
In this guide
- Every endpoint at a glance
- Authentication: API keys and the Bearer header
- Worked example: your first email, end to end
- Emails API: send, batch, list, reschedule, cancel
- Templates API: create, publish and send by alias
- Contacts API
- Idempotency, lifecycle, suppression, limits and errors
- Frequently asked questions
All requests go to https://api.cresca.xyz/api/v1 over HTTPS, and requests and responses are JSON. There is no SDK package to install: any HTTP client works, so the examples below use cURL, Node.js 18+ (built-in fetch) and Python (requests). Field names follow the conventions of popular email APIs, so an existing integration is mostly a change of base URL and key.
The API is for transactional email: messages a person expects because of something they did. Newsletters and promotions belong in Cresca campaigns. If the line is unclear, read transactional vs marketing email.
Every endpoint at a glance
Paths are relative to the base URL. "Sending" means a sending-access key works; "Full" needs a full-access key.
| Method | Path | Purpose | Key | Docs |
|---|---|---|---|---|
| POST | /emails | Send one email (up to 50 recipients) | Sending | Send email |
| POST | /emails/batch | Send up to 100 emails in one request | Sending | Send batch |
| GET | /emails/:id | Retrieve one email and its last_event | Full | Retrieve email |
| GET | /emails | List emails, newest first | Full | List emails |
| PATCH | /emails/:id | Reschedule a scheduled email | Full | Update email |
| POST | /emails/:id/cancel | Cancel a scheduled or queued email | Full | Cancel email |
| POST | /templates | Create a template draft | Full | Create template |
| GET | /templates/:id | Retrieve a template by id or alias | Full | Retrieve template |
| GET | /templates | List templates | Full | List templates |
| PATCH | /templates/:id | Update the draft | Full | Update template |
| POST | /templates/:id/publish | Publish the draft as the next version | Full | Publish template |
| DELETE | /templates/:id | Delete a template | Full | Delete template |
| POST | /contacts | Add up to 1,000 contacts to a list | Full | Add contacts |
Authentication: API keys and the Bearer header
Create keys on the API keys page of the Cresca app. Keys start with cr_live_ and are shown once; Cresca stores only a hash. Send the key as a bearer token:
Authorization: Bearer cr_live_xxxxxxxxx The X-API-Key header also works. Keep keys on your server, never in browser code, URLs or public repositories.
| Permission | Can do | Use it for |
|---|---|---|
| Sending access | Send emails and batches. Can be limited to one verified domain. | Production servers |
| Full access | Everything: send, read and cancel emails, manage templates, add contacts | Scripts and internal tools |
Authentication errors: 401 missing_api_key (no key), 403 invalid_api_key (unknown or revoked key, or suspended account) and 401 restricted_api_key (a sending-access key called a full-access endpoint). Details are on the Authentication docs page.
Worked example: send your first transactional email
This is the whole path from a fresh account to a delivered email. It mirrors the quickstart.
Step 1: Verify a sending domain
In the app, open Sending Domains, add your domain and publish the DNS records Cresca shows you: DKIM, a custom MAIL FROM domain and an ownership record. If your DNS is on Cloudflare, Cresca can publish them for you. The domain becomes active once the records verify. A subdomain such as mail.acme.com keeps the reputation of app email separate from campaigns. See sending domains and SPF, DKIM and DMARC done right.
Step 2: Add a sender
On the same page, create a sender such as hello@acme.com with a display name and an optional Reply-To. Every API from address must be an active sender on a verified domain.
Step 3: Create an API key
Create a Sending access key for your server and store it as CRESCA_API_KEY. Create a separate full-access key for the status check in step 5.
Step 4: Send the email
The Idempotency-Key header makes the request safe to retry.
curl -X POST 'https://api.cresca.xyz/api/v1/emails' \ -H 'Authorization: Bearer cr_live_xxxxxxxxx' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: password-reset/reset-request-9f2c' \ -d '{ "from": "Acme <hello@acme.com>", "to": "user@example.com", "subject": "Reset your password", "html": "<p>Click <a href=\"https://acme.com/reset?token=abc\">here</a> to reset your password.</p>" }' // Node.js 18+, run as an ES module for top-level await const response = await fetch('https://api.cresca.xyz/api/v1/emails', { method: 'POST', headers: { Authorization: `Bearer ${process.env.CRESCA_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'password-reset/reset-request-9f2c', }, body: JSON.stringify({ from: 'Acme <hello@acme.com>', to: 'user@example.com', subject: 'Reset your password', html: '<p>Click <a href="https://acme.com/reset?token=abc">here</a> to reset your password.</p>', }), }); const data = await response.json(); if (!response.ok) throw new Error(`${data.name}: ${data.message}`); console.log(data.id); import os import requests response = requests.post( "https://api.cresca.xyz/api/v1/emails", headers={ "Authorization": f"Bearer {os.environ['CRESCA_API_KEY']}", "Idempotency-Key": "password-reset/reset-request-9f2c", }, json={ "from": "Acme <hello@acme.com>", "to": "user@example.com", "subject": "Reset your password", "html": '<p>Click <a href="https://acme.com/reset?token=abc">here</a> to reset your password.</p>', }, timeout=30, ) response.raise_for_status() print(response.json()["id"]) A 200 response returns the id as soon as the email is accepted; sending happens in the background within seconds.
{ "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c" } Step 5: Check the status
Retrieve the email with your full-access key, or open Transactional in the app for the full timeline.
curl 'https://api.cresca.xyz/api/v1/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c' \ -H 'Authorization: Bearer cr_live_xxxxxxxxx' { "object": "email", "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "cc": null, "bcc": null, "reply_to": null, "subject": "Reset your password", "html": "<p>Click <a href=\"https://acme.com/reset?token=abc\">here</a> to reset your password.</p>", "text": "Click here (https://acme.com/reset?token=abc) to reset your password.", "created_at": "2026-10-19T09:14:03.120Z", "scheduled_at": null, "last_event": "delivered", "tags": [] } last_event moves from queued through sending and sent to delivered (or bounced). Emails are kept for 30 days; another account's id returns 404 not_found.
Emails API
The email sending API has six endpoints. POST /emails takes these fields; the deep dive is in the send email API guide.
| Field | Type | Required | Notes |
|---|---|---|---|
from | string | Yes* | hello@acme.com or Acme <hello@acme.com>; an active sender |
to | string or string[] | Yes | Up to 50 recipients across to, cc and bcc |
subject | string | Yes* | Single line, up to 998 characters |
html / text | string | One of html, text, template | Text is generated from HTML if omitted; "" leaves it out |
cc, bcc, reply_to | string or string[] | No | reply_to defaults to the sender's Reply-To |
template | object | No | { "id", "variables" }; cannot be combined with html or text |
scheduled_at | string | No | ISO 8601, up to 30 days ahead; a past time sends now |
tags | object[] | No | Up to 10 { "name", "value" } pairs |
headers | object | No | Up to 50 custom headers; Cresca-controlled ones are refused |
attachments | object[] | No | Up to 20, 7 MB total, base64 content and filename |
*Optional when a template supplies a default. Unknown fields are ignored. Attaching by URL (path) is not supported.
The shorter Node.js examples below use this helper:
const cresca = (method, path, body) => fetch(`https://api.cresca.xyz/api/v1${path}`, { method, headers: { Authorization: `Bearer ${process.env.CRESCA_API_KEY}`, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }).then((res) => res.json()); Send a batch
POST /emails/batch takes a JSON array of up to 100 emails with the same fields, except attachments and scheduled_at. A batch is all or nothing: if one email is invalid, nothing is sent and the message names it, for example Email 3: ... (zero-based).
curl -X POST 'https://api.cresca.xyz/api/v1/emails/batch' \ -H 'Authorization: Bearer cr_live_xxxxxxxxx' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: weekly-digest/2026-10-19' \ -d '[ { "from": "Acme <hello@acme.com>", "to": "ana@example.com", "subject": "Your weekly summary", "html": "<p>Hi Ana, here is your summary.</p>" }, { "from": "Acme <hello@acme.com>", "to": "ben@example.com", "subject": "Your weekly summary", "html": "<p>Hi Ben, here is your summary.</p>" } ]' const { data } = await cresca('POST', '/emails/batch', emails); // array of up to 100 emails { "data": [ { "id": "b6d1f2a0-5e2f-4c8e-9a51-0c8b5f1d2e33" }, { "id": "0f7e3c11-9b2a-4f6d-8e44-7a1c2d3e4f55" } ] } List emails
GET /emails accepts limit (1 to 100, default 20) and either after or before (an email id). Items leave out html and text.
import os import requests response = requests.get( "https://api.cresca.xyz/api/v1/emails", headers={"Authorization": f"Bearer {os.environ['CRESCA_API_KEY']}"}, params={"limit": 1}, timeout=30, ) print(response.json()) { "object": "list", "has_more": true, "data": [ { "object": "email", "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "cc": null, "bcc": null, "reply_to": null, "subject": "Reset your password", "created_at": "2026-10-19T09:14:03.120Z", "scheduled_at": null, "last_event": "delivered", "tags": [] } ] } For the next page, pass the last item's id as after while has_more is true.
Reschedule and cancel
PATCH /emails/:id takes one required field, scheduled_at, and only works while the status is scheduled. POST /emails/:id/cancel works while the email is scheduled or queued; a canceled email does not count toward your allowance.
curl -X PATCH 'https://api.cresca.xyz/api/v1/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c' \ -H 'Authorization: Bearer cr_live_xxxxxxxxx' \ -H 'Content-Type: application/json' \ -d '{ "scheduled_at": "2026-10-21T09:00:00Z" }' curl -X POST 'https://api.cresca.xyz/api/v1/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/cancel' \ -H 'Authorization: Bearer cr_live_xxxxxxxxx' await cresca('PATCH', `/emails/${id}`, { scheduled_at: '2026-10-21T09:00:00Z' }); await cresca('POST', `/emails/${id}/cancel`); Both return { "object": "email", "id": "4ef9a417-..." }. Rescheduling an email that is not scheduled, or canceling one that is already sending, returns 422 validation_error.
Common Emails API errors: 403 invalid_from_address (not an active sender, or the key is limited to another domain), 422 missing_required_field (no to, from or subject), 422 validation_error (bad address, more than 50 recipients, template combined with html), 422 invalid_attachment, 429 monthly_quota_exceeded and 404 not_found.
Templates API
Templates let you design an email once, publish it, and send it by alias with variables written as {{{NAME}}}. Edits change the draft; sends always use the published version. All six endpoints need a full-access key, and :id accepts the id or the alias. More depth: the email templates API guide and Templates and variables.
| Field | Required | Notes |
|---|---|---|
name | Yes | Up to 120 characters, unique in your account |
html | Yes | Body with {{{VARIABLE}}} placeholders |
alias | No | Lowercase letters, numbers, dashes, underscores; up to 64 |
subject, text, from, reply_to, preview_text | No | Defaults used at send time |
variables | No | Up to 50 { "key", "type", "fallback_value" }; type is string or number |
Create and publish
curl -X POST 'https://api.cresca.xyz/api/v1/templates' \ -H 'Authorization: Bearer cr_live_xxxxxxxxx' \ -H 'Content-Type: application/json' \ -d '{ "name": "Password reset", "alias": "password-reset", "subject": "Reset your password", "from": "Acme <hello@acme.com>", "html": "<p>Hi {{{NAME}}},</p><p><a href=\"{{{RESET_LINK}}}\">Reset your password</a>.</p>", "variables": [ { "key": "NAME", "type": "string", "fallback_value": "there" }, { "key": "RESET_LINK", "type": "string" } ] }' curl -X POST 'https://api.cresca.xyz/api/v1/templates/password-reset/publish' \ -H 'Authorization: Bearer cr_live_xxxxxxxxx' const { id } = await cresca('POST', '/templates', { name: 'Welcome', alias: 'welcome', html: '<p>Hi {{{NAME}}}</p>' }); await cresca('POST', `/templates/${id}/publish`); Create returns 201 with { "object": "template", "id": "6713b1f4c2a94e0012ab34cd" }. Publish returns the new version number:
{ "object": "template", "id": "6713b1f4c2a94e0012ab34cd", "version": 1 } Send by alias with variables
const response = await fetch('https://api.cresca.xyz/api/v1/emails', { method: 'POST', headers: { Authorization: `Bearer ${process.env.CRESCA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to: 'user@example.com', template: { id: 'password-reset', variables: { NAME: 'Ana', RESET_LINK: 'https://acme.com/reset?token=abc' }, }, }), }); console.log(await response.json()); // { id: '...' } Subject and sender come from the template unless your request sets them. Values are HTML-escaped in the HTML body. A variable with no value and no fallback stops the send with 422 validation_error naming it; an unpublished template also returns 422.
Retrieve, list, update and delete
curl 'https://api.cresca.xyz/api/v1/templates/password-reset' -H 'Authorization: Bearer cr_live_xxxxxxxxx' curl 'https://api.cresca.xyz/api/v1/templates?limit=10' -H 'Authorization: Bearer cr_live_xxxxxxxxx' curl -X PATCH 'https://api.cresca.xyz/api/v1/templates/password-reset' \ -H 'Authorization: Bearer cr_live_xxxxxxxxx' -H 'Content-Type: application/json' \ -d '{ "subject": "Reset your Acme password" }' curl -X DELETE 'https://api.cresca.xyz/api/v1/templates/password-reset' -H 'Authorization: Bearer cr_live_xxxxxxxxx' await cresca('GET', '/templates/password-reset'); await cresca('GET', '/templates?limit=10'); await cresca('PATCH', '/templates/password-reset', { subject: 'Reset your Acme password' }); await cresca('DELETE', '/templates/password-reset'); Retrieve returns the template object (list wraps the same objects in { "object": "list", "has_more", "data" }):
{ "object": "template", "id": "6713b1f4c2a94e0012ab34cd", "name": "Password reset", "alias": "password-reset", "subject": "Reset your password", "from": "Acme <hello@acme.com>", "reply_to": null, "preview_text": null, "html": "<p>Hi {{{NAME}}},</p><p><a href=\"{{{RESET_LINK}}}\">Reset your password</a>.</p>", "text": null, "variables": [ { "key": "NAME", "type": "string", "fallback_value": "there" }, { "key": "RESET_LINK", "type": "string", "fallback_value": null } ], "status": "published", "version": 1, "published_at": "2026-10-19T08:30:00.000Z", "created_at": "2026-10-18T16:02:11.000Z", "updated_at": "2026-10-19T08:30:00.000Z" } Update returns { "object": "template", "id" } and changes only the draft; send null for alias to remove it. Delete returns "deleted": true, and later sends that reference the template fail with 404 not_found. Typical errors: 422 missing_required_field (no name or html) and 422 validation_error (duplicate name or alias, bad variable).
Contacts API
POST /contacts adds up to 1,000 contacts per request to a list for your marketing campaigns, creating the list if needed. It needs a full-access key and is limited to 120 requests per 15 minutes. Full walkthrough: the contacts API guide.
