Transactional Email Setup
Set up and send transactional emails like password resets and order confirmations via the Owlat API and SDKs.
Transactional emails are triggered by a user action — a password reset, an order confirmation, a welcome message after signup. Unlike marketing campaigns, they're sent to a single recipient at a specific moment, and recipients expect them immediately. This guide walks through creating a transactional email, generating an API key, and sending from your application.
For the conceptual overview (transactional vs. marketing, slugs, attachments, monitoring), see Transactional Emails. This page focuses on the integration steps.
Common examples:
- Password reset links
- Welcome emails after registration
- Order confirmations and receipts
- Shipping notifications
- Account verification emails
Create a transactional email
Transactional emails are a distinct entity from marketing templates — each one has a unique slug your application references at send time.
Open Mail > Transactional
Go to Mail > Transactional in your dashboard. The list page (titled "Transactional Emails") shows everything you've created.
Create the email
Click New Transactional Email. In the dialog, enter a name (e.g., "Order Confirmation") and a unique slug (e.g., order-confirmation). The slug uses lowercase letters, numbers, and hyphens, and is how your code identifies which email to send — choose it carefully and avoid changing it once it's in use.
Design the content
You're taken into the email editor. Build the layout the same way you would any email. Wherever you need dynamic content, insert a data variable using the /var command — for example a customerName, orderNumber, or total variable. You supply the values for these variables at send time.
Publish
A transactional email must be published before it can be sent. Click Publish in the editor. Until then, send requests are rejected (see Error handling). If the content scanner flags the email, it enters an Awaiting review state rather than going live immediately.
Use descriptive variable names like orderNumber instead of generic ones like var1. It makes your API calls self-documenting and easier to debug. In the rendered HTML these become {{orderNumber}} placeholders, but you never type the braces yourself — the /var command inserts them for you.
Generate an API key
You need an API key to authenticate transactional sends.
Open Settings > API
Go to Settings > API in your dashboard (route /dashboard/settings/api). API key management is available to organization owners and admins.
Create the key
Click Create API Key and give it a descriptive name (e.g., "Production") and at least one scope. Keys are scoped to least privilege — for transactional sends, select transactional:send. (Other available scopes: contacts:read, contacts:write, events:write, topics:write.)
Copy the key
Copy the generated key. It starts with lm_live_ and is shown only once.
An Owlat API key carries only the scopes you grant it — a transactional send needs transactional:send. Treat it like a password: store it in an environment variable or secrets manager, never commit it to version control, and never expose it in client-side code. The key is shown only at creation time.
Send via the TypeScript SDK
Install the Owlat SDK and send your first transactional email:
import { Owlat } from '@owlat/sdk-js'
const owlat = new Owlat('lm_live_...')
const result = await owlat.transactional.send({
email: 'user@example.com',
slug: 'order-confirmation',
dataVariables: {
orderNumber: '#12345',
customerName: 'Jane',
total: '$49.99',
},
})
// result.transactionalEmailId — the send-record id, use it to track delivery
The SDK handles authentication, serialization, and error handling for you. Pass any data variables your email expects in the dataVariables object. You can target an email by slug or by transactionalId.
Send via the Java SDK
Owlat owlat = new Owlat("lm_live_...");
owlat.transactional().send(
SendTransactionalParams.builder("user@example.com")
.slug("order-confirmation")
.dataVariables(Map.of(
"orderNumber", "#12345",
"customerName", "Jane",
"total", "$49.99"
))
.build()
);
The builder takes the recipient email as its only positional argument; set the slug with .slug(...) and pass all variables at once with .dataVariables(Map).
Send via cURL
If you prefer to call the API directly:
curl -X POST https://your-deployment.convex.site/api/v1/transactional \
-H "Authorization: Bearer lm_live_..." \
-H "Content-Type: application/json" \
-d '{
"slug": "order-confirmation",
"email": "user@example.com",
"dataVariables": {
"orderNumber": "#12345",
"customerName": "Jane",
"total": "$49.99"
}
}'
A successful request returns a 202 Accepted status — the email is queued for delivery, not sent synchronously. The response body includes:
| Field | Description |
|---|---|
status | Always "queued" on success |
transactionalEmailId | The send-record id — use it to correlate with delivery webhooks |
email | The recipient address |
slug | The slug used |
contactId | The associated contact (found or created) |
contactCreated | true if a new contact was created for this recipient |
language | The language used for the send |
View API code in the dashboard
You don't have to hand-write the call. On the Transactional list, each email has a View API Code action (the < > icon, also in the row's menu) that opens a ready-made send snippet in cURL, JavaScript, and Python — pre-filled with the email's slug. Copy it, drop in your API key, and you're sending.
Error handling
When a transactional send fails, the API returns one of the categorized statuses below. Map your retry logic to the status code, not the message text.
| Status | Meaning | Common causes |
|---|---|---|
400 | Invalid input | Malformed payload, missing email/slug, or a data variable whose value doesn't match the type declared on the email |
401 | Unauthenticated | Missing or invalid API key |
403 | Forbidden | The account has been suspended, or the API key is missing the required scope (transactional:send) |
404 | Not found | No transactional email matches the given slug or id |
422 | Invalid state | The email is not published, has no content, the sending domain is unverified, or the recipient is blocked (prior bounce/complaint) |
429 | Rate limited | More than 10 requests/second on this key — back off and retry |
500 | Server error | Retry after a short delay |
A missing or wrong-typed data variable is a 400, not a 422 — 422 is reserved for state problems like an unpublished email or unverified domain. Don't silently swallow send errors: log failures and alert your team. A failed password-reset email means a locked-out user.
For 429 and 500 errors, implement exponential backoff in your retry logic. Transactional emails are important to your users, so build resilience into your sending code.
Next steps
- Transactional API — full endpoint and field reference
- TypeScript SDK — complete SDK reference
- Java SDK — Java SDK reference
- Webhooks — get notified about delivery events in real time