stemp Logostemp Developer
Platform API

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

FieldRequiredDescription
toYesRecipient ID — a user (usr_xxx) or member (mem_xxx) TypeId
subjectYesEmail subject line
htmlBodyYesHTML content of the email
textBodyNoPlain text version (auto-generated from HTML if omitted)
categoryYesEmail category (see Categories below)
attachmentsNoArray 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 API
  • mem_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:

CategoryValueUnsubscribableDescription
TransactionaltransactionalNoOrder confirmations, receipts, password resets
SecuritysecurityNoSecurity alerts, login notifications
MarketingmarketingYesPromotions, offers, campaigns
Product Updatesproduct_updatesYesFeature announcements, changelog
GeneralgeneralYesGeneral communications

Unsubscribable Categories

For categories marked as unsubscribable (marketing, product_updates, general):

  • stemp automatically adds List-Unsubscribe headers (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..."
    }
  ]
}
FieldRequiredDescription
cidYesContent-ID referenced in HTML via cid:
filenameYesAttachment filename
contentTypeYesMIME type (e.g., image/png, image/jpeg)
base64ContentYesBase64-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

ParameterDefaultDescription
page0Page number (zero-based)
size50Page size
sourceOptional 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 }
}
FieldDescription
statusSENT, DELIVERED, BOUNCED, COMPLAINED, or FAILED
messageIdDelivery tracking ID (null if failed)
errorMessageError details (null if successful)
categoryThe email category used
sourceAPP_API (sent by your app) or SYSTEM_EVENT (sent by stemp, e.g. pass creation)
deliveredAtWhen the recipient's mail server accepted the email
bouncedAtWhen the email bounced (null if not bounced)
firstOpenedAtWhen the recipient first opened the email (null if not tracked)
firstClickedAtWhen the recipient first clicked a link (null if not tracked)
openCountTotal number of opens
clickCountTotal 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

EventDescription
DELIVERYThe recipient's mail server accepted the email
BOUNCEThe email bounced (see bounceType for details)
COMPLAINTThe recipient marked the email as spam
OPENThe recipient opened the email
CLICKThe recipient clicked a link in the email

Bounce Types

When eventType is BOUNCE, the bounceType field indicates severity:

bounceTypebounceSubTypeMeaning
PermanentGeneral, NoEmail, SuppressedRecipient address doesn't exist — the address is automatically suppressed
TransientMailboxFull, MessageTooLargeTemporary 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 }
}
ReasonDescription
HARD_BOUNCEEmail permanently bounced (address doesn't exist)
COMPLAINTRecipient 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

StatusCodeDescription
400platform.email.user_not_foundUser TypeId not found
400platform.email.member_not_foundMember TypeId not found
400platform.email.invalid_recipient_formatInvalid TypeId format
422Recipient opted out of this email category
422Recipient 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',
    }),
  })
}