Emails
These routes handle email sent from your own code and public email-address verification. The emails.send route is the first-party shape of the same send the Resend facade serves, authenticating with an org API key and answering in the envelope with the delivery id. Unlike the facade it can address a profile instead of an address, which is how a message its recipient can turn off is sent. The public verification routes require no Cowliss credentials and answer with HTML pages rather than the envelope. Verification links authorize through a single-use ?token= that expires after 48 hours, and an invalid, expired, or already-spent token is rejected. The GET request only offers the verification and the POST request performs it, so a link scanner that fetches the URL does not verify the address.
POST /v1/emails
Section titled “POST /v1/emails”Send one email to an address or a profile
The recipient is an address, a list of addresses, or one identifier naming a profile in the source’s app, such as { "userId": "shop_1" }. An email identifier no profile holds yet creates that profile first, exactly as an identify carrying that address as identifier and trait would. A send to a profile goes to its email trait, else its newest email identifier, asks that profile’s consent for the template’s purpose, and carries the unsubscribe headers and footer when the recipient can turn the purpose off. A template the recipient can turn off cannot go to a bare address. With test: true the send runs every check and refuses exactly as a real one would, then records a dry-run delivery (would_send or would_skip_*) and answers with the rendered message, sending nothing, creating no profile and spending neither the Idempotency-Key nor credit. An address at test.cowliss.com (delivered@, bounced@, complained@, suppressed@) is a real send whose outcome the address picks.
Auth: Org API key (Authorization: Bearer <key>).
Request body
{ "data": { "sourceId": "string", "to": "string", "cc": "string", "bcc": "string", "replyTo": "string", "from": "string", "subject": "string", "html": "string", "text": "string", "headers": { "key": "string" }, "template": { "key": "string", "props": { "key": {} } }, "test": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.sourceId |
string | yes | length 0–200 |
data.to |
string | string[] | record<anonymousId | userId | clerkId | email, string> | yes | |
data.cc |
string | string[] | no | |
data.bcc |
string | string[] | no | |
data.replyTo |
string | string[] | no | |
data.from |
string | yes | length 0–200 |
data.subject |
string | no | length 0–1000 |
data.html |
string | no | |
data.text |
string | no | |
data.headers |
record<string, string> | no | |
data.template |
object | no | |
data.template.key |
string | yes | length 0–64 |
data.template.props |
record<string, any> | no | |
data.test |
boolean | no |
Response 201
{ "data": { "deliveryId": "string", "status": "sent", "wouldCreateProfile": true, "rendered": { "subject": "string", "html": "string", "text": "string" } }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.deliveryId |
string | yes | |
data.status |
“sent” | “delivered” | “bounced” | “complained” | “failed” | “skipped_disabled” | “skipped_paused” | “skipped_suspended” | “skipped_suppressed” | “skipped_quota” | “skipped_unknown_purpose” | “skipped_frequency_cap” | “skipped_consent” | “skipped_domain” | “skipped_ssrf” | “skipped_spam” | “would_send” | “would_skip_disabled” | “would_skip_paused” | “would_skip_suspended” | “would_skip_suppressed” | “would_skip_quota” | “would_skip_unknown_purpose” | “would_skip_frequency_cap” | “would_skip_consent” | “would_skip_domain” | “would_skip_ssrf” | “would_skip_spam” | yes | |
data.wouldCreateProfile |
boolean | no | |
data.rendered |
object | no | |
data.rendered.subject |
string | yes | |
data.rendered.html |
string | yes | |
data.rendered.text |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/email-previews
Section titled “POST /v1/email-previews”Render a template with your own props
What a send of this template to an address would carry, rendered with the props you pass, in the source’s app. Nothing is sent and no profile is read, so a template its recipients can turn off renders without its unsubscribe footer.
Auth: Org API key (Authorization: Bearer <key>).
Request body
{ "data": { "key": "string", "props": { "key": {} }, "sourceId": "string" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.key |
string | yes | length 0–64 |
data.props |
record<string, any> | no | |
data.sourceId |
string | yes | length 0–200 |
Response 200
{ "data": { "subject": "string", "html": "string", "text": "string", "versionId": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.subject |
string | yes | |
data.html |
string | yes | |
data.text |
string | yes | |
data.versionId |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/preferences-links
Section titled “POST /v1/preferences-links”Link one profile to its email preferences page
For your app’s own “Manage email preferences” button: to is one identifier naming a profile in the source’s app, resolved exactly as a send to a profile resolves it. The url opens the hosted preferences page for that profile and works for one hour, so mint it when the person clicks.
Auth: Org API key (Authorization: Bearer <key>).
Request body
{ "data": { "sourceId": "string", "to": { "anonymousId": "string" } }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.sourceId |
string | yes | length 0–200 |
data.to |
record<anonymousId | userId | clerkId | email, string> | yes |
Response 201
{ "data": { "url": "https://example.com/hooks/cowliss", "expiresAt": "2026-01-15T09:30:00.000Z" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.url |
string (uri) | yes | |
data.expiresAt |
string (date-time) | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/profile-preferences/find
Section titled “POST /v1/profile-preferences/find”Read one profile’s email preferences
For your app’s own preferences screen: to is one identifier naming a profile in the source’s app, as for a preferences link. Answers one row for marketing and one for each purpose the app declares. enabled is whether a send under that purpose would reach them now, and answered is whether they hold their own answer for it rather than the default (marketing off, transactional on).
Auth: Org API key (Authorization: Bearer <key>).
Request body
{ "data": { "sourceId": "string", "to": { "anonymousId": "string" } }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.sourceId |
string | yes | length 0–200 |
data.to |
record<anonymousId | userId | clerkId | email, string> | yes |
Response 200
{ "data": { "purposes": [ { "key": "string", "label": "string", "description": "string", "enabled": true, "answered": true } ] }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.purposes |
object[] | yes | |
data.purposes[] |
object | yes | |
data.purposes[].key |
string | yes | |
data.purposes[].label |
string | yes | |
data.purposes[].description |
string | null | yes | |
data.purposes[].enabled |
boolean | yes | |
data.purposes[].answered |
boolean | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/profile-preferences
Section titled “POST /v1/profile-preferences”Save one profile’s email preferences
Saves what the person chose on your app’s own preferences screen, exactly as the hosted page saves it. consent names only the switches to change: marketing or a purpose the app declares. Turning a marketing. purpose on turns marketing on, and turning marketing off turns every marketing. purpose off. Each switch that changes writes system.consent_granted or system.consent_revoked with reason preferences. A key the app does not declare is refused and nothing is saved. Answers with the rows as they now stand.
Auth: Org API key (Authorization: Bearer <key>).
Request body
{ "data": { "sourceId": "string", "to": { "anonymousId": "string" }, "consent": { "marketing.newsletter": true } }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.sourceId |
string | yes | length 0–200 |
data.to |
record<anonymousId | userId | clerkId | email, string> | yes | |
data.consent |
record<string, boolean> | yes |
Response 200
{ "data": { "purposes": [ { "key": "string", "label": "string", "description": "string", "enabled": true, "answered": true } ] }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.purposes |
object[] | yes | |
data.purposes[] |
object | yes | |
data.purposes[].key |
string | yes | |
data.purposes[].label |
string | yes | |
data.purposes[].description |
string | null | yes | |
data.purposes[].enabled |
boolean | yes | |
data.purposes[].answered |
boolean | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
GET /v1/public/verify
Section titled “GET /v1/public/verify”Verification confirmation page (public)
Auth: None: verifies the caller’s own signature/token.
Query parameters
{ "token": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
token |
string | no |
Errors follow the shared envelope and code table.
POST /v1/public/verify
Section titled “POST /v1/public/verify”Verify an email address (public)
Marks the address verified. If the profile associated with the link still carries that address, sets emailVerified to true and records a system.email_verified event.
Auth: None: verifies the caller’s own signature/token.
Query parameters
{ "token": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
token |
string | no |
Errors follow the shared envelope and code table.