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.

<...Source:https://dev.to/lalitkumarjangid/transactional-email-api-the-complete-cresca-guide-3n03

Related posts

Field Type Required Notes
list_name string Yes Created if it does not exist
contacts (string \ object)[] Yes