Email API
Send transactional and marketing emails to users via the stemp platform.
Email API
The Platform Email API lets your installed app send emails to an organization's users and members through stemp's email infrastructure. Emails are sent from the organization's branded address with built-in unsubscribe management and delivery logging.
Required scope: platform:email:send
Authentication: App installation tokens only
Send an Email
curl -X POST https://api.stemp.app/api/v1/platform/emails \
-H "Authorization: Bearer <app_installation_token>" \
-H "Content-Type: application/json" \
-d '{
"to": "usr_xyz789",
"subject": "Your reward is ready!",
"htmlBody": "<h1>Congratulations!</h1><p>You earned a free coffee.</p>",
"category": "transactional"
}'Request Body
| Field | Required | Description |
|---|---|---|
to | Yes | Recipient ID — a user (usr_xxx) or member (mem_xxx) TypeId |
subject | Yes | Email subject line |
htmlBody | Yes | HTML content of the email |
textBody | No | Plain text version (auto-generated from HTML if omitted) |
category | Yes | Email category (see Categories below) |
attachments | No | Array of inline CID attachments (see Attachments) |
Response
{
"messageId": "01234567-89ab-cdef-0123-456789abcdef",
"status": "sent"
}Recipient Resolution
The to field accepts stemp TypeId references, not raw email addresses:
usr_xxx— End user (customer) created via the Users APImem_xxx— Organization member (staff)
The API resolves the TypeId to the recipient's email address. If the recipient is not found, you'll receive a 400 Bad Request error with code platform.email.user_not_found or platform.email.member_not_found.
Email Categories
Every email must specify a category that determines delivery behavior and unsubscribe handling:
| Category | Value | Unsubscribable | Description |
|---|---|---|---|
| Transactional | transactional | No | Order confirmations, receipts, password resets |
| Security | security | No | Security alerts, login notifications |
| Marketing | marketing | Yes | Promotions, offers, campaigns |
| Product Updates | product_updates | Yes | Feature announcements, changelog |
| General | general | Yes | General communications |
Unsubscribable Categories
For categories marked as unsubscribable (marketing, product_updates, general):
- stemp automatically adds
List-Unsubscribeheaders (RFC 8058 compliant) - Recipients can opt out via one-click unsubscribe in their email client
- If a recipient has opted out, the API returns a
422 Unprocessable Entity— your app should handle this gracefully - Transactional and security emails are always delivered regardless of preferences
Inline Attachments
You can embed images directly in your HTML using CID (Content-ID) attachments:
{
"to": "usr_xyz789",
"subject": "Your stamp card is complete!",
"htmlBody": "<h1>Congratulations!</h1><img src=\"cid:reward-image\" />",
"category": "transactional",
"attachments": [
{
"cid": "reward-image",
"filename": "reward.png",
"contentType": "image/png",
"base64Content": "iVBORw0KGgoAAAANSUhEUg..."
}
]
}| Field | Required | Description |
|---|---|---|
cid | Yes | Content-ID referenced in HTML via cid: |
filename | Yes | Attachment filename |
contentType | Yes | MIME type (e.g., image/png, image/jpeg) |
base64Content | Yes | Base64-encoded file content |
Email Logs
Retrieve the sending history for the organization. Logs include delivery tracking data — you can see whether each email was delivered, opened, and clicked.
curl "https://api.stemp.app/api/v1/platform/emails?page=0&size=50" \
-H "Authorization: Bearer <app_installation_token>"Query Parameters
| Parameter | Default | Description |
|---|---|---|
page | 0 | Page number (zero-based) |
size | 50 | Page size |
source | — | Optional filter: APP_API (app-sent) or SYSTEM_EVENT (system-sent, e.g. pass creation emails) |
Response
{
"content": [
{
"id": "email_abc123",
"recipientEmail": "jane@example.com",
"subject": "Your reward is ready!",
"status": "DELIVERED",
"messageId": "01234567-89ab-cdef-0123-456789abcdef",
"errorMessage": null,
"category": "transactional",
"source": "APP_API",
"createdAt": "2026-01-15T10:30:00Z",
"deliveredAt": "2026-01-15T10:30:05Z",
"bouncedAt": null,
"firstOpenedAt": "2026-01-15T11:02:00Z",
"firstClickedAt": "2026-01-15T11:02:15Z",
"openCount": 3,
"clickCount": 1
}
],
"page": { "number": 0, "size": 50, "totalElements": 1, "totalPages": 1 }
}| Field | Description |
|---|---|
status | SENT, DELIVERED, BOUNCED, COMPLAINED, or FAILED |
messageId | Delivery tracking ID (null if failed) |
errorMessage | Error details (null if successful) |
category | The email category used |
source | APP_API (sent by your app) or SYSTEM_EVENT (sent by stemp, e.g. pass creation) |
deliveredAt | When the recipient's mail server accepted the email |
bouncedAt | When the email bounced (null if not bounced) |
firstOpenedAt | When the recipient first opened the email (null if not tracked) |
firstClickedAt | When the recipient first clicked a link (null if not tracked) |
openCount | Total number of opens |
clickCount | Total number of link clicks |
Open and click tracking relies on a tracking pixel and link rewriting. Some email clients block tracking pixels, so open counts may undercount actual opens.
Event Timeline
Get the full event timeline for a specific email. This returns every delivery event in chronological order — useful for debugging delivery issues.
curl "https://api.stemp.app/api/v1/platform/emails/email_abc123/events" \
-H "Authorization: Bearer <app_installation_token>"Response
[
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"eventType": "DELIVERY",
"occurredAt": "2026-01-15T10:30:05Z",
"bounceType": null,
"bounceSubType": null,
"clickedLink": null,
"ipAddress": null,
"userAgent": null
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"eventType": "OPEN",
"occurredAt": "2026-01-15T11:02:00Z",
"bounceType": null,
"bounceSubType": null,
"clickedLink": null,
"ipAddress": "203.0.113.1",
"userAgent": "Mozilla/5.0"
},
{
"id": "550e8400-e29b-41d4-a716-446655440003",
"eventType": "CLICK",
"occurredAt": "2026-01-15T11:02:15Z",
"bounceType": null,
"bounceSubType": null,
"clickedLink": "https://example.com/redeem",
"ipAddress": "203.0.113.1",
"userAgent": "Mozilla/5.0"
}
]Event Types
| Event | Description |
|---|---|
DELIVERY | The recipient's mail server accepted the email |
BOUNCE | The email bounced (see bounceType for details) |
COMPLAINT | The recipient marked the email as spam |
OPEN | The recipient opened the email |
CLICK | The recipient clicked a link in the email |
Bounce Types
When eventType is BOUNCE, the bounceType field indicates severity:
| bounceType | bounceSubType | Meaning |
|---|---|---|
Permanent | General, NoEmail, Suppressed | Recipient address doesn't exist — the address is automatically suppressed |
Transient | MailboxFull, MessageTooLarge | Temporary issue — no suppression, email can be retried |
Email Suppressions
When an email permanently bounces or a recipient files a spam complaint, stemp automatically suppresses that email address to protect your sender reputation. Future sends to suppressed addresses will be rejected with a 422 error.
List Suppressed Addresses
curl "https://api.stemp.app/api/v1/platform/emails/suppressions?page=0&size=50" \
-H "Authorization: Bearer <app_installation_token>"{
"content": [
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"email": "invalid@example.com",
"reason": "HARD_BOUNCE",
"createdAt": "2026-01-15T10:30:05Z"
}
],
"page": { "number": 0, "size": 50, "totalElements": 1, "totalPages": 1 }
}| Reason | Description |
|---|---|
HARD_BOUNCE | Email permanently bounced (address doesn't exist) |
COMPLAINT | Recipient reported the email as spam |
Removing a suppression is only available to organization admins via the Core API (DELETE /v1/emails/suppressions/{id}). Apps have read-only access to the suppression list.
Sender Address
Emails are sent from the organization's branded address:
From: Acme Coffee <acme-coffee@stemp.email>The format is {organization_name} <{org_slug}@stemp.email>. This is managed automatically — your app does not control the sender address.
Error Handling
| Status | Code | Description |
|---|---|---|
400 | platform.email.user_not_found | User TypeId not found |
400 | platform.email.member_not_found | Member TypeId not found |
400 | platform.email.invalid_recipient_format | Invalid TypeId format |
422 | — | Recipient opted out of this email category |
422 | — | Recipient is suppressed (hard bounce or complaint) — see Suppressions |
Example: Stamp Reward Notification
A complete example of sending a reward notification when a stamp card is completed:
import { Stemp } from '@stemp/node'
const stemp = new Stemp({ apiKey: process.env.STEMP_API_KEY! })
async function notifyRewardEarned(userId: string, rewardName: string) {
await fetch('https://api.stemp.app/api/v1/platform/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${installationAccessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: userId,
subject: `You earned a reward: ${rewardName}!`,
htmlBody: `
<h1>Congratulations! 🎉</h1>
<p>You've completed your stamp card and earned: <strong>${rewardName}</strong></p>
<p>Show this email at the counter to redeem your reward.</p>
`,
category: 'transactional',
}),
})
}