Skip to content
Cowliss is in beta: features and pricing may change.

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.

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.

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.

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.

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.

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.

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.

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.