# Export messaging analytics data
URL: https://samva.app/docs/api-reference/analytics/exportData
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get channel performance analytics
URL: https://samva.app/docs/api-reference/analytics/getChannelStats
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get message delivery metrics
URL: https://samva.app/docs/api-reference/analytics/getDeliveryMetrics
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get geographic message distribution
URL: https://samva.app/docs/api-reference/analytics/getGeographicDistribution
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get hourly message distribution
URL: https://samva.app/docs/api-reference/analytics/getHourlyDistribution
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get message volume analytics
URL: https://samva.app/docs/api-reference/analytics/getStats
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List top contacts by message volume
URL: https://samva.app/docs/api-reference/analytics/getTopContacts
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create an API key
URL: https://samva.app/docs/api-reference/apiKeys/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an API key
URL: https://samva.app/docs/api-reference/apiKeys/get
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List API key activity
URL: https://samva.app/docs/api-reference/apiKeys/getActivity
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get API key usage statistics
URL: https://samva.app/docs/api-reference/apiKeys/getStats
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List API keys
URL: https://samva.app/docs/api-reference/apiKeys/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete an API key
URL: https://samva.app/docs/api-reference/apiKeys/remove
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Revoke an API key
URL: https://samva.app/docs/api-reference/apiKeys/revoke
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Rotate an API key
URL: https://samva.app/docs/api-reference/apiKeys/rotate
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update an API key
URL: https://samva.app/docs/api-reference/apiKeys/update
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an attachment download URL
URL: https://samva.app/docs/api-reference/attachments/downloadUrl
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an attachment
URL: https://samva.app/docs/api-reference/attachments/getById
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List message attachments
URL: https://samva.app/docs/api-reference/attachments/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete an attachment
URL: https://samva.app/docs/api-reference/attachments/remove
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an attachment thumbnail URL
URL: https://samva.app/docs/api-reference/attachments/thumbnailUrl
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Archive a campaign
URL: https://samva.app/docs/api-reference/campaigns/archive
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel a campaign run
URL: https://samva.app/docs/api-reference/campaigns/cancelRun
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a campaign
URL: https://samva.app/docs/api-reference/campaigns/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a campaign
URL: https://samva.app/docs/api-reference/campaigns/get
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a campaign run
URL: https://samva.app/docs/api-reference/campaigns/getRun
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List campaigns
URL: https://samva.app/docs/api-reference/campaigns/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List campaign recipients
URL: https://samva.app/docs/api-reference/campaigns/listRecipients
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List campaign runs
URL: https://samva.app/docs/api-reference/campaigns/listRuns
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pause a campaign run
URL: https://samva.app/docs/api-reference/campaigns/pauseRun
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Resume a campaign run
URL: https://samva.app/docs/api-reference/campaigns/resumeRun
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Schedule a campaign run
URL: https://samva.app/docs/api-reference/campaigns/scheduleRun
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a campaign
URL: https://samva.app/docs/api-reference/campaigns/update
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add members to a contact group
URL: https://samva.app/docs/api-reference/contactGroups/addMembers
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a contact group
URL: https://samva.app/docs/api-reference/contactGroups/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Evaluate a dynamic contact group
URL: https://samva.app/docs/api-reference/contactGroups/evaluateDynamic
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a contact group
URL: https://samva.app/docs/api-reference/contactGroups/getById
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List members of a contact group
URL: https://samva.app/docs/api-reference/contactGroups/getMembers
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List contact groups
URL: https://samva.app/docs/api-reference/contactGroups/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Preview contact group criteria
URL: https://samva.app/docs/api-reference/contactGroups/preview
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Recount a contact group's members
URL: https://samva.app/docs/api-reference/contactGroups/recount
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a contact group
URL: https://samva.app/docs/api-reference/contactGroups/remove
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove members from a contact group
URL: https://samva.app/docs/api-reference/contactGroups/removeMembers
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a contact group
URL: https://samva.app/docs/api-reference/contactGroups/update
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete contacts
URL: https://samva.app/docs/api-reference/contacts/bulkDelete
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk import contacts
URL: https://samva.app/docs/api-reference/contacts/bulkImport
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk update contact status
URL: https://samva.app/docs/api-reference/contacts/bulkUpdateStatus
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk update contact tags
URL: https://samva.app/docs/api-reference/contacts/bulkUpdateTags
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a contact
URL: https://samva.app/docs/api-reference/contacts/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export contacts
URL: https://samva.app/docs/api-reference/contacts/export
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Find a contact
URL: https://samva.app/docs/api-reference/contacts/find
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Find or create a contact
URL: https://samva.app/docs/api-reference/contacts/findOrCreate
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a contact
URL: https://samva.app/docs/api-reference/contacts/get
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get contact activity timeline
URL: https://samva.app/docs/api-reference/contacts/getActivityTimeline
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get contact engagement metrics
URL: https://samva.app/docs/api-reference/contacts/getEngagement
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List a contact's group memberships
URL: https://samva.app/docs/api-reference/contacts/getGroupMemberships
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Contacts
URL: https://samva.app/docs/api-reference/contacts
`contacts` are the people you send to. Store them once and address a
[message](/docs/api-reference/messages) by `contactId` instead of a raw email, or
group them with [contact groups](/docs/api-reference/contactGroups/list) for segmented
sends.
## What you can do [#what-you-can-do]
* **Manage** contacts, create, retrieve, list, update, and remove.
* **Bulk operations**, import many contacts at once, and bulk-update status or tags.
* **Engagement**, read a contact's engagement history.
* **Export** your contacts.
## Addressing a contact [#addressing-a-contact]
Once a contact exists, reference it from the send body instead of an email address:
```json
{
"to": [{ "contactId": "c0a8011e-0000-4000-8000-000000000001" }],
"channel": "email",
"email": { "subject": "Hello", "html": "
Hi!
" }
}
```
Shared conventions (auth, status codes, errors, pagination) are in the
[REST API reference](/docs/developers/rest-api). Per-operation request and response
detail is in the sidebar.
# List contacts
URL: https://samva.app/docs/api-reference/contacts/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a contact
URL: https://samva.app/docs/api-reference/contacts/remove
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a contact
URL: https://samva.app/docs/api-reference/contacts/update
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add contacts to a conversation
URL: https://samva.app/docs/api-reference/conversations/addContacts
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a conversation
URL: https://samva.app/docs/api-reference/conversations/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a conversation
URL: https://samva.app/docs/api-reference/conversations/getById
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List conversations
URL: https://samva.app/docs/api-reference/conversations/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove contacts from a conversation
URL: https://samva.app/docs/api-reference/conversations/removeContacts
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a conversation
URL: https://samva.app/docs/api-reference/conversations/update
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an API request log
URL: https://samva.app/docs/api-reference/developers/getApiLog
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List API request logs
URL: https://samva.app/docs/api-reference/developers/getApiLogs
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a draft
URL: https://samva.app/docs/api-reference/drafts/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a draft
URL: https://samva.app/docs/api-reference/drafts/getById
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List drafts
URL: https://samva.app/docs/api-reference/drafts/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a draft
URL: https://samva.app/docs/api-reference/drafts/remove
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a draft
URL: https://samva.app/docs/api-reference/drafts/update
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add a block entry
URL: https://samva.app/docs/api-reference/email/addBlock
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add a sending domain
URL: https://samva.app/docs/api-reference/email/addDomain
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add a sender identity
URL: https://samva.app/docs/api-reference/email/addSender
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Check whether an address is blocked
URL: https://samva.app/docs/api-reference/email/checkBlocked
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Check domain verification status
URL: https://samva.app/docs/api-reference/email/checkDomainVerification
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Check sender verification status
URL: https://samva.app/docs/api-reference/email/checkSenderVerification
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Complete email onboarding
URL: https://samva.app/docs/api-reference/email/completeEmailOnboarding
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a forwarding rule
URL: https://samva.app/docs/api-reference/email/createForwardingRule
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create an email template
URL: https://samva.app/docs/api-reference/email/createTemplate
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a forwarding rule
URL: https://samva.app/docs/api-reference/email/deleteForwardingRule
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Detect a domain's registrar
URL: https://samva.app/docs/api-reference/email/detectRegistrar
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disable catch-all receiving
URL: https://samva.app/docs/api-reference/email/disableCatchAll
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Enable catch-all receiving
URL: https://samva.app/docs/api-reference/email/enableCatchAll
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Enable inbound email for a domain
URL: https://samva.app/docs/api-reference/email/enableReceiving
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Generate an onboarding API key
URL: https://samva.app/docs/api-reference/email/generateOnboardingApiKey
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an email message
URL: https://samva.app/docs/api-reference/email/get
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List blocked email logs
URL: https://samva.app/docs/api-reference/email/getBlockLogs
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get catch-all configuration
URL: https://samva.app/docs/api-reference/email/getCatchAllConfig
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get catch-all statistics
URL: https://samva.app/docs/api-reference/email/getCatchAllStats
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a sending domain
URL: https://samva.app/docs/api-reference/email/getDomainById
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get sending domain status
URL: https://samva.app/docs/api-reference/email/getDomainStatus
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List email forwarding logs
URL: https://samva.app/docs/api-reference/email/getForwardingLog
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get email onboarding state
URL: https://samva.app/docs/api-reference/email/getOnboardingState
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get email statistics
URL: https://samva.app/docs/api-reference/email/getStats
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an email thread
URL: https://samva.app/docs/api-reference/email/getThread
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get email thread details
URL: https://samva.app/docs/api-reference/email/getThreadById
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get email thread counts
URL: https://samva.app/docs/api-reference/email/getThreadCounts
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List email thread messages
URL: https://samva.app/docs/api-reference/email/getThreadMessages
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get message threading metadata
URL: https://samva.app/docs/api-reference/email/getThreadMetadata
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Email
URL: https://samva.app/docs/api-reference/email
The `email` resource covers the infrastructure behind email sending and receiving:
the domains you send from, the senders you send as, suppression, inbound routing,
and conversation threads. The actual send call lives on
[Messages](/docs/api-reference/messages); this resource is the setup and inbound
surface around it.
## What you can do [#what-you-can-do]
* **Domains**, add a sending domain, verify its DNS, check status, and enable receiving for inbound.
* **Senders**, register and verify the From addresses you send as.
* **Suppression (blocks)**, manage the suppression list and inspect why an address was blocked.
* **Inbound**, configure catch-all and forwarding rules for received mail.
* **Threads**, read inbound conversation threads and their messages.
* **Stats**, pull aggregate email statistics.
## Before you can send from your domain [#before-you-can-send-from-your-domain]
Brand-new accounts can send immediately on a shared domain. To send from your own
domain, add and verify it first, see
[Verify your domain](/docs/channels/email/verify-your-domain) for the DNS walkthrough.
Shared conventions (auth, status codes, errors, pagination) are in the
[REST API reference](/docs/developers/rest-api). Per-operation request and response
detail is in the sidebar.
# List block entries
URL: https://samva.app/docs/api-reference/email/listBlocks
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List sending domains
URL: https://samva.app/docs/api-reference/email/listDomains
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List forwarding rules
URL: https://samva.app/docs/api-reference/email/listForwardingRules
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List sender identities
URL: https://samva.app/docs/api-reference/email/listSenders
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List email templates
URL: https://samva.app/docs/api-reference/email/listTemplates
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List email threads
URL: https://samva.app/docs/api-reference/email/listThreads
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove a block entry
URL: https://samva.app/docs/api-reference/email/removeBlock
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove a sending domain
URL: https://samva.app/docs/api-reference/email/removeDomain
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove a sender identity
URL: https://samva.app/docs/api-reference/email/removeSender
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a block entry
URL: https://samva.app/docs/api-reference/email/updateBlock
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update catch-all configuration
URL: https://samva.app/docs/api-reference/email/updateCatchAllConfig
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a forwarding rule
URL: https://samva.app/docs/api-reference/email/updateForwardingRule
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Verify a sending domain
URL: https://samva.app/docs/api-reference/email/verifyDomain
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# API Reference
URL: https://samva.app/docs/api-reference
The Samva API is a REST API: requests and responses are JSON, and standard HTTP status codes signal success or failure. Browse endpoints by service in the sidebar, or start from the conventions reference below.
**OpenAPI specification**: the live spec is published at
[api.samva.app/v1/openapi.json](https://api.samva.app/v1/openapi.json). See [the OpenAPI
page](/docs/api-reference/openapi) for details.
## Conventions [#conventions]
Shared conventions (base URL, authentication, request and response shapes, status codes, errors, and pagination) are documented once in the REST API reference. Start there, then use the per-endpoint pages in the sidebar.
## Endpoints by service [#endpoints-by-service]
**Email**
* **[Messages](/docs/api-reference/messages/send)**: send and manage messages
* **[Email](/docs/api-reference/email/get)**: domains, senders, threading, and inbound
* **[Templates](/docs/api-reference/templates/list)**: reusable, versioned email templates
* **[Drafts](/docs/api-reference/drafts/list)**: saved message drafts
* **[Scheduled messages](/docs/api-reference/scheduledMessages/list)**: one-off scheduled sends
* **[Campaigns](/docs/api-reference/campaigns/list)**: audience campaigns with scheduled runs
* **[Attachments](/docs/api-reference/attachments/list)**: message attachments
* **[Media](/docs/api-reference/media/get)**: media uploads
**Contacts & Conversations**
* **[Contacts](/docs/api-reference/contacts/list)**: contact management
* **[Contact groups](/docs/api-reference/contactGroups/list)**: segments and audiences
* **[Conversations](/docs/api-reference/conversations/list)**: conversation threading
**Configuration**
* **[Webhooks](/docs/api-reference/webhooks/create)**: webhook endpoint configuration
* **[API keys](/docs/api-reference/apiKeys/create)**: API key management
* **[Developers](/docs/api-reference/developers/getApiLogs)**: API request logs
* **[Organizations](/docs/api-reference/organizations/get)**: organization settings
**Analytics & Reference**
* **[Analytics](/docs/api-reference/analytics/getStats)**: delivery metrics and exports
## Support [#support]
Need help with the API? Email [support@samva.app](mailto:support@samva.app).
# Cancel a media upload
URL: https://samva.app/docs/api-reference/media/cancelUpload
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Confirm a media upload
URL: https://samva.app/docs/api-reference/media/confirmUpload
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Upload a media file directly
URL: https://samva.app/docs/api-reference/media/directUpload
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a media record
URL: https://samva.app/docs/api-reference/media/getMedia
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a media download URL
URL: https://samva.app/docs/api-reference/media/getMediaUrl
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Initialize a media upload
URL: https://samva.app/docs/api-reference/media/initializeUpload
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a message
URL: https://samva.app/docs/api-reference/messages/get
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List message events
URL: https://samva.app/docs/api-reference/messages/getEvents
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get message delivery status
URL: https://samva.app/docs/api-reference/messages/getStatus
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Messages
URL: https://samva.app/docs/api-reference/messages
`messages` is the core send resource. Every send, whether you call the SDK's
`samva.email.send` or post directly, resolves to `POST /v1/messages`, and the
same resource lets you list messages and follow their delivery lifecycle.
## What you can do [#what-you-can-do]
* **Send** a message (`POST /v1/messages`).
* **List** and **retrieve** messages.
* **Track delivery**, fetch a message's current `status` and its full event timeline.
* **Delete** a message record.
## Send a message [#send-a-message]
```bash
curl -X POST https://api.samva.app/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"to": [{ "email": "ada@example.com" }],
"channel": "email",
"email": { "subject": "Welcome to Samva", "html": "
Welcome!
" }
}'
```
A successful send returns `201` with the created message in `pending` status. Poll the
message to watch it move through `pending` → `processing` → `sent` → `delivered`.
For a runnable walkthrough in curl, Python, Go, and PHP, see
[Send an email](/docs/channels/email/send-an-email). Shared conventions (auth, status
codes, errors, pagination) live in the [REST API reference](/docs/developers/rest-api).
Per-operation request and response detail is in the sidebar.
# List messages
URL: https://samva.app/docs/api-reference/messages/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a message
URL: https://samva.app/docs/api-reference/messages/remove
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send a message
URL: https://samva.app/docs/api-reference/messages/send
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# OpenAPI Specification
URL: https://samva.app/docs/api-reference/openapi
The Samva API is fully documented using the OpenAPI 3.0 specification. You can use this specification to generate client libraries, explore the API, or integrate with API tools.
## Accessing the Specification [#accessing-the-specification]
The OpenAPI specification is available at:
```
https://api.samva.app/v1/openapi.json
```
For development:
```
https://api.samva.localhost/v1/openapi.json
```
## Using the Specification [#using-the-specification]
### Interactive Documentation [#interactive-documentation]
You can use tools like Swagger UI or Scalar to explore the API interactively:
1. Download the OpenAPI spec from `/v1/openapi.json`
2. Import it into Swagger UI, Postman, or your preferred API tool
3. Explore endpoints, schemas, and examples
### Code Generation [#code-generation]
Generate client libraries for your preferred language:
**TypeScript/JavaScript:**
```bash
npx @hey-api/openapi-ts -i https://api.samva.app/v1/openapi.json -o ./src/client
```
**Python:**
```bash
openapi-generator generate -i https://api.samva.app/v1/openapi.json -g python -o ./python-client
```
**Go:**
```bash
openapi-generator generate -i https://api.samva.app/v1/openapi.json -g go -o ./go-client
```
### Postman Collection [#postman-collection]
Import the OpenAPI spec into Postman:
1. Open Postman
2. Click **Import**
3. Select **Link** and enter: `https://api.samva.app/v1/openapi.json`
4. Postman will create a collection with all endpoints
## Specification Details [#specification-details]
The OpenAPI spec includes:
* **All exported SDK endpoints** - Complete list of authenticated and public HttpApi operations included in the generated SDK surface
* **Request/Response schemas** - Detailed schemas for all requests and responses
* **Authentication** - API key authentication details
* **Examples** - Request and response examples
* **Error responses** - Error schema documentation
## SDK Generation [#sdk-generation]
The official Samva TypeScript SDK is generated from this OpenAPI specification:
```bash
npm install samva
```
[View SDK Documentation →](/docs/developers/typescript-sdk)
## Next Steps [#next-steps]
* [API Endpoints](/docs/api-reference)
* [API keys](/docs/developers/authentication)
* [SDK Documentation](/docs/developers/typescript-sdk)
# Create a workspace
URL: https://samva.app/docs/api-reference/organizations/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get the current organization
URL: https://samva.app/docs/api-reference/organizations/get
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get organization usage
URL: https://samva.app/docs/api-reference/organizations/getUsage
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update the current organization
URL: https://samva.app/docs/api-reference/organizations/update
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel a scheduled message
URL: https://samva.app/docs/api-reference/scheduledMessages/cancel
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Schedule a message
URL: https://samva.app/docs/api-reference/scheduledMessages/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a scheduled message
URL: https://samva.app/docs/api-reference/scheduledMessages/get
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List scheduled messages
URL: https://samva.app/docs/api-reference/scheduledMessages/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add Family Channel
URL: https://samva.app/docs/api-reference/templates/addFamilyChannel
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Approve a template
URL: https://samva.app/docs/api-reference/templates/approve
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Attach Family Member
URL: https://samva.app/docs/api-reference/templates/attachFamilyMember
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a template
URL: https://samva.app/docs/api-reference/templates/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create Family
URL: https://samva.app/docs/api-reference/templates/createFamily
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create Family From Template
URL: https://samva.app/docs/api-reference/templates/createFamilyFromTemplate
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Family
URL: https://samva.app/docs/api-reference/templates/deleteFamily
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Detach Family Member
URL: https://samva.app/docs/api-reference/templates/detachFamilyMember
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Duplicate a template
URL: https://samva.app/docs/api-reference/templates/duplicate
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a template
URL: https://samva.app/docs/api-reference/templates/getById
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Family
URL: https://samva.app/docs/api-reference/templates/getFamily
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Templates
URL: https://samva.app/docs/api-reference/templates
`templates` are reusable, versioned email bodies. Create a template once, then send
from it by passing `templateId` on a [message](/docs/api-reference/messages) instead
of inline `subject`/`html`, so content lives in one place and can change without a
code deploy.
## What you can do [#what-you-can-do]
* **Manage** templates, create, retrieve, list, update, and remove.
* **Render** a template to its final HTML, and **preview / validate** its content before publishing.
* **Approve** a template to mark it ready for sending.
## Sending from a template [#sending-from-a-template]
Reference a stored template from the send body instead of inline content:
```json
{
"to": [{ "email": "ada@example.com" }],
"channel": "email",
"email": { "templateId": "tmpl_..." }
}
```
Shared conventions (auth, status codes, errors, pagination) are in the
[REST API reference](/docs/developers/rest-api). Per-operation request and response
detail is in the sidebar.
# List templates
URL: https://samva.app/docs/api-reference/templates/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List Dashboard
URL: https://samva.app/docs/api-reference/templates/listDashboard
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List Families
URL: https://samva.app/docs/api-reference/templates/listFamilies
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Publish a template
URL: https://samva.app/docs/api-reference/templates/publish
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a template
URL: https://samva.app/docs/api-reference/templates/remove
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Rename Family
URL: https://samva.app/docs/api-reference/templates/renameFamily
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Render a template
URL: https://samva.app/docs/api-reference/templates/render
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Restore a template version
URL: https://samva.app/docs/api-reference/templates/restoreVersion
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Spam-check a template or email content
URL: https://samva.app/docs/api-reference/templates/spamCheck
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send a test email from a template
URL: https://samva.app/docs/api-reference/templates/testSend
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Unpublish a template
URL: https://samva.app/docs/api-reference/templates/unpublish
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a template
URL: https://samva.app/docs/api-reference/templates/update
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List template versions
URL: https://samva.app/docs/api-reference/templates/versionHistory
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a webhook
URL: https://samva.app/docs/api-reference/webhooks/create
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a webhook
URL: https://samva.app/docs/api-reference/webhooks/get
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List webhook delivery logs
URL: https://samva.app/docs/api-reference/webhooks/getLogs
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get webhook delivery statistics
URL: https://samva.app/docs/api-reference/webhooks/getStats
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Webhooks
URL: https://samva.app/docs/api-reference/webhooks
`webhooks` push delivery and inbound events to your endpoints in real time, so you
don't have to poll [messages](/docs/api-reference/messages). Every payload is signed;
verify the signature before trusting it.
## What you can do [#what-you-can-do]
* **Manage** endpoints, create, retrieve, list, update, and remove.
* **Secrets**, regenerate an endpoint's signing secret.
* **Observability**, inspect delivery logs and stats.
* **Reliability**, send a test event and retry a failed delivery.
## Verifying payloads [#verifying-payloads]
Webhook requests are signed with your endpoint's secret. Verify the signature on every
request and reject anything that doesn't match, see
[Webhooks](/docs/platform/webhooks) for the signing scheme and a verification example.
Shared conventions (auth, status codes, errors, pagination) are in the
[REST API reference](/docs/developers/rest-api). Per-operation request and response
detail is in the sidebar.
# List webhooks
URL: https://samva.app/docs/api-reference/webhooks/list
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Rotate a webhook signing secret
URL: https://samva.app/docs/api-reference/webhooks/regenerateSecret
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a webhook
URL: https://samva.app/docs/api-reference/webhooks/remove
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Retry a webhook delivery
URL: https://samva.app/docs/api-reference/webhooks/retryDelivery
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send a test webhook delivery
URL: https://samva.app/docs/api-reference/webhooks/test
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a webhook
URL: https://samva.app/docs/api-reference/webhooks/update
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Deliverability
URL: https://samva.app/docs/channels/email/deliverability
Deliverability is the question behind every email API: not just whether a message was accepted for sending, but whether it actually reaches the inbox rather than the spam folder or a silent block. Sending is the easy part. Earning the trust of the mailbox providers that decide where your mail lands is the work, and most of that trust is built before you ever send a message.
This page explains the forces that shape inbox placement and where Samva fits into each one. It is background, not a checklist. When you're ready to act on it, start with [verifying your sending domain](/docs/channels/email/verify-your-domain), which is the single highest-leverage step.
## Why inbox placement is hard [#why-inbox-placement-is-hard]
When you hand a message to Samva, it is relayed through AWS SES to the recipient's mailbox provider: Gmail, Outlook, and the rest. Each provider runs its own filtering, and none of them publish exactly how it works. Broadly, they weigh three things:
* **Identity**: can the provider prove the mail genuinely came from the domain it claims to be from?
* **Reputation**: does mail from this sender historically get delivered, opened, and left in the inbox, or does it bounce and get marked as spam?
* **Engagement**: do recipients actually want this mail, measured by opens, clicks, replies, and the absence of complaints?
You can't control a provider's filter directly. What you can control is the signal you send it. Samva's role is to make the strong signals easy to establish and the damaging signals automatic to suppress.
## Identity: verified domains and authentication [#identity-verified-domains-and-authentication]
The foundation of deliverability is proving that your mail is yours. A mailbox provider that can't
authenticate a message has little reason to trust it, and increasingly will reject or quarantine it
outright. Samva lets new accounts send a first test email right away, but production traffic should
come from a [verified sending domain](/docs/channels/email/verify-your-domain). An unverified customer domain
has no established identity, so sends from that domain are rejected.
Verification works by publishing DNS records that mailbox providers check at delivery time. Each record answers a different question:
* **DKIM** attaches a cryptographic signature to every message, published as CNAME records on your domain. The receiving provider verifies the signature against your DNS, confirming the message was authorized by your domain and wasn't altered in transit. Samva signs your outgoing mail with DKIM once the domain is verified.
* **SPF**, published as a TXT record, declares which servers are allowed to send on behalf of your domain. It lets a provider confirm the mail came from a sanctioned source rather than a spoofer.
* **DMARC**, also a TXT record, ties DKIM and SPF together into a policy: it tells providers what to do with mail that fails authentication, and gives you reporting on attempts to send as your domain. It's recommended rather than strictly required, but it's where casual authentication becomes a deliberate posture.
There's one more piece. By default, mail can appear to recipients as sent "via amazonses.com," which weakens the sense that the mail is really yours. Verification includes a **MAIL FROM domain**, an MX record and a TXT record on a subdomain you control, so your mail sends from your own domain and aligns cleanly with your DKIM and SPF identity.
The takeaway is that authentication isn't a formality you complete once and forget. It is the mechanism by which every message you send carries a verifiable claim of origin. Without it, reputation has nothing to attach to.
The exact host and value for each DNS record are generated per domain and shown in your
[dashboard](https://samva.app/dashboard). The [verify your domain](/docs/channels/email/verify-your-domain)
guide walks through publishing them.
## Reputation: bounces, complaints, and suppression [#reputation-bounces-complaints-and-suppression]
Once your identity is established, mailbox providers track how your mail behaves over time. Two signals damage reputation faster than anything else, and Samva is built to keep both of them from compounding.
**Bounces** happen when a message can't be delivered, most importantly hard bounces to addresses that don't exist. A sender that keeps emailing dead addresses looks careless or like a list buyer, and providers respond by throttling or filtering the sender. **Complaints** happen when a recipient marks your mail as spam; a complaint is the strongest possible signal that mail is unwanted, and a rising complaint rate is the quickest route to the spam folder.
Samva handles both automatically. Delivery events flow back from SES: bounces, complaints, and successful deliveries are all captured; addresses that hard-bounce or complain are added to a **suppression list**. Once an address is suppressed, Samva won't send to it again, so a single bad address can't keep accumulating bounces against your reputation. Bounce handling and suppression are managed for you, so you don't have to track dead or complaining addresses yourself.
Underneath this, each organization sends through its own isolated reputation context in SES, so the sending behavior of one tenant doesn't drag down another. Your reputation is yours to build, and it's insulated from everyone else's.
The practical consequence: keep your lists clean and let suppression do its job. Trying to route around a suppressed address, or repeatedly sending to addresses that bounce, works against the very reputation that gets the rest of your mail delivered.
## Engagement: tracking opens and clicks [#engagement-tracking-opens-and-clicks]
Beyond authentication and reputation, mailbox providers increasingly read engagement as a signal of wanted mail. Messages that recipients open, click, and reply to reinforce that your mail belongs in the inbox; messages that are ignored or deleted unread erode that standing over time.
Samva can record **opens** (when a recipient opens a message) and **clicks** (when a recipient clicks a link), and surfaces them alongside delivery status in your dashboard activity log. Engagement tracking is primarily there to give you visibility: it tells you which messages are landing and resonating, and which sends or segments are quietly failing. That feedback is what lets you stop sending mail nobody wants before it costs you reputation.
It's worth being precise about what tracking is and isn't. Open and click data are useful but imperfect: opens depend on a recipient loading remote content, and some providers pre-fetch or block tracking pixels, so the numbers are directional rather than exact. Treat engagement as a trend to watch, not a guarantee, and use it to inform what and how often you send.
## How the pieces fit together [#how-the-pieces-fit-together]
These three forces aren't independent steps; they reinforce each other in order:
1. **Authentication** gives your mail a verifiable identity, so reputation has something durable to attach to.
2. **Reputation**, protected by automatic bounce and complaint suppression, determines whether providers keep delivering your mail.
3. **Engagement** signals from real recipients confirm the mail is wanted and sustain reputation over the long run.
Samva carries the parts that are mechanical: domain authentication during verification, suppression of bounced and complained addresses, and visibility into delivery and engagement. What remains yours is the judgment: sending relevant mail to people who asked for it, at a cadence they welcome. No API can manufacture that, but with the foundations handled, it's the part worth your attention.
## Next steps [#next-steps]
Publish DKIM, SPF, and DMARC records to authenticate your mail, the first deliverability step.
See how sending, delivery events, and tracking fit into the wider system.
# Schedule an email
URL: https://samva.app/docs/channels/email/schedule-an-email
Use a scheduled message when the recipients and content are already known and the email should send
later. This is a separate resource from an immediate send. `messages.send` has no scheduling field.
## Choose the time and content [#choose-the-time-and-content]
`scheduledFor` must be a future absolute ISO-8601 instant with `Z` or a numeric offset, for example
`2026-08-01T09:00:00Z` or `2026-08-01T14:30:00+05:30`. A local date-time without an offset is rejected.
`timezone` is optional validated IANA metadata for display and audit. It never converts or
reinterprets `scheduledFor`.
Choose exactly one content source:
* Inline email requires `subject` and `html`; `text` is an optional fallback.
* Template email references exactly one published template by `templateSlug` or `templateId`, with
optional `templateData`. Omit inline `subject`, `html`, and `text`.
## Use the Promise SDK [#use-the-promise-sdk]
```typescript title="schedule-email.ts"
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const scheduled = await samva.scheduledMessages.create({
send: {
to: [{ email: "ada@example.com" }],
channel: "email",
email: {
subject: "Your appointment is tomorrow",
html: "
We will see you at 9:00.
",
text: "We will see you at 9:00.",
},
},
scheduledFor: "2026-08-01T09:00:00Z",
timezone: "America/New_York",
idempotencyKey: "appointment-ada-2026-08-01",
});
if (scheduled.error) throw new Error("Could not schedule email");
await samva.scheduledMessages.cancel({ id: scheduled.data.id });
```
## Use the Effect SDK [#use-the-effect-sdk]
```typescript title="schedule-email-effect.ts"
import { Effect } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import { createClient } from "samva/effect";
const program = Effect.gen(function* () {
const samva = yield* createClient({ apiKey: process.env.SAMVA_API_KEY! });
const scheduled = yield* samva.scheduledMessages.create({
payload: {
send: {
to: [{ email: "ada@example.com" }],
channel: "email",
email: { subject: "Reminder", html: "
" }
},
"scheduledFor": "2026-08-01T09:00:00Z",
"timezone": "America/New_York",
"idempotencyKey": "reminder-ada-2026-08-01"
}'
```
Use `GET /v1/messages/scheduled/{id}` to inspect the schedule and
`POST /v1/messages/scheduled/{id}/cancel` to cancel it. See the
[generated scheduled messages reference](/docs/api-reference/scheduledMessages/create) for schemas
and response fields.
## Use the CLI [#use-the-cli]
```bash
samva scheduled create --to ada@example.com \
--subject "Reminder" --html "
See you tomorrow.
" \
--at 2026-08-01T09:00:00Z --timezone America/New_York \
--idempotency-key reminder-ada-2026-08-01
samva scheduled get
samva scheduled cancel
```
Use `--template-slug --template-data ''` instead of the inline content flags to send a
published template. `samva scheduled create --dry-run` validates and prints the request without
calling the API.
## Use MCP [#use-mcp]
Ask the agent to call `samva_scheduled_messages_schedule_email` with `to`, one valid content source,
and `scheduledFor`. It can inspect the result with `samva_scheduled_messages_get` or
`samva_scheduled_messages_list`, and cancel it with `samva_scheduled_messages_cancel`.
## Cancellation, errors, and usage [#cancellation-errors-and-usage]
A scheduled email can be cancelled only while its status is `pending`. Once dispatch begins,
cancellation returns a conflict. At dispatch, it follows the normal email send path and usage
semantics. Validation failures do not create a schedule; provider and delivery outcomes appear on
the resulting message after dispatch. Handle authentication, validation, not-found, conflict, and
usage-limit errors as described in the [error reference](/docs/developers/error-reference).
# Send an email campaign
URL: https://samva.app/docs/channels/email/send-a-campaign
Use a campaign for a named broadcast whose recipients come from contact IDs or tags. Creating a
campaign sends nothing. A run snapshots the definition and starts immediately or at a future time.
## Define the campaign [#define-the-campaign]
Campaigns are email-only and support at most 25,000 resolved recipients per run. Choose exactly one
content source:
* Inline email requires `subject` and `html`; `text` is an optional fallback.
* Template email references exactly one published template by `templateSlug` or `templateId`, with
optional `templateData`. Do not mix it with inline `subject`, `html`, or `text`.
The audience must use `includeTags` or `contactIds` as its source. `excludeTags` removes matches.
Contacts resolve when the run starts. After the first run is created, the campaign definition is
frozen and updates return a conflict.
## Use the Promise SDK [#use-the-promise-sdk]
```typescript title="send-campaign.ts"
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const campaign = await samva.campaigns.create({
name: "August newsletter",
channel: "email",
content: {
channel: "email",
email: { subject: "What's new", html: "
August updates
" },
},
audience: { includeTags: ["newsletter"], excludeTags: ["bounced"] },
});
if (campaign.error) throw new Error("Could not create campaign");
const run = await samva.campaigns.scheduleRun({
id: campaign.data.id,
scheduledFor: "2026-08-01T09:00:00Z",
idempotencyKey: "august-newsletter-v1",
});
if (run.error) throw new Error("Could not schedule campaign");
```
## Use the Effect SDK [#use-the-effect-sdk]
```typescript title="send-campaign-effect.ts"
import { Effect } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import { createClient } from "samva/effect";
const program = Effect.gen(function* () {
const samva = yield* createClient({ apiKey: process.env.SAMVA_API_KEY! });
const campaign = yield* samva.campaigns.create({
payload: {
name: "August newsletter",
channel: "email",
content: {
channel: "email",
email: { subject: "What's new", html: "
" }
},
"audience": { "includeTags": ["newsletter"] }
}'
curl -X POST https://api.samva.app/v1/campaigns//runs \
-H "X-API-Key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduledFor": "2026-08-01T09:00:00Z",
"idempotencyKey": "august-newsletter-v1"
}'
```
Omit `scheduledFor` to start immediately. When present, it must be an absolute ISO-8601 instant
with `Z` or a numeric offset. Campaign runs do not accept timezone metadata. See the
[generated campaigns reference](/docs/api-reference/campaigns/create) for every operation.
## Use the CLI [#use-the-cli]
```bash
samva campaigns create --name "August newsletter" \
--subject "What's new" --html "
August updates
" \
--include-tags newsletter
samva campaigns runs schedule --at 2026-08-01T09:00:00Z \
--idempotency-key august-newsletter-v1
samva campaigns runs get
samva campaigns runs recipients --status failed
```
Use `samva campaigns create --dry-run` to validate the definition without creating it. Use
`samva campaigns runs pause`, `resume`, or `cancel` with the campaign ID and run ID to control a run.
## Use MCP [#use-mcp]
Call `samva_campaigns_create`, then `samva_campaigns_schedule_run`. Track progress with
`samva_campaigns_get_run` and inspect outcomes with `samva_campaigns_list_recipients`. The
`samva_campaigns_control_run` tool accepts `pause`, `resume`, or `cancel`.
## Idempotency, controls, errors, and usage [#idempotency-controls-errors-and-usage]
Pass an `idempotencyKey` when scheduling a run. It is scoped to the organization, and replaying the
same key returns the original run. Pausing stops new dispatch and resuming safely re-enqueues
unsettled recipients. Cancelling a future scheduled run is terminal. Cancellation after dispatch
begins is best effort, so already claimed or provider-submitted deliveries may finish.
Each recipient follows the normal email send and usage path when dispatched. Run totals distinguish
dispatched, failed, and skipped recipients; recipient records carry the outcome. Invalid state
transitions and edits after the first run return conflicts. See the
[error reference](/docs/developers/error-reference) for shared API error handling.
# Send an email
URL: https://samva.app/docs/channels/email/send-an-email
This guide shows the fastest way to send an email with Samva, from a single line of
TypeScript to a raw REST call in the language of your choice. Each recipe is self-contained;
pick the one that matches your stack.
You need a Samva API key. Grab one from your [dashboard](https://samva.app/dashboard).
Brand-new accounts can send a first test email right away; verify a sending domain before
you send from your own address or move real traffic into production. See
[Verify your domain](/docs/channels/email/verify-your-domain) when you're ready to set that up.
## Send with the TypeScript SDK [#send-with-the-typescript-sdk]
The `email.send()` facade is the canonical path: pass the recipient, subject, and HTML body
directly and you're done.
1. Install the SDK.
npm
bun
yarn
```bash
npm install samva
```
```bash
bun add samva
```
```bash
yarn add samva
```
2. Create a client and send the email.
```typescript
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const result = await samva.email.send({
to: "ada@example.com",
cc: "grace@example.com",
replyTo: "support@example.com",
subject: "Welcome to Samva",
html: "
Welcome!
Thanks for joining.
",
});
if (result.error) {
console.error("Failed to send:", result.error);
} else {
console.log("Email sent:", result.data?.id);
}
```
The `to`, `cc`, and `bcc` fields accept a single value or an array, and each entry can be a plain email
string, `{ email }`, or `{ contactId }`. You can also pass `replyTo`, `text` for a plaintext fallback,
plus optional fields like `attachments`, `templateId`, `templateData`, and `metadata`. See the
[TypeScript SDK reference](/docs/developers/typescript-sdk) for the full surface.
## Send to an existing contact or multiple recipients [#send-to-an-existing-contact-or-multiple-recipients]
`email.send()` is a thin facade over the unified `messages.send()` API. Reach for
`messages.send()` directly when you need to address existing contacts by `contactId`, send to
multiple recipients, or thread into an existing conversation. Here `to` is always an array and
the email content is nested under `email`.
```typescript
await samva.messages.send({
to: [
{ contactId: "9b2c0e3a-7d41-4f2e-8a16-1f2c3d4e5f60" },
{ email: "grace@example.com" },
],
cc: [{ email: "ops@example.com" }],
channel: "email",
email: {
subject: "Welcome to Samva",
html: "
Welcome!
Thanks for joining.
",
replyTo: "support@example.com",
},
});
```
For why the facade and the unified API exist side by side, see
[Email and the unified API](/docs/developers/email-and-the-unified-api).
## Send with the REST API [#send-with-the-rest-api]
There is no `email.send` HTTP endpoint; the SDK facade is sugar over `POST /v1/messages`.
Every REST call uses that endpoint with the unified body shape: a `channel` of `email` and the
content nested under `email`. Authenticate with the `X-API-Key` header.
cURL
Python
Go
PHP
```bash
curl -X POST https://api.samva.app/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"to": [{ "email": "ada@example.com" }],
"cc": [{ "email": "grace@example.com" }],
"channel": "email",
"email": {
"subject": "Welcome to Samva",
"html": "
Welcome!
Thanks for joining.
",
"text": "Welcome! Thanks for joining.",
"replyTo": ["support@example.com"]
}
}'
```
To address an existing contact, swap `email` for `contactId` in the `to` array; both forms
hit the same endpoint:
```bash
curl -X POST https://api.samva.app/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"to": [{ "contactId": "9b2c0e3a-7d41-4f2e-8a16-1f2c3d4e5f60" }],
"channel": "email",
"email": { "subject": "Welcome to Samva", "html": "
Welcome!
" }
}'
```
```python
import requests
headers = {
"X-API-Key": "samva_sk_live_your_api_key",
"Content-Type": "application/json",
}
response = requests.post(
"https://api.samva.app/v1/messages",
headers=headers,
json={
"to": [{"email": "ada@example.com"}],
"channel": "email",
"email": {
"subject": "Welcome to Samva",
"text": "Welcome! Thanks for joining.",
},
},
)
print(response.json())
```
```go
package main
import (
"bytes"
"encoding/json"
"net/http"
)
func main() {
payload := map[string]any{
"to": []map[string]string{{"email": "ada@example.com"}},
"channel": "email",
"email": map[string]string{
"subject": "Welcome to Samva",
"text": "Welcome! Thanks for joining.",
},
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST",
"https://api.samva.app/v1/messages",
bytes.NewBuffer(body))
req.Header.Set("X-API-Key", "samva_sk_live_your_api_key")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}
```
```php
'https://api.samva.app/v1/messages',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: samva_sk_live_your_api_key',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'to' => [['email' => 'ada@example.com']],
'channel' => 'email',
'email' => [
'subject' => 'Welcome to Samva',
'text' => 'Welcome! Thanks for joining.',
],
]),
]);
$response = curl_exec($ch);
curl_close($ch);
print_r(json_decode($response, true));
```
A successful call returns the new message with its `id` and `status`:
```json
{
"id": "4f8e1c2a-3b6d-4e9f-a012-3456789abcde",
"status": "pending"
}
```
For every parameter, status code, and pagination detail, see the
[REST API reference](/docs/developers/rest-api).
## Next steps [#next-steps]
# Verify your sending domain
URL: https://samva.app/docs/channels/email/verify-your-domain
Before you can send email from your own address, the sending domain must be verified in Samva. Verification proves you own the domain and lets Samva sign your mail with DKIM so it lands in the inbox. This guide takes you from adding a domain to a verified status.
New accounts can start sending right away on a Samva-provided domain. To send from your own
address, verify the domain first.
## Before you start [#before-you-start]
You need:
* A Samva account and access to your organization's [dashboard](https://samva.app/dashboard).
* A domain you control, with access to its DNS settings (at your registrar or DNS host).
## 1. Add your domain [#1-add-your-domain]
1. In the dashboard, go to **Email → Domains**.
2. Choose to add a domain and enter the domain you want to send from (for example, `yourdomain.com`).
3. Save it. Samva creates the domain record and generates the DNS records you'll publish next.
## 2. Publish the DNS records [#2-publish-the-dns-records]
Samva returns a set of DNS records for the domain. Add each one at your DNS host exactly as shown in the dashboard. You'll typically see:
* A **TXT** record that verifies domain ownership.
* Three **CNAME** records for DKIM, which let Samva cryptographically sign your outgoing mail.
* **MX** and **TXT** (SPF) records for a custom MAIL FROM subdomain, so your mail sends from your own domain rather than a shared one.
* A **TXT** record for a DMARC policy (recommended).
Copy the host and value for each record from the dashboard rather than retyping them. The exact names and values are specific to your domain. To learn what each record does and why it matters for inbox placement, see [Deliverability](/docs/channels/email/deliverability).
## 3. Wait for verification [#3-wait-for-verification]
DNS changes take time to propagate, often minutes, sometimes up to a few hours, depending on your provider. Samva checks for the records and updates the domain's status once it detects them.
You don't need to keep the page open. Come back to **Email → Domains** to check progress.
## 4. Confirm the status [#4-confirm-the-status]
1. Return to **Email → Domains**.
2. Find your domain and confirm its status shows as verified.
Once the domain is verified, you can send from any address on it.
If the domain hasn't verified after a few hours, double-check that each DNS record's host and value match the dashboard exactly, with no extra characters or trailing dots added by your DNS host. Then wait for the next check.
## Next steps [#next-steps]
With a verified domain in place, send an email from your own address.
Understand DKIM, SPF, DMARC, and how verification protects inbox placement.
# Email guides
URL: https://samva.app/docs/channels
Use Samva for transactional, conversational, scheduled, and campaign email. Every email shares the
same contacts, conversations, templates, and delivery events.
# Agent Skills
URL: https://samva.app/docs/developers/agent-skills
An **Agent Skill** is a portable instruction file (`SKILL.md`) that teaches an AI agent how to
do something correctly. Samva publishes a `samva` skill so that agents working in your codebase
or terminal, not just ones connected to the [hosted MCP server](/docs/developers/mcp), know how to
integrate Samva email the right way.
## What the skill teaches [#what-the-skill-teaches]
The `samva` skill routes an agent to the right surface for the job, then to a focused reference.
* **Choosing a surface**: Promise SDK for async/await TypeScript, Effect SDK for native Effect
applications, REST for other runtimes, CLI for terminal work, MCP for live agent actions, and
the dashboard for visual setup and inspection.
* **Authentication**: API keys versus OAuth login, and how each scopes the organization.
* **Email operations**: sends and status, domains and senders, templates, scheduled email,
campaigns, inbound receiving, webhooks, usage, and readiness checks.
* **Safe automation**: keyed retries for sends and revision-safe template editing.
## Discovery [#discovery]
Samva serves the skill for automatic discovery following the
[Cloudflare Agent Skills discovery format](https://github.com/cloudflare/agent-skills-discovery-rfc).
A skills-capable client fetches the index and then the skill archive:
| Resource | URL |
| --------------- | --------------------------------------------------------- |
| Discovery index | `https://samva.app/.well-known/agent-skills/index.json` |
| Skill archive | `https://samva.app/.well-known/agent-skills/samva.tar.gz` |
The index lists the `samva` skill with a `sha256` digest of the archive. Clients verify the
digest after download, so they only ever load the exact bytes Samva published.
```json
{
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
"skills": [
{
"name": "samva",
"type": "archive",
"description": "Integrate Samva for transactional email…",
"url": "samva.tar.gz",
"digest": "sha256:…"
}
]
}
```
## Agent Skills versus MCP [#agent-skills-versus-mcp]
Both help an agent work with Samva, through different mechanisms:
| | Agent Skill | Hosted MCP |
| ------------------ | --------------------------------------------- | --------------------------- |
| **What it is** | Instructions an agent reads | Tools an agent calls |
| **When it helps** | Deciding *how* to use Samva and setting it up | Taking actions at runtime |
| **Where it lives** | Discoverable `SKILL.md` archive | `https://mcp.samva.app/mcp` |
A typical agent loads the skill to decide which surface to use and how to authenticate, then acts
through MCP, a TypeScript SDK, REST, the CLI, or the dashboard.
## Next steps [#next-steps]
# Organizations and tenancy
URL: https://samva.app/docs/developers/authentication-and-tenancy
Every request to Samva's email API answers two questions before anything else happens: *who is asking*, and *whose data are they allowed to touch*. An API key answers the first. Tenancy answers the second. This page is about how a key binds to an organization and how that organization becomes the boundary around everything you send and store.
This page is about the model, not the mechanics. For key formats, the `X-API-Key` header, rate limits, and error shapes, see the [API keys reference](/docs/developers/authentication). For choosing between a durable key and a human OAuth session, see [API keys and OAuth sessions](/docs/developers/oauth-device-login).
## The organization is the unit of tenancy [#the-organization-is-the-unit-of-tenancy]
Samva is multi-tenant, and the tenant is the **organization**. An organization is the boundary that owns everything: verified domains, contacts, conversations, messages, webhooks, and the API keys themselves. When you sign up, you get an organization; when you invite teammates, they join *your* organization; when you send an email, it is recorded against your organization.
This matters because an API key is not a free-floating secret; it belongs to exactly one organization. That binding is what makes authentication and tenancy collapse into a single step at request time. Samva does not need a separate "which account?" parameter on your calls, because the key already answers it. Present a key, and Samva knows both that the request is legitimate *and* whose data it is permitted to read or write.
The deeper consequence is an invariant that runs through the entire system: **every record is scoped to an organization, and every read is filtered by it.** There is no request shape that crosses the tenant boundary. A key issued to one organization cannot list another organization's contacts, fetch another organization's messages, or send from another organization's domain, not because a check happens to be in place on each endpoint, but because tenant scoping is structural. Data is partitioned by organization at the storage layer, and the organization is derived from your credential rather than from anything you pass in. You cannot ask for the wrong tenant's data, because there is no field in which to ask.
This is also why a verified domain is an organization-level asset. The work of authenticating a sending domain, covered in [deliverability](/docs/channels/email/deliverability), is done once per organization, and from then on every key in that organization can send from it. The trust you build attaches to the tenant, not to an individual credential.
## Teams and roles [#teams-and-roles]
Because the organization owns the data and the credentials, it is also where collaboration lives. You invite teammates *into* an organization and assign each one a role. Roles exist so that the people who manage billing and members are not necessarily the same people who write integration code or who only need to watch what is happening.
The point of roles is least privilege: a teammate who only needs to read delivery activity should not also be able to rotate the API keys that production depends on. Splitting responsibilities this way limits the blast radius of any single compromised account, a principle that mirrors, at the human level, the same instinct behind scoping keys narrowly at the machine level. Roles and their exact capabilities are managed in the dashboard.
In practice this collaboration happens through invitation rather than shared credentials. From the dashboard you invite a teammate by their email address and assign them a role; they receive an email invitation to join the organization, and from then on they authenticate as themselves with their own session rather than borrowing anyone else's. That is the human-side counterpart to never sharing an API key: each person has a distinct identity and a role that bounds what they can do.
## Usage is organization-scoped [#usage-is-organization-scoped]
Billing follows the same boundary as everything else: usage is metered against the organization, not the individual key. Every send your organization makes is tracked against its allowance, and when an organization exceeds what it is entitled to, the API returns a `402 Payment Required` rather than silently sending.
The practical consequence is that cost visibility lives in one place. Every key and every teammate counts against the same organization-level usage, so spend is not fragmented across credentials; there is a single pool to watch and a single limit to manage, regardless of how many keys an organization has issued.
## Security posture [#security-posture]
Authentication proves who you are; the surrounding security features narrow *how* and *from where* that identity can be used, and they verify that traffic claiming to come from Samva actually does.
### IP allowlisting [#ip-allowlisting]
By default an API key works from anywhere it is presented. For organizations that send only from known infrastructure, that is more surface area than necessary; a leaked key is useful to an attacker from any network. IP allowlisting closes that gap by restricting a key's use to a defined set of addresses or ranges. It is defense in depth: the key remains the primary credential, and the allowlist is a second condition that an attacker on an unknown network cannot satisfy even if they hold a valid key. You enable and configure it in the dashboard.
### Webhook signatures [#webhook-signatures]
Webhooks invert the usual direction of trust. Normally your server authenticates *to* Samva; with webhooks, Samva calls *your* endpoint, and now your server is the one that must decide whether to trust the caller. A public webhook URL can be hit by anyone who discovers it, so "this request arrived at my endpoint" is not evidence it came from Samva.
Signatures resolve this. Samva signs each webhook delivery with a shared secret, and your handler recomputes the signature over the received payload and compares. A match proves two things at once: the payload was produced by someone holding the secret (so it is genuinely from Samva), and it was not altered in transit (so the events you act on are the events Samva sent). This is why signature verification is the first thing a webhook handler should do, before parsing or acting on any event. The mechanics of verifying a signature, including the constant-time comparison that avoids leaking information through timing, are covered in [Receive webhooks](/docs/platform/webhooks).
## Where to go next [#where-to-go-next]
# API keys
URL: https://samva.app/docs/developers/authentication
Samva authenticates email API requests with an API key sent in the `X-API-Key` header. This page documents key format, request authentication, rate limiting, and authentication errors. For the model behind keys, how one binds to an organization and scopes every request, see [Organizations and tenancy](/docs/developers/authentication-and-tenancy).
## API key format [#api-key-format]
Production keys start with `samva_sk_live_`; keys minted outside production start with
`samva_sk_test_`. Each key authenticates requests for the organization it belongs to.
## Using API keys [#using-api-keys]
Include the key in the `X-API-Key` header on every request.
The SDK reads the key from the `apiKey` option and sets the header for you.
```typescript
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
```
```bash
curl -X POST https://api.samva.app/v1/messages \
-H "X-API-Key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": [{ "email": "ada@example.com" }],
"channel": "email",
"email": {
"subject": "Welcome to Samva",
"html": "
Welcome!
"
}
}'
```
Store keys in environment variables, never in source. The full key value is shown only once at
creation time.
## Key handling [#key-handling]
| Practice | Detail |
| ------------------------- | -------------------------------------------------------------------------- |
| Use environment variables | Read the key from the environment; never hardcode it in source. |
| Scope permissions | Set the minimum permissions required when creating a key. |
| Rotate keys | Replace keys periodically and after any suspected exposure. |
| Monitor usage | Track per-key usage in the [Samva Dashboard](https://samva.app/dashboard). |
## Key lifecycle [#key-lifecycle]
Keys are created and managed under **Developers → API Keys** in the [Samva Dashboard](https://samva.app/dashboard). Each key carries a set of permissions and an optional expiry, both set at creation. The full key value is returned only at creation time and is not retrievable afterward; a lost key must be replaced. Keys can be revoked at any time, and a revoked key stops authenticating immediately.
## Rate limiting [#rate-limiting]
Send throughput is rate limited per organization. When a limit is exceeded, the request returns `429 Too Many Requests` with a `RateLimitedError` body. The `retryAfterSeconds` field tells you how long to wait before retrying.
```json
{
"_tag": "RateLimitedError",
"retryAfterSeconds": 30
}
```
Back off for at least `retryAfterSeconds` seconds before sending the request again.
## Authentication errors [#authentication-errors]
Errors are returned as tagged JSON bodies. Each body carries a `_tag` identifying the error and a `message` describing it.
| Status | `_tag` | Description | Resolution |
| ------ | ------------------- | -------------------------- | ---------------------------------- |
| `401` | `UnauthorizedError` | Invalid or missing API key | Check the `X-API-Key` header value |
| `403` | `ForbiddenError` | Insufficient permissions | Use a key with the required scope |
### Invalid or missing API key [#invalid-or-missing-api-key]
Returned when the API key cannot be validated.
```json
{
"_tag": "UnauthorizedError",
"message": "Invalid API key"
}
```
### Insufficient permissions [#insufficient-permissions]
Returned when the key is valid but not authorized for the request, for example a key without the scope the endpoint requires.
```json
{
"_tag": "ForbiddenError",
"message": "API key lacks the required permission"
}
```
These are the authentication-specific errors. For the full list the API can return, with each
error's fields and how to resolve it, see the [Error reference](/docs/developers/error-reference).
## Related [#related]
# Use the CLI
URL: https://samva.app/docs/developers/cli
The `samva` CLI drives the same email API from your terminal, handy for trying things out,
scripting, and working across multiple organizations without writing code.
## Install [#install]
```bash
npm install -g @samva/cli
```
npm
yarn
pnpm
bun
```bash
npm install -g @samva/cli
```
```bash
yarn global add @samva/cli
```
```bash
pnpm add -g @samva/cli
```
```bash
bun add -g @samva/cli
```
Once installed, the `samva` command is on your `PATH`.
## Authenticate [#authenticate]
The CLI accepts either of Samva's two credential types. For when to use which, see
[API keys and OAuth sessions](/docs/developers/oauth-device-login).
OAuth login
API key
Sign in interactively with the OAuth device flow; it opens your browser to approve the
session:
```bash
samva login # add --no-browser to print the URL instead
samva logout # sign out and clear the stored session
```
For non-interactive use, set an API key in the environment:
```bash
export SAMVA_API_KEY="samva_sk_live_your_api_key"
```
When both are present, `SAMVA_API_KEY` takes precedence so CI and agent automation stay
deterministic. OAuth is the interactive fallback. Sessions have no refresh token; run
`samva login` again after one expires.
## Select an organization [#select-an-organization]
An API key is already scoped to one organization. An OAuth session is tied to your user, so it
can act on any organization you belong to; pick the active one:
```bash
samva org list
samva org use
samva org current
```
The active organization is remembered between commands and sent as the `x-org-slug` header.
## Send an email and check status [#send-an-email-and-check-status]
```bash
samva email send \
--to ada@example.com \
--cc grace@example.com \
--reply-to support@example.com \
--subject "Welcome to Samva" \
--html "
Welcome!
"
samva email status --message-id
```
* `--to` is repeatable to address multiple recipients.
* Use `--cc`, `--bcc`, and `--reply-to` for copy recipients and Reply-To headers.
* Provide exactly one body: `--html`, `--text`, or `--template-id` (with optional
`--template-data ''`).
* `--reply-to-message-id ` threads the email into an existing conversation.
* Add `--json` to any command to print raw JSON for scripting.
## Manage domains and senders [#manage-domains-and-senders]
```bash
samva email domains list
samva email domains add --domain # dry-run by default; pass --execute to apply
samva email domains check --id # check verification status
samva email senders list
samva email senders add --email # dry-run by default; pass --execute to apply
samva email senders check --id
```
See [Verify your domain](/docs/channels/email/verify-your-domain) for the full DNS walkthrough.
## Schedule email and run campaigns [#schedule-email-and-run-campaigns]
The CLI exposes the complete email scheduling lifecycle. Use the task guides for the exact
commands, content rules, time semantics, and cancellation behavior.
## Run a channel proof [#run-a-channel-proof]
The proof runners walk the email channel end to end, useful before you ship. Both
default to a dry run that lists the checks; pass `--execute` to run them:
```bash
samva email proof local --execute # against a local or dev API
samva email proof production --execute --yes # production needs both flags
```
## Configuration [#configuration]
The CLI keeps state under `~/.config/samva/`: `config.json` for preferences (including the
active organization) and `credentials.json` (mode `0600`) for the OAuth session token. Override
the API base URL with the `SAMVA_API_URL` environment variable.
## Next steps [#next-steps]
# Email and the unified API
URL: https://samva.app/docs/developers/email-and-the-unified-api
Samva gives you two ways to send email: a small, email-shaped `email.send()` facade and a
broader `messages.send()` API that maps directly onto `POST /v1/messages`. They are not competing
products or different transports; they are two views of the same send pipeline. This page explains
why both exist, how they relate, and what that design buys you over time.
## Two shapes, one pipeline [#two-shapes-one-pipeline]
Underneath, every send Samva performs is a message: a piece of content, a recipient, and a channel
that carries it. The unified API names those parts explicitly. When you call `messages.send()`, you
declare the `channel`, list your recipients in `to`, and nest the channel-specific content under a
matching key:
```typescript
await samva.messages.send({
to: [{ contactId: "contact_123" }],
cc: [{ email: "grace@example.com" }],
channel: "email",
email: {
subject: "Welcome to Samva",
html: "
Welcome!
Thanks for joining.
",
replyTo: "support@example.com",
},
});
```
The `email.send()` facade is the same call wearing email-shaped clothes. It assumes the channel is
email, flattens the email content up to the top level of the input, and accepts a single recipient
or an array. The result is a call that reads like sending an email rather than configuring a generic
message:
```typescript
const result = await samva.email.send({
to: "ada@example.com",
cc: "grace@example.com",
replyTo: "support@example.com",
subject: "Welcome to Samva",
html: "
Welcome!
",
});
```
There is no separate `email.send` HTTP endpoint. The facade is an SDK-only ergonomic wrapper: it
builds the unified body shape for you and posts it to `POST /v1/messages`. From the server's point of
view, both snippets above are the same request to the same endpoint. The difference lives entirely in
the SDK, in service of the developer reading the code.
## Why a facade at all [#why-a-facade-at-all]
Most email you send is straightforward. You have an address, a subject, and some HTML, and you want
the call site to say exactly that. Forcing every send to spell out `channel: "email"` and nest content
under an `email:` key adds ceremony to the common case without adding meaning; the channel is obvious
when you reached for `email.send()`.
The facade optimizes for that common case. Flattening the email fields directly into the SDK input keeps the
simple path short and removes a layer of nesting you would otherwise repeat on every send. It is the
canonical way to send a one-off transactional email, and it is what the [send an email
guide](/docs/channels/email/send-an-email) walks through.
That convenience is deliberately narrow. The facade does not hide the unified API or replace it; it
sits on top of it. When your needs outgrow "one address, one subject, one body," you drop down to the
API the facade was built on, and nothing about the underlying send changes.
## When to reach for the unified API [#when-to-reach-for-the-unified-api]
Reach for `messages.send()` (or `POST /v1/messages` directly) when the send is no longer a single
self-contained email:
* **Addressing existing contacts.** The unified `to` array accepts `{ contactId }`, so you can send to
a [contact](/docs/api-reference) you already store in Samva without re-supplying their email address.
* **Multiple recipients.** When you are addressing a list rather than one person, the array form of
`to` is the natural fit, with `cc` and `bcc` available when you need mail-style copy semantics.
* **Threading a reply.** When a send belongs to an existing conversation, the unified body is where you
express that relationship so the message lands in the right thread.
The dividing line is conceptual, not performance- or feature-based: the facade is for "send this email
to this address," and the unified API is for everything that needs to talk about messages, recipients,
contacts, and conversations as first-class things. Because the facade compiles down to the same
request, you can start with `email.send()` and move to `messages.send()` later without rethinking how
your email is delivered.
## REST is always the unified shape [#rest-is-always-the-unified-shape]
The split between a facade and a unified API is an SDK convenience. It does not exist over the wire.
Every REST integration, whether or not you use the TypeScript SDK, sends to `POST /v1/messages`
using the unified body: a `channel`, a `to` array, and channel content nested under its key. If you
are calling the API directly, there is one shape to learn, and the SDK's `email.send()` is simply
producing that shape on your behalf.
This is why the [REST reference](/docs/developers/rest-api) and the [TypeScript SDK
reference](/docs/developers/typescript-sdk) can describe the same capability from two angles without
contradiction: one documents the unified endpoint, the other documents the ergonomics layered over it.
## Why the shape is a channel discriminator [#why-the-shape-is-a-channel-discriminator]
The word "unified" in the unified API describes the send model, not just email. Modeling a send as a
`channel` plus channel-specific content is what lets `POST /v1/messages` address more than one kind of
message through a single endpoint. `channel: "email"` names the channel explicitly, and the `email: {...}`
block carries the subject, HTML, and text; that same shape, a `channel` value paired with its own content
block, is what any other channel addresses through the same `to` array and the same endpoint.
Crucially, that generality does not touch the email path. The `email.send()` facade is pinned to email by
definition, so its behavior, its fields, and what it sends never change. Code written against
`email.send()` keeps working unchanged no matter what else the unified shape carries. The unified API
absorbs the general case; the email facade stays exactly as small as it is today.
This is the tradeoff the two-API design resolves. A single generic API would make every email verbose.
A single email-only API would have no room for a channel discriminator at all. By offering the facade for
the common case and the unified API as its foundation, Samva keeps the simple thing simple while
keeping the underlying model general.
## Related [#related]
# Error reference
URL: https://samva.app/docs/developers/error-reference
Every Samva API error is returned as a flat JSON body. A `_tag` field carries the machine-readable
discriminator, and the remaining fields are specific to that error type. There is no wrapper
object; the fields sit at the top level of the response. The `_tag` value determines the HTTP
status code. For the request and response conventions around errors, see the
[REST API reference](/docs/developers/rest-api).
```json
{
"_tag": "ValidationError",
"message": "Invalid request body",
"fields": {
"to": ["Must be a valid email address"]
}
}
```
## Error types [#error-types]
| `_tag` | Status | Cause | How to resolve |
| ----------------------- | ------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `ValidationError` | `422` | The request body or parameters failed validation. | Read `fields` for per-field detail and correct the request. |
| `UnauthorizedError` | `401` | The API key is missing or invalid. | Check the `X-API-Key` header carries a valid, unrevoked key. |
| `ForbiddenError` | `403` | The key is valid but lacks permission for the resource. | Use a key whose permissions include the required scope. |
| `PaymentRequiredError` | `402` | A plan usage limit or entitlement blocks the request. | Reduce usage or upgrade the plan; `currentUsage` and `limit` show the ceiling. |
| `ResourceNotFoundError` | `404` | The referenced resource does not exist. | Check the id; `resource` and `id` name what was not found. |
| `ConflictError` | `409` | The request conflicts with existing state (e.g. duplicate). | Reconcile with the existing resource named by `resource`. |
| `RateLimitedError` | `429` | The organization exceeded its request rate limit. | Back off for `retryAfterSeconds`, then retry. |
| `FlagDisabledError` | `403` | A feature required by the request is not enabled. | The feature is unavailable for the organization; no client-side fix. |
| `WebhookEnqueueError` | `500` | The message was accepted but its webhook could not be queued. | Transient; the message send itself succeeded. Retry-safe on the client. |
| `ExternalServiceError` | `502` | An upstream provider returned an error. | Transient upstream fault; retry with backoff. `provider` names the source. |
| `BillingProviderError` | `502` | The billing provider returned an error. | Transient; retry with backoff. |
| `InternalError` | `500` | An unexpected server error. | Retry with backoff; if it persists, contact support. |
## Response fields [#response-fields]
Each error carries only the fields listed for its type. Common fields:
* `message` is present on `ValidationError`, `UnauthorizedError`, `ForbiddenError`, `ConflictError`,
`ExternalServiceError`, `BillingProviderError`, and `InternalError`.
* `resource` and `id` are present on `ResourceNotFoundError`.
* `fields` is an optional map of field name to error messages on `ValidationError`.
* `retryAfterSeconds` is present on `RateLimitedError`.
* `currentUsage` and `limit` are present on `PaymentRequiredError`.
* `provider` is present on `ExternalServiceError` and `BillingProviderError`.
* `flag` is present on `FlagDisabledError`.
* `reason` is present on `WebhookEnqueueError`.
### ValidationError (422) [#validationerror-422]
Returned when the request body or parameters fail schema validation. The optional `fields` map
lists per-field error messages.
```json
{
"_tag": "ValidationError",
"message": "Invalid request body",
"fields": {
"to": ["Must be a valid email address"]
}
}
```
### UnauthorizedError (401) [#unauthorizederror-401]
Returned when the API key cannot be validated. See [API keys](/docs/developers/authentication).
```json
{
"_tag": "UnauthorizedError",
"message": "Invalid API key"
}
```
### ForbiddenError (403) [#forbiddenerror-403]
Returned when the key is valid but not authorized for the request.
```json
{
"_tag": "ForbiddenError",
"message": "API key lacks the required permission"
}
```
### PaymentRequiredError (402) [#paymentrequirederror-402]
Returned when a plan usage limit or entitlement blocks the request. `currentUsage` and `limit`
describe the ceiling reached for `resource`.
```json
{
"_tag": "PaymentRequiredError",
"resource": "messages",
"currentUsage": 10000,
"limit": 10000
}
```
### ResourceNotFoundError (404) [#resourcenotfounderror-404]
Returned when the referenced resource does not exist. `resource` and `id` identify what was not
found.
```json
{
"_tag": "ResourceNotFoundError",
"resource": "Message",
"id": "3f2a9c1e-7b4d-4e8a-9c2f-1a2b3c4d5e6f"
}
```
### ConflictError (409) [#conflicterror-409]
Returned when the request conflicts with existing state, such as a duplicate. `resource` names the
conflicting resource, and `id` identifies it when applicable.
```json
{
"_tag": "ConflictError",
"message": "A webhook with this URL already exists",
"resource": "webhook"
}
```
### RateLimitedError (429) [#ratelimitederror-429]
Returned when the organization exceeds its request rate limit. `retryAfterSeconds` tells you how
long to wait before retrying.
```json
{
"_tag": "RateLimitedError",
"retryAfterSeconds": 30
}
```
### FlagDisabledError (403) [#flagdisablederror-403]
Returned when the request requires a feature that is not enabled for the organization. `flag`
names the feature.
```json
{
"_tag": "FlagDisabledError",
"flag": "campaigns"
}
```
### WebhookEnqueueError (500) [#webhookenqueueerror-500]
Returned when a message was accepted but Samva could not enqueue its webhook delivery. The send
itself succeeded; this reflects a transient failure in the webhook pipeline.
```json
{
"_tag": "WebhookEnqueueError",
"reason": "Failed to enqueue webhook delivery"
}
```
### ExternalServiceError (502) [#externalserviceerror-502]
Returned when an upstream provider returns an error. `provider` names the source.
```json
{
"_tag": "ExternalServiceError",
"provider": "ses",
"message": "Upstream provider rejected the request"
}
```
### BillingProviderError (502) [#billingprovidererror-502]
Returned when the billing provider returns an error while resolving entitlements.
```json
{
"_tag": "BillingProviderError",
"provider": "stripe",
"message": "Billing provider request failed"
}
```
### InternalError (500) [#internalerror-500]
Returned for an unexpected server error. Retry with backoff; if it persists, contact support.
```json
{
"_tag": "InternalError",
"message": "Internal server error"
}
```
## Related [#related]
# How Samva works
URL: https://samva.app/docs/developers/how-samva-works
Samva is an email API for developers. This page is about the shape of that API, not how to call it, but why it looks the way it does and what is happening underneath. If you want to send your first email, the [Get started](/docs/developers) tutorial walks you through it; if you have a specific job to do, the [channel guides](/docs/channels) are task-focused. This page is for the moments in between, when you want a mental model that makes the rest of the documentation click into place.
## The shape of a send [#the-shape-of-a-send]
Every email you send through Samva follows the same path, whether you triggered it from the SDK or a raw HTTP request. Understanding that path is the fastest way to understand the product.
You begin with an **intent to send**: a recipient, a subject, and some content. The default path is the **API**, the TypeScript SDK or a `POST /v1/messages` HTTP call. Samva also has an **SMTP** front door for software that already speaks the mail protocol; the hosted relay routes accepted messages into the same delivery pipeline as API sends. SMTP exists so legacy clients can adopt Samva without rewriting their sending code; the API exists for everything you build deliberately.
Once Samva accepts the message, it takes responsibility for **delivery**. Email is sent on your behalf through Samva's sending infrastructure, which is built on AWS SES. Each organization is isolated for the purposes of sending reputation, so one customer's sending behavior does not affect another's; this is part of why a multi-tenant API can offer credible deliverability rather than a shared, undifferentiated pool.
After a message leaves Samva, the story is not over. The receiving mail servers report back: the message was delivered, it bounced, the recipient marked it as spam, or, if you have engagement tracking enabled, it was opened or a link was clicked. Samva ingests these signals as **events**. Events are the truth about what actually happened to your email after it left your code, and they are the reason an email API is more than a thin wrapper over a mail server.
Finally, those events reach **you** through **webhooks**. Rather than asking you to poll for status, Samva pushes delivery and inbound events to an endpoint you control. This closes the loop: your application sends an email, and your application learns its fate, without a human in between. The [Receive webhooks](/docs/platform/webhooks) guide covers wiring this up.
So the full arc is: **intent → API → Samva → delivery via SES → events → webhooks → your application.** Everything else in the product hangs off this spine.
## The core objects [#the-core-objects]
Samva models email with a small set of objects. They are worth learning as a group, because their value comes from how they relate, not from any one of them in isolation.
A **message** is a single email, the unit you send. It carries the recipient, the subject, the HTML and text bodies, and any attachments. A message is also where Samva attaches the email-specific facts it learns over time: the delivery status, the events it accumulated, the threading headers that tie it to a conversation. When you send, you create a message; when you query delivery status, you read one back.
A **contact** is a person you send to. Modeling recipients as first-class contacts rather than bare email strings means Samva can carry a stable identity across many messages: you can address a send by `contactId` instead of repeating an address, and the recipient's history is one coherent record rather than a scattering of independent emails. This matters more as your sending grows beyond one-off transactional mail.
A **conversation** is a thread, a sequence of related messages between you and a recipient. Email is fundamentally conversational: replies carry `Message-ID` and `References` headers that connect them to what came before. Samva uses those RFC 2822 headers to group messages into conversations automatically, so a reply to an email you sent lands in the same thread rather than appearing as an orphaned new message. If your product has any two-way email, support, notifications that invite replies, anything where the recipient writes back, conversations are how that coherence is preserved.
A **webhook** is your subscription to events. It is the object that represents *where* Samva should deliver the delivery and inbound signals described above. One webhook can receive many event types; configuring it is how your application stays in sync with reality.
These objects compose. A contact receives messages; messages thread into conversations; events about those messages flow out through webhooks. Learning them as a connected model, rather than as four unrelated endpoints, is the point of this section.
## One API, two send shapes [#one-api-two-send-shapes]
Samva exposes a single sending endpoint, `POST /v1/messages`, reached through two shapes in the SDK: the email-shaped `samva.email.send(...)` facade for the common case, and the unified `samva.messages.send(...)` for everything that needs recipients, contacts, and conversations as first-class things. Both compile to the same request, so choosing between them is a question of ergonomics, not capability. The [email and the unified API](/docs/developers/email-and-the-unified-api) concept covers both shapes and when each one fits.
## The agent-native, single-API philosophy [#the-agent-native-single-api-philosophy]
Two design commitments run through everything above, and they reinforce each other.
The first is **one API surface, not a sprawl of feature endpoints**. Sending, threading, contacts, and events are not bolted-on subsystems with their own conventions; they are facets of one coherent model reached through one client. A reader who has learned how `samva.email.send` works has most of what they need to reason about `samva.contacts`, `samva.conversations`, and `samva.webhooks`, because all of them share the same call conventions, the same authentication, and the same tenancy rules. Coherence lowers the cost of doing the next thing.
The second is being **agent-native**: built so that software, including autonomous agents, can drive email end to end without a human clicking through a dashboard. The full send-to-events loop is reachable over the API, status comes back as structured events over webhooks rather than as something a person has to go and read, and the simple send path is short enough to be obvious to generate correctly. An agent can send an email, observe what happened to it, and react, all through the same typed surface a developer uses by hand. The single-API shape is what makes this philosophy possible: the fewer special cases there are, the more reliably software can operate the system on its own.
Every send in Samva is scoped to an organization, and that tenancy boundary is what keeps one customer's contacts, conversations, and sending reputation cleanly separated from another's. The [organizations and tenancy](/docs/developers/authentication-and-tenancy) concept explains how that boundary is enforced, and [deliverability](/docs/channels/email/deliverability) explains how Samva works to land your mail in the inbox.
## Where to go next [#where-to-go-next]
# Send your first email
URL: https://samva.app/docs/developers
Welcome to Samva. In this tutorial you'll send a real email from start to finish:
get an API key, install the SDK, send one email, and confirm it arrived. You need
a Samva account and Node.js 24 or newer, nothing else.
By the end you'll have sent your first email and seen its delivery status. Let's go.
## Before you start [#before-you-start]
You need two things:
* A Samva account. Sign up at [samva.app/dashboard](https://samva.app/dashboard).
* Node.js 24 or newer installed. Node runs TypeScript files natively.
Follow each step in order.
## Step 1: Get your API key [#step-1-get-your-api-key]
1. Log in to your [Samva dashboard](https://samva.app/dashboard).
2. Open **Developers → API Keys**.
3. Click **Create API Key**.
4. Copy the key somewhere safe; you'll use it in a moment.
Now make the key available to your code as an environment variable. In your terminal,
run this once (it lasts for your current terminal session):
```bash
export SAMVA_API_KEY="samva_sk_live_your_api_key"
```
Keep your API key secret. Read it from an environment variable, and never commit it to
version control.
## Step 2: Install the SDK [#step-2-install-the-sdk]
In your project directory, install the `samva` package:
npm
bun
yarn
```bash
npm install samva
```
```bash
bun add samva
```
```bash
yarn add samva
```
## Step 3: Create the client [#step-3-create-the-client]
Create a file called `send.ts` and initialize the Samva client with your API key:
```typescript
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
```
The client reads `SAMVA_API_KEY` from the environment variable you set in Step 1, so the
key never appears in your code.
## Step 4: Send your first email [#step-4-send-your-first-email]
Add the send call to `send.ts`. Pass the recipient address as a string, a subject, and an
HTML body. Replace `ada@example.com` with an address you can check:
```typescript
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const result = await samva.email.send({
to: "ada@example.com",
subject: "Welcome to Samva",
html: "
Welcome!
Thanks for joining.
",
});
console.log("Email sent:", result.data?.id);
```
Now run it. Node runs TypeScript files directly, so there's no build step or extra tooling:
```bash
node send.ts
```
Prefer Bun? `bun run send.ts` runs the file the same way.
You should see output like `Email sent: 7c9e6679-7425-40de-944b-e07fc1f90ae7`. That `id` is
the message you just created; hold onto it for the next step.
## Step 5: Check that it arrived [#step-5-check-that-it-arrived]
Use the message `id` from Step 4 to look up its delivery status:
```typescript
const message = await samva.messages.get({
id: result.data?.id!,
});
console.log("Status:", message.data?.status);
```
The status moves through `pending`, `processing`, `sent`, and finally `delivered` once the
email reaches the recipient. Check the inbox you sent to. Your first Samva email should be
waiting there.
Brand-new accounts can send right away to get going. To send from your own domain (and
reach more inboxes), verify a domain. See
[Verify your domain](/docs/channels/email/verify-your-domain).
## What you've done [#what-youve-done]
You just:
* Created a Samva API key and stored it as an environment variable.
* Installed the SDK and initialized the client.
* Sent a real email with `samva.email.send`.
* Looked up its delivery status.
## What's next [#whats-next]
Need a hand? Email us at [support@samva.app](mailto:support@samva.app).
# Connect an agent over MCP
URL: https://samva.app/docs/developers/mcp
Samva runs a hosted [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server so
AI agents can take organization-scoped email actions without you wiring up the REST API.
## Endpoint [#endpoint]
| | |
| ------------- | ---------------------------------------------- |
| **URL** | `https://mcp.samva.app/mcp` |
| **Transport** | Streamable HTTP (no SSE) |
| **Scope** | One organization, derived from your credential |
## Authentication [#authentication]
The server accepts either credential type, both organization-scoped:
| Mode | Header |
| ------- | ----------------------------------------------------------------------- |
| API key | `X-API-Key: samva_sk_live_…` or `Authorization: Bearer samva_sk_live_…` |
| OAuth | `Authorization: Bearer ` |
For OAuth, the server advertises discovery per RFC 9728: an unauthenticated request returns
`401` with a `WWW-Authenticate: Bearer resource_metadata="…"` header pointing at
`/.well-known/oauth-protected-resource`. OAuth-capable clients (such as Cursor) follow that
automatically; you only give them the endpoint URL. See
[API keys and OAuth sessions](/docs/developers/oauth-device-login) for the difference.
## Configure your client [#configure-your-client]
Client configuration schemas vary, but the generic Streamable HTTP shape with an API key is:
API key
OAuth
```json
{
"mcpServers": {
"samva": {
"type": "http",
"url": "https://mcp.samva.app/mcp",
"headers": { "X-API-Key": "samva_sk_live_your_api_key" }
}
}
}
```
Drop the headers and point the client at the same URL; it runs the OAuth flow using the
discovery document above.
```json
{
"mcpServers": {
"samva": {
"type": "http",
"url": "https://mcp.samva.app/mcp"
}
}
}
```
## Available tool families [#available-tool-families]
The server publishes these organization-scoped tool families. Use MCP tool discovery for each
tool's current input schema.
{/* email-launch-mcp-tools:start */}
| Family | Tools |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Contacts | `samva_contacts_find_or_create` |
| Messages | `samva_messages_send_email`, `samva_messages_get`, `samva_messages_list_email`, `samva_messages_get_email_status`, `samva_messages_list_email_events`, `samva_messages_list_inbound_email` |
| Conversations | `samva_conversations_get` |
| Domains | `samva_email_domains_add`, `samva_email_domains_list`, `samva_email_domains_get`, `samva_email_domains_verify`, `samva_email_domains_check_verification`, `samva_email_domains_get_status`, `samva_email_domains_remove`, `samva_email_domains_enable_receiving` |
| Senders | `samva_email_senders_add`, `samva_email_senders_list`, `samva_email_senders_get`, `samva_email_senders_check_verification`, `samva_email_senders_remove` |
| Webhooks | `samva_webhooks_create`, `samva_webhooks_list`, `samva_webhooks_get`, `samva_webhooks_update`, `samva_webhooks_delete`, `samva_webhooks_test`, `samva_webhooks_list_logs`, `samva_webhooks_get_stats`, `samva_webhooks_retry_delivery`, `samva_webhooks_rotate_secret` |
| Usage and proof | `samva_usage_get`, `samva_email_get_stats`, `samva_email_check_readiness`, `samva_email_get_launch_proof` |
| Template editor | `samva_templates_open_editor_session`, `samva_templates_connect_editor_session`, `samva_templates_read_source`, `samva_templates_read_model`, `samva_templates_apply_ops`, `samva_templates_write_source`, `samva_templates_render_document`, `samva_templates_check_document`, `samva_templates_send_test_email`, `samva_templates_set_font`, `samva_templates_save_editor_session` |
| Scheduled email | `samva_scheduled_messages_schedule_email`, `samva_scheduled_messages_list`, `samva_scheduled_messages_get`, `samva_scheduled_messages_cancel` |
| Campaigns | `samva_campaigns_create`, `samva_campaigns_update`, `samva_campaigns_list`, `samva_campaigns_get`, `samva_campaigns_archive`, `samva_campaigns_schedule_run`, `samva_campaigns_list_runs`, `samva_campaigns_get_run`, `samva_campaigns_control_run`, `samva_campaigns_list_recipients` |
{/* email-launch-mcp-tools:end */}
The MCP surface includes usage totals and email proof reads. Billing status, entitlements, and
billing portal actions are available through the dashboard, not MCP.
## Resources [#resources]
The server returns operating instructions during initialization and publishes these readable
resources:
| URI | Contents |
| -------------------------------------- | -------------------------------------- |
| `samva://guide/email` | Transactional email workflow |
| `samva://guide/template-editor` | Template editor session workflow |
| `samva://guide/scheduling` | Scheduled email and campaign workflows |
| `samva://reference/sml-agent-contract` | Compact machine-readable SML contract |
| `samva://reference/sml` | Full SML language reference |
## Retry a send safely [#retry-a-send-safely]
Pass a stable `idempotencyKey` to `samva_messages_send_email` when an automation might retry the
request. An identical retry returns the original email. Reusing the key with changed recipients,
content, template, or variables returns a conflict.
The tool advertises the static MCP annotation `idempotentHint: false` because the key is optional.
The annotation does not change for a keyed call. Treat a send as retry-safe only when you supplied
the key.
## Edit a template safely [#edit-a-template-safely]
Open a session with `samva_templates_open_editor_session`, or connect to a supplied session id.
Then:
1. Read `samva_templates_read_model` or `samva_templates_read_source`.
2. Call `samva_templates_apply_ops` with `expectedRevision` from that read.
3. Re-read after every mutation before targeting more node ids.
4. Run `samva_templates_check_document` and `samva_templates_render_document`.
5. Call `samva_templates_send_test_email` only when you want a real test delivery.
6. Call `samva_templates_save_editor_session` when you want to save the draft.
Hosted MCP does not create, find, publish, or unpublish templates. Use the CLI, SDK, REST API, or
dashboard for those lifecycle operations.
See [Schedule an email](/docs/channels/email/schedule-an-email#use-mcp) and
[Send a campaign](/docs/channels/email/send-a-campaign#use-mcp) for task-oriented tool sequences.
## MCP versus Agent Skills [#mcp-versus-agent-skills]
The MCP server is *live tool execution*: the agent does things against Samva. A
[Samva Agent Skill](/docs/developers/agent-skills) is *instructions*: it teaches an agent how to
use Samva across the SDK, CLI, and MCP, and which surface to reach for. They complement each
other.
## Next steps [#next-steps]
# API keys and OAuth sessions
URL: https://samva.app/docs/developers/oauth-device-login
Samva has two credential types. They differ in **who is acting** and **how the organization is
chosen**. This page explains the model; for the exact key formats and error shapes, see the
[API keys reference](/docs/developers/authentication).
Pick by actor: an **API key** for servers and automation, an **OAuth session** for a person at
a terminal.
## Two credentials [#two-credentials]
| | API key | OAuth session |
| ---------------- | --------------------- | ---------------------------------------------- |
| **For** | Servers, backends, CI | A person (or agent) at a terminal |
| **Identity** | The key | Your user account |
| **Organization** | Bound to one org | Any org you belong to; you pick the active one |
| **Lifetime** | Until revoked | Until the session expires |
An API key carries its organization with it; key-based callers never choose an org. An OAuth
session is tied to your user, so it can act on any organization you belong to, and you select the
active one.
## How device login works [#how-device-login-works]
`samva login` runs the OAuth 2.0 **device authorization flow**:
1. The CLI requests a device code and shows you a verification URL and a short user code.
2. It opens your browser (or prints the URL with `--no-browser`).
3. You approve the session in the browser, signed in as your Samva user.
4. The CLI receives a session token and stores it locally (mode `0600`).
From then on, requests send `Authorization: Bearer ` instead of an `X-API-Key` header.
## Organization scoping [#organization-scoping]
Because a session is user-scoped, the active organization travels separately as the
`x-org-slug` header:
* In the CLI, set it with `samva org use `.
* In the SDK, pass it when you construct an `authToken` client:
```typescript
import { createClient } from "samva";
const samva = createClient({
authToken: process.env.SAMVA_AUTH_TOKEN!,
headers: { "x-org-slug": "acme" },
});
```
## Session lifetime [#session-lifetime]
Device sessions have **no refresh token**. A session slides while you use it and expires after a
period of inactivity; when it does, run `samva login` again. There is nothing to rotate or store
on a server; that is what API keys are for.
## Which to use [#which-to-use]
* **API key**: anything unattended, a backend sending email, a cron job, CI. One organization,
long-lived, stored as a secret.
* **OAuth session**: interactive work, especially across multiple organizations, where you
don't want a long-lived key on your laptop.
## Related [#related]
# REST API
URL: https://samva.app/docs/developers/rest-api
Conventions reference for Samva's REST API. Every email send and management call resolves to a request against this API. This page documents the shared conventions: base URL, headers, body and response shapes, status codes, rate limiting, pagination, idempotency, and error format.
For a runnable send walkthrough (curl, Python, Go, PHP), see [Send an email](/docs/channels/email/send-an-email). For per-endpoint request and response detail, see the [API reference](/docs/api-reference).
## Base URL [#base-url]
```
https://api.samva.app/v1
```
All endpoints are relative to this base URL.
## Authentication [#authentication]
Every request authenticates with an API key passed in the `X-API-Key` header.
```
X-API-Key: samva_sk_live_your_api_key
```
See [API keys](/docs/developers/authentication) for key format and management.
## Headers [#headers]
| Header | Required | Description |
| -------------- | ------------------------ | -------------------------------------------------- |
| `X-API-Key` | Yes | Your API key. |
| `Content-Type` | For requests with a body | `application/json` for `POST`, `PUT`, and `PATCH`. |
## Content type [#content-type]
Requests carrying a body must send `Content-Type: application/json`. All responses are returned as JSON.
## Endpoints [#endpoints]
All operations are REST endpoints under the base URL. The send path is the most common:
| Method | Path | Purpose |
| ------ | ------------------- | -------------------------- |
| `POST` | `/v1/messages` | Send a message. |
| `GET` | `/v1/messages` | List messages. |
| `GET` | `/v1/messages/{id}` | Retrieve a single message. |
There is no `email.send` HTTP endpoint. The SDK's `samva.email.send` is an ergonomic wrapper
that posts to `POST /v1/messages`. All REST calls use the `POST /v1/messages` body shape below.
Full per-resource endpoint listings (conversations, contacts, webhooks, API keys, organizations) live in the [API reference](/docs/api-reference).
## Request body [#request-body]
`POST /v1/messages` accepts a JSON body with a `channel` discriminator and channel content nested under a matching key. For email, content is nested under `email`.
```json
{
"to": [{ "email": "ada@example.com" }],
"cc": [{ "email": "grace@example.com" }],
"channel": "email",
"email": {
"subject": "Welcome to Samva",
"html": "
Welcome!
",
"text": "Welcome!",
"replyTo": ["support@example.com"]
}
}
```
### Recipient fields [#recipient-fields]
`to` is required. `cc` and `bcc` are optional. Each field is an array, and each entry is one of:
| Form | Example |
| -------------------------- | --------------------------------------------------------- |
| Email address object | `{ "email": "ada@example.com" }` |
| Existing contact reference | `{ "contactId": "c0a8011e-0000-4000-8000-000000000001" }` |
Both forms resolve to the same `POST /v1/messages` endpoint.
### email object [#email-object]
| Field | Type | Required | Description |
| ------------ | --------- | -------- | --------------------------------------------------------------- |
| `subject` | string | Cond. | Email subject line. Required unless `templateId` is provided. |
| `html` | string | Cond. | HTML body. Required unless `templateId` is provided. |
| `text` | string | No | Plain-text body. |
| `replyTo` | string\[] | No | Reply-To address(es). |
| `templateId` | string | No | Send from a stored template instead of inline `subject`/`html`. |
## Response format [#response-format]
### Success [#success]
A successful `POST /v1/messages` returns `201` with the created message object. A newly
accepted send starts in `pending` status. The full object carries many more fields (recipients,
deliveries, metadata); the most relevant are shown here:
```json
{
"id": "3f2a9c1e-7b4d-4e8a-9c2f-1a2b3c4d5e6f",
"status": "pending",
"channel": "email",
"createdAt": "2024-01-15T10:30:00.000Z"
}
```
See the [API reference](/docs/api-reference) for the complete response schema.
### Error [#error]
Errors are returned as a flat JSON object. A `_tag` field carries the machine-readable
discriminator, and the remaining fields are specific to that error type. There is no wrapper
object; the error fields sit at the top level of the response body.
A validation failure (`422`):
```json
{
"_tag": "ValidationError",
"message": "Invalid request body",
"fields": {
"to": ["Must be a valid email address"]
}
}
```
The `_tag` value maps to the HTTP status code:
| `_tag` | Status | Fields |
| ----------------------- | ------ | ----------------------------------- |
| `ValidationError` | `422` | `message`, `fields` (optional) |
| `ResourceNotFoundError` | `404` | `resource`, `id` |
| `UnauthorizedError` | `401` | `message` |
| `PaymentRequiredError` | `402` | `resource`, `currentUsage`, `limit` |
| `ForbiddenError` | `403` | `message` |
| `ConflictError` | `409` | `message`, `resource` |
| `RateLimitedError` | `429` | `retryAfterSeconds` |
| `ExternalServiceError` | `502` | `provider`, `message` |
| `InternalError` | `500` | `message` |
For example, a missing resource (`404`):
```json
{
"_tag": "ResourceNotFoundError",
"resource": "Message",
"id": "3f2a9c1e-7b4d-4e8a-9c2f-1a2b3c4d5e6f"
}
```
A rate-limit response (`429`):
```json
{
"_tag": "RateLimitedError",
"retryAfterSeconds": 30
}
```
For the full list of error types, their fields, and how to resolve each one, see the
[Error reference](/docs/developers/error-reference). The machine-readable source of truth is the
[OpenAPI specification](https://api.samva.app/v1/openapi.json).
## HTTP status codes [#http-status-codes]
| Code | Meaning | Notes |
| ----- | --------------------- | --------------------------------------------------- |
| `200` | OK | Request completed. |
| `201` | Created | Resource created (a successful send returns `201`). |
| `400` | Bad Request | Malformed request. |
| `401` | Unauthorized | Missing or invalid API key. |
| `402` | Payment Required | Usage limit or plan restriction. |
| `403` | Forbidden | Key lacks permission for the resource. |
| `404` | Not Found | Resource does not exist. |
| `409` | Conflict | Duplicate request. |
| `422` | Unprocessable Entity | Validation failed. |
| `429` | Too Many Requests | Rate limit exceeded. |
| `500` | Internal Server Error | Unexpected server error. |
| `502` | Bad Gateway | Upstream provider error. |
| `503` | Service Unavailable | Temporary outage; retry later. |
## Rate limiting [#rate-limiting]
Requests are rate-limited per organization. When a limit is exceeded, the API returns `429 Too Many Requests` with a `RateLimitedError` body. Its `retryAfterSeconds` field tells you how long to wait before retrying.
```json
{
"_tag": "RateLimitedError",
"retryAfterSeconds": 30
}
```
Back off for `retryAfterSeconds` and retry.
## Pagination [#pagination]
List endpoints accept `page` and `limit` query parameters.
| Parameter | Default | Description |
| --------- | ------- | ----------------------------- |
| `page` | `1` | Page number (1-based). |
| `limit` | `20` | Items per page (maximum 100). |
```
GET /v1/messages?page=2&limit=50
```
Paginated responses include a `pagination` object.
```json
{
"data": [],
"pagination": {
"page": 2,
"limit": 50,
"total": 245,
"totalPages": 5
}
}
```
| Field | Type | Description |
| ----------------------- | ------ | ---------------------- |
| `pagination.page` | number | Current page. |
| `pagination.limit` | number | Items per page. |
| `pagination.total` | number | Total matching items. |
| `pagination.totalPages` | number | Total number of pages. |
## Idempotency [#idempotency]
`POST /v1/messages` accepts an optional `idempotencyKey` in the JSON body. Use a stable key for one logical send whenever your application may retry. The key is scoped to your organization and must contain 1 to 255 characters.
Repeating an identical request with the same key returns the original message without creating another message, provider send, or billing effect. Reusing the key with different request data returns `409 Conflict`. If an attempt fails before the send is committed, Samva releases the claim so a corrected retry can proceed with the same key.
## OpenAPI specification [#openapi-specification]
The live machine-readable specification is published at
[api.samva.app/v1/openapi.json](https://api.samva.app/v1/openapi.json). See the
[OpenAPI reference](/docs/api-reference/openapi) for details.
## Related [#related]
# TypeScript SDK
URL: https://samva.app/docs/developers/typescript-sdk
The `samva` package is the official TypeScript/JavaScript SDK for the Samva email API. It is generated from the [OpenAPI specification](https://api.samva.app/v1/openapi.json), so its surface mirrors the REST API exactly.
This page is a reference. For a step-by-step walkthrough of sending your first email, see [Send an email](/docs/channels/email/send-an-email).
## Installation [#installation]
npm
yarn
pnpm
bun
```bash
npm install samva
```
```bash
yarn add samva
```
```bash
pnpm add samva
```
```bash
bun add samva
```
## Entrypoints [#entrypoints]
The `samva` package ships three entrypoints from a single install. They are all generated from the same OpenAPI surface, so they stay in sync with the REST API; pick the one that matches your runtime style.
| Import | Style | When to use |
| ---------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `samva` | Promise (default) | Most apps. Re-exports the Promise client. |
| `samva/promises` | Promise (explicit) | The same Promise client as the root; use the explicit path when you also import `samva/effect`. |
| `samva/effect` | Native Effect v4 | Effect-native apps. Returns `Effect`s rather than wrapping the Promise client. Requires `effect` as a peer dependency. |
The rest of this page documents the **Promise client**. For the Effect client, see [Native Effect client](#native-effect-client) below.
## Client [#client]
### createClient(config) [#createclientconfig]
Creates a configured client instance. The organization context is derived from the API key; no organization slug or ID is required.
```typescript
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
```
#### Config options [#config-options]
| Option | Type | Required | Default | Description |
| ----------- | -------- | ----------------------------- | ----------------------- | ------------------------------------------------------------------------------ |
| `apiKey` | `string` | One of `apiKey` / `authToken` | none | API key. Sent as the `X-API-Key` header. |
| `authToken` | `string` | One of `apiKey` / `authToken` | none | OAuth bearer token (e.g. from `samva login`). Sent as `Authorization: Bearer`. |
| `baseUrl` | `string` | No | `https://api.samva.app` | API base URL. Override for testing or self-hosting. |
Pass exactly one of `apiKey` or `authToken`. Use `apiKey` for servers (the organization is derived from the key); use `authToken` for first-party or OAuth tooling, where the session is user-scoped, and pass the active organization with `headers: { "x-org-slug": "" }`. See [API keys and OAuth sessions](/docs/developers/oauth-device-login).
Additional fields from the underlying generated HTTP client `Config` (such as `headers` and `fetch`) are also accepted.
### Return shape [#return-shape]
`createClient` returns a `SamvaClient` exposing the live services below plus `client`, the underlying HTTP client.
| Property | Description |
| ------------------- | ----------------------------------------------------------- |
| `email` | Email send helper plus domain, sender, and template methods |
| `messages` | Unified message send and retrieval |
| `contacts` | Contact records |
| `conversations` | Threaded message grouping |
| `webhooks` | Event delivery configuration |
| `apiKeys` | Programmatic access management |
| `organizations` | Organization settings |
| `scheduledMessages` | Schedule, list, get, and cancel future sends |
| `campaigns` | Audience campaigns with scheduled runs and recipients |
| `client` | Underlying HTTP client |
## Calling convention [#calling-convention]
Methods take flat request parameters as the first argument. Request options such as `throwOnError`, `headers`, and `signal` are passed as an optional second argument.
Every method resolves to a result object:
| Field | Description |
| ------- | -------------------------------------------------------- |
| `data` | The response payload on success (for example, `data.id`) |
| `error` | The error payload on failure; `undefined` on success |
```typescript
const result = await samva.email.send({
to: "ada@example.com",
cc: "grace@example.com",
replyTo: "support@example.com",
subject: "Welcome to Samva",
html: "
Welcome!
",
});
if (result.error) {
console.error("Send failed:", result.error);
} else {
console.log("Sent:", result.data?.id);
}
```
## samva.email [#samvaemail]
The canonical surface for email.
### email.send(input, options?) [#emailsendinput-options]
The simplest way to send email. It is an SDK-only ergonomic wrapper over `messages.send()` - there is no `email.send` REST endpoint. Email fields are flattened directly into the input object (not nested under an `email` key).
```typescript
const result = await samva.email.send({
to: "ada@example.com",
subject: "Welcome to Samva",
html: "
Welcome!
",
});
```
#### Input fields [#input-fields]
| Field | Type | Required | Description |
| -------------------- | ---------------------------- | -------- | ------------------------------------------------------ |
| `to` | recipient or recipient array | Yes | Recipient(s). See [Recipients](#recipients). |
| `cc` | recipient or recipient array | - | Carbon-copy recipient(s). |
| `bcc` | recipient or recipient array | - | Blind-carbon-copy recipient(s). |
| `replyTo` | `string` or `string[]` | - | Reply-To address(es) for the email. |
| `subject` | `string` | - | Email subject line. |
| `html` | `string` | - | HTML body. Provide `html`, `text`, or both. |
| `text` | `string` | - | Plain-text body. |
| `attachments` | array | - | File attachments. |
| `templateId` | `string` | - | Template to render instead of inline `html`/`text`. |
| `templateData` | `object` | - | Variables passed to the template. |
| `conversationId` | `string` | - | Attach the message to an existing conversation. |
| `inReplyToMessageId` | `string` | - | Thread the message as a reply to a prior message. |
| `unsubscribeGroupId` | `string` | - | Unsubscribe group to associate with the message. |
| `metadata` | `object` | - | Arbitrary key/value metadata to attach to the message. |
#### Recipients [#recipients]
`to`, `cc`, and `bcc` accept a single value or an array of any of:
| Form | Example |
| ----------------- | --------------------------------------------------------------------- |
| Email string | `"ada@example.com"` |
| Email object | `{ email: "ada@example.com" }` |
| Contact reference | `{ contactId: "contact_123" }` |
| Contact object | `{ id: "contact_123" }` (an object with `id` is treated as a contact) |
### Email helper methods [#email-helper-methods]
The `email` service also exposes domain, sender, and template management. All take the standard options object and return `{ data, error }`.
| Method | Purpose |
| ---------------------------------- | ----------------------------------------------------- |
| `listTemplates(options?)` | List email templates. |
| `createTemplate(options)` | Create a template with a variable schema. |
| `getStats(options?)` | Email sending statistics. |
| `addDomain(options)` | Add a sending domain; returns the DNS records to set. |
| `listDomains(options?)` | List sending domains. |
| `verifyDomain(options)` | Trigger verification for a domain. |
| `checkDomainVerification(options)` | Check verification status. |
| `getDomainStatus(options)` | Current domain status. |
| `getDomainById(options)` | Fetch a domain by ID. |
| `removeDomain(options)` | Remove a domain. |
| `addSender(options)` | Add a verified sender address. |
| `listSenders(options?)` | List sender addresses. |
| `checkSenderVerification(options)` | Check sender verification status. |
| `removeSender(options)` | Remove a sender. |
For domain verification steps, see [Verify your domain](/docs/channels/email/verify-your-domain).
## samva.messages [#samvamessages]
The unified send and retrieval API. `email.send()` delegates to `messages.send()`; reach for `messages.send()` directly when you want the explicit nested shape, for example to address contacts by `contactId`, send to multiple recipients, or thread a reply.
### messages.send(input, options?) [#messagessendinput-options]
```typescript
const result = await samva.messages.send({
to: [{ contactId: "contact_123" }],
cc: [{ email: "grace@example.com" }],
channel: "email",
email: {
subject: "Welcome to Samva",
html: "
Welcome!
",
replyTo: "support@example.com",
},
});
```
#### Input fields [#input-fields-1]
| Field | Type | Required | Description |
| ---------------- | --------------- | -------- | ----------------------------------------------------------- |
| `to` | recipient array | Yes | Recipients. Always an array. See [Recipients](#recipients). |
| `cc` | recipient array | - | Carbon-copy recipients. Always an array. |
| `bcc` | recipient array | - | Blind-carbon-copy recipients. Always an array. |
| `channel` | `"email"` | Yes | Send channel. Email-only at launch. |
| `email` | object | Yes | Nested email content (see below). |
| `conversationId` | `string` | - | Attach to an existing conversation. |
| `metadata` | `object` | - | Arbitrary key/value metadata. |
The nested `email` object carries `subject`, `html`, `text`, `replyTo`, `attachments`, `templateId`, `templateData`, `inReplyToMessageId`, and `unsubscribeGroupId` - the same content fields that `email.send()` flattens into `body`.
### Other message methods [#other-message-methods]
| Method | Purpose |
| -------------------- | ---------------------------------- |
| `list(options?)` | List messages. |
| `get(options)` | Get a message by ID. |
| `getStatus(options)` | Get delivery status for a message. |
| `getEvents(options)` | Get the event log for a message. |
| `remove(options)` | Delete a message. |
```typescript
const messages = await samva.messages.list({ channel: "email", limit: "20" });
const message = await samva.messages.get({ id: "msg_123" });
const events = await samva.messages.getEvents({ id: "msg_123" });
```
## Other services [#other-services]
All services follow the same calling convention and result shape.
### samva.contacts [#samvacontacts]
| Method | Purpose |
| ------------------------------------------------------------- | ----------------------------------------- |
| `create`, `update`, `remove` | Manage a contact record. |
| `get`, `find`, `list` | Fetch and list contacts. |
| `findOrCreate` | Look up a contact or create it if absent. |
| `bulkImport`, `bulkDelete` | Bulk create or delete. |
| `bulkUpdateStatus`, `bulkUpdateTags` | Bulk attribute updates. |
| `export` | Export contacts. |
| `getActivityTimeline`, `getEngagement`, `getGroupMemberships` | Contact activity and grouping. |
### samva.conversations [#samvaconversations]
| Method | Purpose |
| ------------------------------- | --------------------------------- |
| `create`, `update` | Manage a conversation. |
| `getById`, `list` | Fetch and list conversations. |
| `addContacts`, `removeContacts` | Manage conversation participants. |
### samva.webhooks [#samvawebhooks]
| Method | Purpose |
| ---------------------------- | -------------------------------------- |
| `create`, `update`, `remove` | Manage a webhook endpoint. |
| `get`, `list` | Fetch and list endpoints. |
| `getStats`, `getLogs` | Delivery statistics and logs. |
| `test`, `retryDelivery` | Send a test event or retry a delivery. |
| `regenerateSecret` | Rotate the signing secret. |
For receiving and verifying webhook events, see [Receive webhooks](/docs/platform/webhooks).
### samva.apiKeys [#samvaapikeys]
| Method | Purpose |
| ---------------------------- | ------------------------------ |
| `create`, `update`, `remove` | Manage an API key. |
| `get`, `list` | Fetch and list keys. |
| `revoke`, `rotate` | Revoke or rotate a key. |
| `getStats`, `getActivity` | Usage statistics and activity. |
### samva.organizations [#samvaorganizations]
| Method | Purpose |
| --------------- | -------------------------------------- |
| `get`, `update` | Read and update organization settings. |
| `getUsage` | Usage figures for the organization. |
### samva.scheduledMessages [#samvascheduledmessages]
Scheduling is its own resource; there is no `scheduledAt` field on `messages.send`.
| Method | Purpose |
| ------------- | ---------------------------------------------------- |
| `create` | Schedule a message for a future `scheduledFor` time. |
| `list`, `get` | List scheduled messages or fetch one by id. |
| `cancel` | Cancel a pending scheduled message. |
### samva.campaigns [#samvacampaigns]
| Method | Purpose |
| ------------------------------------ | --------------------------------------------- |
| `create`, `list`, `get`, `update` | Manage campaign drafts. |
| `archive` | Archive a campaign. |
| `scheduleRun`, `listRuns`, `getRun` | Schedule and inspect campaign runs. |
| `pauseRun`, `resumeRun`, `cancelRun` | Control an in-flight run. |
| `listRecipients` | List a run's recipients with delivery status. |
## Error handling [#error-handling]
Methods do not throw on API errors by default. Inspect the result instead:
```typescript
const result = await samva.email.send({
to: "ada@example.com",
subject: "Test",
text: "Hello",
});
if (result.error) {
// result.error holds the error payload
console.error(result.error);
} else {
console.log(result.data?.id);
}
```
## Environment variables [#environment-variables]
Store the API key in an environment variable rather than hard-coding it.
```bash
# .env
SAMVA_API_KEY=samva_sk_live_your_api_key
```
```typescript
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
```
## Key format [#key-format]
Production keys start with `samva_sk_live_`; keys minted outside production start with
`samva_sk_test_`. Pass either environment's key into the client as `apiKey`.
```typescript
const samva = createClient({ apiKey: "samva_sk_live_..." });
```
See [API keys](/docs/developers/authentication) for key format and details.
## Native Effect client [#native-effect-client]
`samva/effect` is a first-class [Effect](https://effect.website) v4 client. It returns `Effect`s rather than wrapping the Promise client, so it composes directly into Effect programs. Install `effect` alongside `samva`:
```bash
npm install samva effect
```
Create a client inside an `Effect` and provide an HTTP layer:
```typescript
import { Effect } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import { createClient } from "samva/effect";
const program = Effect.gen(function* () {
const samva = yield* createClient({ apiKey: process.env.SAMVA_API_KEY! });
return yield* samva.email.send({
to: "ada@example.com",
subject: "Welcome to Samva",
html: "
Welcome!
",
});
}).pipe(Effect.provide(FetchHttpClient.layer));
```
For application-wide dependency injection, use the `SamvaClient` service and provide its layer:
```typescript
import { Effect } from "effect";
import { SamvaClient } from "samva/effect";
const program = Effect.gen(function* () {
const samva = yield* SamvaClient;
return yield* samva.email.send({
to: "ada@example.com",
subject: "Welcome to Samva",
html: "
Welcome!
",
});
}).pipe(Effect.provide(SamvaClient.layerFetch({ apiKey: process.env.SAMVA_API_KEY! })));
```
The Effect client exposes flat `email` and `messages` send helpers, the same email-first surface as the Promise client, plus a top-level `raw` property (e.g. `samva.raw`) for the full generated Effect operation surface. Errors surface through the Effect error channel rather than a `{ data, error }` result, so handle them with the usual combinators (`Effect.catchAll`, `Effect.match`, and so on).
## Support matrix [#support-matrix]
| SDK | Package | API version | Runtime |
| ---------- | ------- | ----------- | ----------------------------------- |
| TypeScript | `samva` | v1 | Modern Node.js, Bun, and Deno (ESM) |
| REST API | - | v1 | Any HTTP client |
The SDK is generated from the Samva [OpenAPI specification](https://api.samva.app/v1/openapi.json) using `@hey-api/openapi-ts`, so it stays in sync with the REST API. For other languages, use the [REST API](/docs/developers/rest-api) with your language's HTTP client.
## See also [#see-also]
# Samva Documentation
URL: https://samva.app/docs
Samva is a modern email API and dashboard for product teams. Send transactional, scheduled, and
campaign email through a type-safe TypeScript SDK, a CLI, an MCP server, or REST. Manage contacts,
keep replies threaded into conversations, and receive delivery and inbound events over webhooks.
## Find your way [#find-your-way]
## How the SDK and REST API relate [#how-the-sdk-and-rest-api-relate]
There are two ways to send the same email. The TypeScript SDK exposes `samva.email.send(...)`, an ergonomic wrapper that flattens email fields like `to`, `subject`, and `html` directly into the request. Under the hood, every send, whether from the SDK or a raw HTTP request, goes to `POST /v1/messages`. The REST endpoint takes the unified message shape, where the channel is named explicitly and email content is nested under `email`. The [unified API concept](/docs/developers/email-and-the-unified-api) explains why both shapes exist and when to choose each.
## Support [#support]
* **Email**: [support@samva.app](mailto:support@samva.app)
# Astro
URL: https://samva.app/docs/integrations/astro
Send email from Astro with the `samva` SDK in server code. Use an Astro Action
for progressive-enhancement forms, or use an API endpoint when a JSON client
needs to trigger the send.
Samva sends from the verified sender configured on your account, so Astro
examples should not pass a `from` field.
## Install [#install]
```sh
bun add samva
bunx astro add cloudflare
```
This guide uses the Cloudflare adapter to show the Worker path. If you deploy to
Node, Vercel, or Netlify, swap in that adapter; the Samva client and send calls
stay the same.
Configure a typed server secret with `astro:env`:
```js
import cloudflare from "@astrojs/cloudflare";
import { defineConfig, envField } from "astro/config";
export default defineConfig({
output: "server",
adapter: cloudflare(),
env: {
schema: {
SAMVA_API_KEY: envField.string({ context: "server", access: "secret" }),
},
},
});
```
Create a server-only SDK module and import it only from Actions, endpoints, or
server-rendered `.astro` frontmatter:
```ts
import { SAMVA_API_KEY } from "astro:env/server";
import { createClient } from "samva";
export const samva = createClient({ apiKey: SAMVA_API_KEY });
```
## Astro Action [#astro-action]
Actions are the best default for contact forms because Astro handles form posts,
input validation, and the post-submit result.
```ts
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro/zod";
import { samva } from "../lib/samva";
const escapeHtml = (value: string) =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
export const server = {
send: defineAction({
accept: "form",
input: z.object({
email: z.email(),
subject: z.string().min(1),
message: z.string().min(1),
}),
handler: async ({ email, message, subject }) => {
try {
await samva.messages.send({
to: [{ email }],
channel: "email",
email: {
subject,
html: `
${escapeHtml(message).replaceAll("\n", " ")}
`,
text: message,
},
});
} catch {
throw new ActionError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to send email.",
});
}
return { ok: true };
},
}),
};
```
Use the action from an on-demand Astro page:
```astro
---
import { actions } from "astro:actions";
const result = Astro.getActionResult(actions.send);
---
{result?.data?.ok &&
Email sent.
}
{result?.error &&
{result.error.message}
}
```
Actions are public endpoints. Add the same authorization, rate limiting, and
abuse checks you would add to an API route.
## API endpoint [#api-endpoint]
Use a server endpoint for headless clients or non-Astro frontends.
```ts
import type { APIRoute } from "astro";
import { z } from "astro/zod";
import { samva } from "../../lib/samva";
export const prerender = false;
const emailInput = z.email();
const escapeHtml = (value: string) =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
export const POST: APIRoute = async ({ request }) => {
let body: unknown;
try {
body = await request.json();
} catch {
return Response.json({ error: "Expected a JSON request body." }, { status: 400 });
}
if (
typeof body !== "object" ||
body === null ||
!("email" in body) ||
!("subject" in body) ||
!("message" in body) ||
typeof body.email !== "string" ||
typeof body.subject !== "string" ||
typeof body.message !== "string" ||
!emailInput.safeParse(body.email).success ||
body.email.length === 0 ||
body.subject.length === 0 ||
body.message.length === 0
) {
return Response.json(
{ error: "email, subject, and message are required string fields." },
{ status: 400 },
);
}
try {
await samva.messages.send({
to: [{ email: body.email }],
channel: "email",
email: {
subject: body.subject,
html: `
${escapeHtml(body.message).replaceAll("\n", " ")}
`,
text: body.message,
},
});
} catch {
return Response.json({ error: "Failed to send email." }, { status: 502 });
}
return Response.json({ ok: true });
};
```
Email sends must happen at runtime. Use `output: "server"` globally or
`export const prerender = false` on the send endpoint.
## Cloudflare Workers and React Email [#cloudflare-workers-and-react-email]
The SDK is `fetch`-based and works under `@astrojs/cloudflare`, so you can render
and send on the Worker. For React Email templates, import the bare render package
and let the bundler pick the edge build:
```tsx
import { render } from "@react-email/render";
const html = await render();
```
For deeper template patterns, see the [React Email integration](/docs/integrations/react-email).
## Cookbook and example [#cookbook-and-example]
The [Astro cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/astro.md)
goes deeper: choosing an Action versus an API endpoint, why prerendered routes can't
send email, picking `astro:env/server` over a public env prefix, and receiving Samva
webhooks in Astro. The [Astro email example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/astro-email)
is a runnable project with both send paths wired up.
* [Astro cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/astro.md)
* [Astro email example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/astro-email)
* [Astro Actions docs](https://docs.astro.build/en/guides/actions/)
* [Astro Cloudflare adapter docs](https://docs.astro.build/en/guides/integrations-guide/cloudflare/)
# Auth.js
URL: https://samva.app/docs/integrations/authjs
[Auth.js](https://authjs.dev) email providers send magic links through one
callback: `sendVerificationRequest`. Use a custom HTTP email provider and call
`samva.messages.send` from that callback.
Auth.js owns authentication and verification-token storage. Samva sends the
email over your verified sender, so the Samva payload has no `from` field.
## Install [#install]
```sh
bun add next-auth@beta samva
```
Keep `SAMVA_API_KEY` server-side only. Auth.js also needs `AUTH_SECRET`; generate
it with:
```sh
npx auth secret
```
## Add the provider [#add-the-provider]
```ts
import NextAuth from "next-auth";
import type { EmailConfig } from "next-auth/providers/email";
import { createClient } from "samva";
const apiKey = process.env.SAMVA_API_KEY;
if (!apiKey) {
throw new Error("SAMVA_API_KEY is not set.");
}
const samva = createClient({ apiKey });
function SamvaEmail(config: Partial = {}): EmailConfig {
return {
id: "samva",
type: "email",
name: "Email",
from: "",
maxAge: 24 * 60 * 60,
async sendVerificationRequest({ identifier: to, url }) {
const { host } = new URL(url);
await samva.messages.send({
to: [{ email: to }],
channel: "email",
email: {
subject: `Sign in to ${host}`,
html: `
`,
text: `Sign in to ${host}\n${url}\n`,
},
});
},
options: config,
};
}
export const { handlers, signIn, signOut, auth } = NextAuth({
// adapter: ,
providers: [SamvaEmail()],
});
```
The empty provider-level `from` exists only because Auth.js' email-provider type
still carries that field. It is not forwarded to Samva.
## Add the route handler [#add-the-route-handler]
```ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
```
Place that in `app/api/auth/[...nextauth]/route.ts`.
## Trigger sign-in [#trigger-sign-in]
```tsx
import { signIn } from "@/auth";
export function SignInForm() {
return (
);
}
```
## Template with React Email [#template-with-react-email]
Render a React Email component to `html`, derive `text`, then pass both strings
to Samva. Replace `sendVerificationRequest` inside the `SamvaEmail` provider
factory with:
```tsx
import { render, toPlainText } from "react-email";
import { MagicLinkEmail } from "./emails/magic-link";
async sendVerificationRequest({ identifier: to, url }) {
const { host } = new URL(url);
const html = await render();
const text = toPlainText(html);
await samva.messages.send({
to: [{ email: to }],
channel: "email",
email: { subject: `Sign in to ${host}`, html, text },
});
}
```
See the [React Email integration](/docs/integrations/react-email) for the full
rendering and preview workflow.
## Runtime notes [#runtime-notes]
The Samva send is edge-safe because the SDK is `fetch`-based. The database
adapter you choose for Auth.js verification-token storage is the runtime gate:
many adapters need Node, or Auth.js' split-config pattern with `auth.config.ts`
for edge proxy/middleware and full `auth.ts` with the adapter elsewhere.
NextAuth v4's `EmailProvider` uses the same `sendVerificationRequest` seam, but
the wiring differs; see the
[Auth.js v5 migration guide](https://authjs.dev/getting-started/migrating-to-v5).
For delivery and bounce events, see [Verify webhooks](/docs/integrations/webhooks).
## Cookbook [#cookbook]
For the Nodemailer-provider migration bridge if your app is already built around it, and an FAQ covering the missing `from` field, edge-runtime support, and NextAuth v4 wiring, see the [Auth.js cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/authjs.md).
# Better Auth
URL: https://samva.app/docs/integrations/better-auth
Use [`@samva/better-auth`](https://github.com/AryaLabsHQ/samva-integrations/tree/main/packages/better-auth)
to send Better Auth email callbacks through Samva. The package provides callback
fragments for manual wiring and a `withSamva()` helper for the common setup.
Better Auth owns authentication. Samva sends the rendered email over your
verified sender, so there is no `from` option in the integration.
## Install [#install]
```sh
bun add better-auth samva @samva/better-auth
```
## Wire the common callbacks [#wire-the-common-callbacks]
```ts
import { betterAuth } from "better-auth";
import { withSamva } from "@samva/better-auth";
export const auth = betterAuth(
withSamva(
{
emailAndPassword: {
enabled: true,
},
},
{
apiKey: process.env.SAMVA_API_KEY!,
plugins: {
emailOTP: true,
magicLink: true,
},
},
),
);
```
`withSamva()` fills missing email verification and password reset callbacks. It
also fills change-email and delete-account callbacks when those Better Auth user
flows are already enabled. Existing callbacks are preserved.
## Manual wiring [#manual-wiring]
Use `samvaEmail()` when you want to control each Better Auth option yourself:
```ts
import { betterAuth } from "better-auth";
import { emailOTP, magicLink } from "better-auth/plugins";
import { samvaEmail } from "@samva/better-auth";
const samva = samvaEmail({ apiKey: process.env.SAMVA_API_KEY! });
export const auth = betterAuth({
emailVerification: samva.emailVerification,
emailAndPassword: {
enabled: true,
sendResetPassword: samva.emailAndPassword.sendResetPassword,
},
plugins: [
emailOTP(samva.plugins.emailOTP),
magicLink(samva.plugins.magicLink),
],
});
```
## Covered triggers [#covered-triggers]
The package includes defaults for:
* email verification
* password reset
* change-email confirmation
* delete-account confirmation
* email OTP
* two-factor OTP
* magic links
* organization invitations
Override any template with a function returning HTML, `{ subject, html, text }`,
or a React Email element. React Email rendering is optional and uses
`@react-email/render` when your template returns an element.
## Cookbook and example [#cookbook-and-example]
For the callback-fragment wiring, the full list of default templates, and template overrides with `{ subject, html, text }` or a React Email element, see the [Better Auth cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/better-auth.md). The [Next.js example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/better-auth-nextjs) is a runnable project with both wiring styles.
# Clerk
URL: https://samva.app/docs/integrations/clerk
Use Clerk webhooks to send auth and lifecycle email through Samva. Clerk verifies
users and signs webhook events; your route verifies the Svix signature with
Clerk's helper, then sends through the `samva` SDK.
Clerk owns authentication. Samva sends from the verified sender configured on
your account, so there is no `from` field in the send payload.
## Install [#install]
```sh
bun add @clerk/nextjs@^7.5.10 samva server-only
```
Add React Email when you want component templates:
```sh
bun add @react-email/render @react-email/components react react-dom
```
Use a Clerk version that resolves `@clerk/backend >=2.4.0`. Clerk fixed an
improper webhook-signature acceptance issue in that line. `@clerk/nextjs@6.23.3`
and newer include the patched v2 backend dependency, and
`@clerk/nextjs@^7.5.10` already resolves to the patched v3 backend line.
## Create a server-only Samva client [#create-a-server-only-samva-client]
```ts title="lib/samva.ts"
import "server-only";
import { createClient } from "samva";
const apiKey = process.env.SAMVA_API_KEY;
if (!apiKey) {
throw new Error("SAMVA_API_KEY is not set.");
}
export const samva = createClient({ apiKey });
```
Configure both secrets only on the server:
```sh
SAMVA_API_KEY=samva_sk_live_...
CLERK_WEBHOOK_SIGNING_SECRET=whsec_...
```
## Keep the webhook route public [#keep-the-webhook-route-public]
Clerk webhook requests are signed, but they are not signed-in user requests. Make
the webhook path public in `clerkMiddleware()`.
```ts
// proxy.ts in Next.js 16+. Use middleware.ts in Next.js 15 and earlier.
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher(["/", "/api/webhooks/clerk"]);
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect();
}
});
```
## Send a welcome email on user.created [#send-a-welcome-email-on-usercreated]
```ts title="app/api/webhooks/clerk/route.ts"
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { render, toPlainText } from "@react-email/render";
import type { NextRequest } from "next/server";
import WelcomeEmail from "../../../../emails/welcome";
import { samva } from "../../../../lib/samva";
export const runtime = "edge";
export async function POST(request: NextRequest) {
let event: Awaited>;
try {
event = await verifyWebhook(request);
} catch {
return new Response("Verification failed", { status: 400 });
}
if (event.type === "user.created") {
const email =
event.data.email_addresses.find(
(address) => address.id === event.data.primary_email_address_id,
)?.email_address ?? event.data.email_addresses[0]?.email_address;
if (email) {
try {
const html = await render(
WelcomeEmail(event.data.first_name ? { firstName: event.data.first_name } : {}),
);
await samva.messages.send({
to: [{ email }],
channel: "email",
email: {
subject: "Welcome",
html,
text: toPlainText(html),
},
});
} catch (error) {
console.error("Failed to send Clerk welcome email", error);
return new Response("OK", { status: 200 });
}
}
}
return new Response("OK", { status: 200 });
}
```
`verifyWebhook()` reads the raw request body and checks the Svix headers. Do not
parse JSON before verification. Return `400` for missing or invalid signatures.
## Custom delivery with email.created [#custom-delivery-with-emailcreated]
For Clerk-rendered auth email, open a Clerk email template and turn off
**Delivered by Clerk**. Clerk emits `email.created`; forward the rendered email
through Samva:
```ts
if (event.type === "email.created") {
const to = event.data.to_email_address;
if (to) {
try {
await samva.messages.send({
to: [{ email: to }],
channel: "email",
email: {
subject: event.data.subject ?? "Clerk email",
html: event.data.body ?? undefined,
text: event.data.body_plain ?? undefined,
},
});
} catch (error) {
console.error("Failed to send Clerk auth email", error);
}
}
}
```
You can also render your own React Email template from `event.data.data`.
`otp_code` is documented for verification email; other keys vary by template
`slug`, so inspect a first live event before relying on them.
## Runtime notes [#runtime-notes]
`verifyWebhook()` accepts a standard web `Request`, and the Samva SDK is
`fetch`-based. The same pattern works in Next.js Route Handlers, Vercel Edge
Functions, Cloudflare Workers, and Hono routes that expose a web request.
Use the `svix-id` header as your idempotency key if duplicate sends matter.
Svix retries non-`2xx` responses.
## Cookbook and example [#cookbook-and-example]
This page is the minimal working path. For React Email templates, custom auth
delivery, the Workers request shape, and a full runnable project, follow the
cookbook and example:
* [Clerk cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/clerk.md)
* [Next.js Clerk webhook example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/clerk-webhook)
* [Clerk webhook docs](https://clerk.com/docs/webhooks/overview)
# Effect SDK
URL: https://samva.app/docs/integrations/effect-sdk
Use the [`samva/effect`](https://github.com/AryaLabsHQ/samva/tree/main/packages/sdk/src/effect)
entrypoint when your app already uses [Effect](https://effect.website). It gives
you typed Samva errors, `Effect.retry` with `Schedule`, and a Layer-provided
client over the fetch HTTP transport.
Samva sends from the verified sender configured on your account, so the
Effect SDK examples do not include a `from` field.
## Install [#install]
```sh
bun add samva effect@4.0.0-beta.85
```
Keep `SAMVA_API_KEY` server-side only. The Effect SDK currently uses
`effect/unstable/http`, so pin a compatible Effect 4 beta while the namespace is
still pre-stable.
## First send [#first-send]
Use `createClient` inside an Effect program and provide `FetchHttpClient.layer`
at the edge of the program:
```ts
import { Effect } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import { createClient } from "samva/effect";
const program = Effect.gen(function* () {
const samva = yield* createClient({ apiKey: process.env.SAMVA_API_KEY! });
return yield* samva.email.send({
to: "ada@example.com",
subject: "Welcome",
html: "
Hi Ada
",
text: "Hi Ada",
});
}).pipe(Effect.provide(FetchHttpClient.layer));
const message = await Effect.runPromise(program);
console.log(message.id, message.status);
```
## Layer-provided client [#layer-provided-client]
In a real app, build the layer once and read `SamvaClient` wherever you need to
send:
```ts
import { Effect } from "effect";
import { SamvaClient } from "samva/effect";
const SamvaLayer = SamvaClient.layerFetch({
apiKey: process.env.SAMVA_API_KEY!,
});
const sendWelcome = (to: string) =>
Effect.gen(function* () {
const samva = yield* SamvaClient;
return yield* samva.email.send({
to,
subject: "Welcome",
html: "
Your workspace is ready.
",
text: "Your workspace is ready.",
});
});
await Effect.runPromise(sendWelcome("ada@example.com").pipe(Effect.provide(SamvaLayer)));
```
`SamvaClient.layerFetch(config)` uses `globalThis.fetch`. Use
`SamvaClient.layer(config)` if you want to provide a custom
`HttpClient.HttpClient` implementation.
## Typed errors and retry [#typed-errors-and-retry]
`email.send` is backed by the generated `messages.send` operation, so generated
error tags are operation-status tags such as `MessagesSend429` and
`MessagesSend422`. Match those wrapper tags, then read the status-specific
`cause`.
```ts
import { Effect, Schedule } from "effect";
const retryableSend = sendWelcome("ada@example.com").pipe(
Effect.retry({
schedule: Schedule.exponential("200 millis").pipe(Schedule.jittered),
times: 3,
while: (error) =>
error._tag === "MessagesSend429" ||
error._tag === "MessagesSend500" ||
error._tag === "MessagesSend502",
}),
Effect.catchTags({
MessagesSend422: (error) =>
Effect.succeed({
status: 400,
fields: error.cause.fields ?? {},
}),
MessagesSend401: () =>
Effect.succeed({
status: 500,
message: "SAMVA_API_KEY is invalid or missing permissions.",
}),
MessagesSend429: (error) =>
Effect.succeed({
status: 429,
retryAfterSeconds:
typeof error.cause.retryAfterSeconds === "number" &&
Number.isFinite(error.cause.retryAfterSeconds)
? error.cause.retryAfterSeconds
: undefined,
}),
MessagesSend500: () =>
Effect.succeed({
status: 502,
message: "Samva returned a transient server error after retries.",
}),
MessagesSend502: () =>
Effect.succeed({
status: 502,
message: "Samva returned a transient gateway error after retries.",
}),
}),
);
```
## React Email and edge runtimes [#react-email-and-edge-runtimes]
Samva takes rendered `html` and optional `text`. If you use React Email, render
the component first and pass the strings to `email.send`; the
[React Email integration](/docs/integrations/react-email) covers templates and
previewing in more depth.
Because the Effect transport uses fetch, the same send path runs in Bun, Node
with fetch, Vercel Edge, and Cloudflare Workers. Pass the API key from your
server or edge environment binding, not from browser code.
## Cookbook and example [#cookbook-and-example]
The [Effect SDK cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/effect-sdk.md)
goes deeper: the layer-provided client, typed error tags, retrying with `Schedule`,
and React Email on edge runtimes.
* [Effect SDK cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/effect-sdk.md)
* [`examples/effect-sdk`](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/effect-sdk)
If your app does not use Effect, use the standard
[TypeScript SDK](/docs/developers/typescript-sdk) instead.
# Hono on Cloudflare Workers
URL: https://samva.app/docs/integrations/hono-cloudflare-workers
Send email from a [Hono](https://hono.dev) app running on
[Cloudflare Workers](https://developers.cloudflare.com/workers/) with the
`samva` SDK. The send path runs on Workers with `fetch`, so it does not need
SMTP, Node APIs, or `nodejs_compat`.
Use Worker bindings for secrets. Read `c.env.SAMVA_API_KEY` inside the route
handler instead of `process.env` or a module-top client.
## Install [#install]
```sh
bun add hono samva
bun add -d wrangler @cloudflare/workers-types typescript
```
Store the Samva API key as a Worker secret:
```sh
wrangler secret put SAMVA_API_KEY
```
For local development, put the same binding in `.dev.vars`:
```ini
SAMVA_API_KEY="samva_sk_live_..."
```
## Route handler [#route-handler]
```ts
import { Hono } from "hono";
import { createClient } from "samva";
type Bindings = {
SAMVA_API_KEY: string;
};
type SendRequestBody = {
to?: unknown;
subject?: unknown;
html?: unknown;
text?: unknown;
};
const app = new Hono<{ Bindings: Bindings }>();
const isRecord = (value: unknown): value is Record =>
typeof value === "object" && value !== null && !Array.isArray(value);
const readString = (value: unknown): string => (typeof value === "string" ? value.trim() : "");
app.post("/send", async (c) => {
const body: unknown = await c.req.json().catch(() => null);
if (!isRecord(body)) {
return c.json({ error: "Expected a JSON object." }, 400);
}
const { to, subject, html, text } = body as SendRequestBody;
const recipientEmail = readString(to);
const emailSubject = readString(subject);
if (!recipientEmail || !emailSubject) {
return c.json({ error: "to and subject are required" }, 400);
}
const samva = createClient({ apiKey: c.env.SAMVA_API_KEY });
const { data, error } = await samva.messages.send({
to: [{ email: recipientEmail }],
channel: "email",
email: {
subject: emailSubject,
html: readString(html) || "
Hello from Hono on Cloudflare Workers.
",
text: readString(text) || undefined,
},
});
if (error) {
return c.json({ ok: false, error }, 502);
}
return c.json({ ok: true, id: data?.id });
});
export default app;
```
Samva sends from the verified sender configured on your account, so the payload
has no `from` field.
## Worker config [#worker-config]
Use a module Worker entrypoint and omit Node compatibility flags:
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "hono-cloudflare-workers-samva",
"main": "src/index.ts",
"compatibility_date": "2026-06-29"
}
```
Hono's `export default app` maps to the Worker `fetch(request, env, ctx)` entry,
and Hono exposes Worker bindings through `c.env`.
## waitUntil [#waituntil]
Await `samva.messages.send(...)` for user-facing sends where the caller should
see API errors. Use `c.executionCtx.waitUntil(...)` only for background work
where returning immediately matters more than surfacing the send result.
```ts
c.executionCtx.waitUntil(
samva.messages.send({
to: [{ email: "ada@example.com" }],
channel: "email",
email: {
subject: "Background notification",
html: "
This send runs after the response.
",
},
}),
);
```
## React Email [#react-email]
React Email's render package has a Workers-compatible edge build. Render to
HTML, derive text, then pass both strings to Samva:
```sh
bun add @react-email/render react react-dom
```
```tsx
import { render, toPlainText } from "@react-email/render";
const html = await render();
await samva.messages.send({
to: [{ email: "ada@example.com" }],
channel: "email",
email: {
subject: "Welcome",
html,
text: toPlainText(html),
},
});
```
For deeper templating, see the [React Email integration](/docs/integrations/react-email).
## Webhook receiver shape [#webhook-receiver-shape]
If the Worker receives Samva webhooks, read the raw body before parsing JSON.
Signature verification belongs in the `samva/webhooks` SDK subpath.
Do not deploy this receiver until it verifies the Samva webhook signature. The
stub below only shows the raw-body shape that verification needs.
```ts
app.post("/webhooks/samva", async (c) => {
const payload = await c.req.text();
const signature = c.req.header("x-webhook-signature");
// Verify payload and signature with samva/webhooks before trusting the event.
void payload;
void signature;
return c.body(null, 204);
});
```
## Cookbook and example [#cookbook-and-example]
The [Hono on Cloudflare Workers cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/hono-cloudflare-workers.md)
goes deeper: the Worker seam, await versus `waitUntil` tradeoffs, React Email on
Workers, and the full webhook receiver shape. The [`hono-cloudflare-workers` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/hono-cloudflare-workers)
is a runnable Worker with `wrangler.jsonc`, `.dev.vars.example`, `POST /send`, and a
webhook stub.
* [Hono on Cloudflare Workers cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/hono-cloudflare-workers.md) - complete setup, send route, `waitUntil`, React Email, and webhook notes.
* [`hono-cloudflare-workers` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/hono-cloudflare-workers) - runnable Worker with `wrangler.jsonc`, `.dev.vars.example`, `POST /send`, and webhook stub.
* [Hono Cloudflare Workers docs](https://hono.dev/docs/getting-started/cloudflare-workers)
* [Cloudflare Workers docs](https://developers.cloudflare.com/workers/)
# Import the Postman collection
URL: https://samva.app/docs/integrations/import-postman
Import the Samva API into Postman to test and explore the email endpoints from a request client. Postman builds the collection from the published OpenAPI spec, so it always reflects the current API.
## Import the collection [#import-the-collection]
Postman can build a collection directly from the OpenAPI spec.
From a URL
From a file
1. Open Postman.
2. Click **Import** in the top left.
3. Select the **Link** tab.
4. Enter `https://api.samva.app/v1/openapi.json`.
5. Click **Continue**, then **Import**.
1. Open [api.samva.app/v1/openapi.json](https://api.samva.app/v1/openapi.json) and save the file locally.
2. In Postman, click **Import** then **Upload Files**.
3. Select the saved JSON file and import it.
## Authenticate the collection [#authenticate-the-collection]
Set your API key once at the collection level so every request inherits it.
1. Click the collection name in the sidebar.
2. Go to the **Variables** tab.
3. Add a variable named `apiKey` and set its value to your API key.
4. Click **Save**.
Then configure the collection's authorization:
1. Go to the **Authorization** tab.
2. Select **API Key** as the type.
3. Set **Key** to `x-api-key`.
4. Set **Value** to `{{apiKey}}`.
5. Set **Add to** to `Header`.
Requests authenticate with the `x-api-key` header. See [API keys](/docs/developers/authentication) for details on creating and scoping keys.
## Set up environments [#set-up-environments]
Use Postman environments to switch between production and staging without editing requests.
```json
// Production
{
"baseUrl": "https://api.samva.app/v1",
"apiKey": "samva_sk_live_your_api_key"
}
```
```json
// Staging
{
"baseUrl": "https://staging.api.samva.app/v1",
"apiKey": "samva_sk_test_your_api_key"
}
```
## Send a test request [#send-a-test-request]
1. Open the request for `POST /v1/messages`.
2. Set the body to an email send payload:
```json
{
"to": [{ "email": "ada@example.com" }],
"channel": "email",
"email": {
"subject": "Welcome to Samva",
"html": "
Welcome!
"
}
}
```
3. Click **Send** and check the response.
## Next steps [#next-steps]
# Integrations
URL: https://samva.app/docs/integrations
Integrations cover the glue between Samva and the rest of your stack: **API tooling** for exploring and testing the API outside your code, **webhook verification** for acting on Samva's outbound events, **Next.js** Server Actions and Route Handlers, **Hono on Cloudflare Workers** edge sends, **TanStack Start** server functions and server routes, **Astro** Actions and endpoints for framework-native sends, the **Effect SDK** for Effect-native sends, **SvelteKit** form actions and endpoints, **React Email** templating for component-based HTML email, **Better Auth** transactional email callbacks, **Supabase Auth** Send Email Hooks, **Clerk** webhook delivery for auth and lifecycle email, **Prisma** database-event patterns, and **Auth.js** magic-link email. Anything that can make an HTTP request can use the [REST API](/docs/developers/rest-api) directly.
# Next.js
URL: https://samva.app/docs/integrations/nextjs
Use the [`samva`](https://github.com/AryaLabsHQ/samva/tree/main/packages/sdk)
SDK directly from Next.js server code. Server Actions work well for forms, Route
Handlers work well for JSON endpoints, and both keep your API key server-side.
Samva sends from the verified sender configured on your account, so the
payload has no `from` field.
## Install [#install]
```sh
bun add samva server-only
```
Create a server-only client module:
```ts title="lib/samva.ts"
import "server-only";
import { createClient } from "samva";
const apiKey = process.env.SAMVA_API_KEY;
if (!apiKey) {
throw new Error("SAMVA_API_KEY is not set.");
}
export const samva = createClient({ apiKey });
```
## Server Action form [#server-action-form]
Next.js Server Actions let a form call server code directly:
```ts title="app/contact/actions.ts"
"use server";
import { samva } from "../../lib/samva";
export async function sendContactEmail(formData: FormData) {
const email = String(formData.get("email") ?? "").trim();
const message = String(formData.get("message") ?? "").trim();
if (!email || !message) {
throw new Error("Email and message are required.");
}
const safeMessage = message.replaceAll("<", "<").replaceAll(">", ">");
await samva.messages.send({
to: [{ email }],
channel: "email",
email: {
subject: "Thanks for contacting us",
html: `
Thanks for reaching out.
${safeMessage}
`,
text: `Thanks for reaching out.\n\n${message}`,
},
});
}
```
Use [`useActionState`](https://nextjs.org/docs/app/guides/forms) and
[`useFormStatus`](https://nextjs.org/docs/app/guides/forms) when the UI needs
pending and error state.
## Route Handler endpoint [#route-handler-endpoint]
Use an App Router Route Handler for JSON clients:
```ts title="app/api/send/route.ts"
import { samva } from "../../../lib/samva";
export const runtime = "edge";
export async function POST(request: Request) {
const body = (await request.json()) as { email?: unknown; message?: unknown };
const email = typeof body.email === "string" ? body.email.trim() : "";
const message = typeof body.message === "string" ? body.message.trim() : "";
if (!email || !message) {
return Response.json({ ok: false, error: "email and message are required" }, { status: 400 });
}
const safeMessage = message.replaceAll("<", "<").replaceAll(">", ">");
let result;
try {
result = await samva.messages.send({
to: [{ email }],
channel: "email",
email: {
subject: "Thanks for contacting us",
html: `
Thanks for reaching out.
${safeMessage}
`,
text: `Thanks for reaching out.\n\n${message}`,
},
});
} catch {
return Response.json({ ok: false, error: "Failed to send message." }, { status: 502 });
}
return Response.json({ ok: true, result });
}
```
The SDK is fetch-based, so this send path can run in the
[Next.js Edge runtime](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#runtime).
Choose Node instead when the surrounding handler needs Node-only database
drivers, filesystem access, or other Node built-ins.
## React Email [#react-email]
Render React Email to HTML, then send the strings with Samva:
```tsx
import { render, toPlainText } from "react-email";
import { samva } from "./lib/samva";
import WelcomeEmail from "./emails/welcome";
const html = await render();
await samva.messages.send({
to: [{ email: "ada@example.com" }],
channel: "email",
email: { subject: "Welcome", html, text: toPlainText(html) },
});
```
See the [React Email integration](/docs/integrations/react-email) for the full
templating workflow.
## Full cookbook and example [#full-cookbook-and-example]
The [Next.js cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/nextjs.md)
goes deeper: choosing between a Server Action and a Route Handler, fire-and-forget
versus awaited sends, the Pages Router equivalent, and edge-versus-Node tradeoffs.
The [`nextjs-transactional` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/nextjs-transactional)
is a runnable project with both send paths wired up.
* [Next.js cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/nextjs.md)
* [`nextjs-transactional` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/nextjs-transactional)
* [Next.js Server Actions and forms](https://nextjs.org/docs/app/guides/forms)
* [Next.js Route Handlers](https://nextjs.org/docs/app/getting-started/route-handlers)
# Prisma
URL: https://samva.app/docs/integrations/prisma
Use Prisma for persistence and the `samva` SDK for transactional email. The
primary pattern is query-then-send: create or update the row, then send email
from the persisted data.
Samva sends from the verified sender configured on your account, so there is
no `from` field in the send payload.
## Install [#install]
```sh
bun add samva @prisma/client
```
Keep `SAMVA_API_KEY` server-side only:
```ts
import "server-only";
import { createClient } from "samva";
const apiKey = process.env.SAMVA_API_KEY;
if (!apiKey) {
throw new Error("SAMVA_API_KEY is required");
}
export const samva = createClient({ apiKey });
```
## Query, then send [#query-then-send]
Use the committed row as the source of truth for the email:
```ts
import { prisma } from "@/lib/prisma";
import { samva } from "@/lib/samva";
const order = await prisma.order.create({
data: {
email: "ada@example.com",
total: 4900,
status: "confirmed",
},
});
await samva.messages.send({
to: [{ email: order.email }],
channel: "email",
email: {
subject: `Order #${order.id} confirmed`,
html: `
Thanks. Your order total is ${order.total}.
`,
},
});
```
This keeps the boundary clear: Prisma owns database state, Samva owns delivery.
If you need durable retries, write an outbox row in the same database transaction
and let a worker send from that outbox after commit.
## Data-layer hook [#data-layer-hook]
For advanced cases, Prisma Client Extensions can centralize the send around a
specific write:
```ts
import { PrismaClient } from "@prisma/client";
import { samva } from "@/lib/samva";
const base = new PrismaClient();
const prisma = base.$extends({
name: "samva-order-emails",
query: {
order: {
async create({ args, query }) {
const order = await query(args);
await samva.messages.send({
to: [{ email: order.email }],
channel: "email",
email: {
subject: `Order #${order.id} confirmed`,
html: `
Thanks. Your order total is ${order.total}.
`,
},
});
return order;
},
},
},
});
```
Use this only when the data layer really owns the side effect. For transaction
correctness, send after an interactive transaction resolves or use an outbox;
email delivery cannot be rolled back with the database write.
## Runtime notes [#runtime-notes]
The Samva SDK is `fetch`-based, so the send works in Node, Workers, and edge
runtimes. Prisma can query at the edge when you use the Rust-free client with an
edge-compatible driver adapter.
React Email works well for templates: render the component to `html`, derive a
plain-text fallback, and pass both strings to Samva. See the
[React Email integration](/docs/integrations/react-email) for the template
workflow.
Signup, reset, magic link, and OTP emails should live in your auth callbacks
instead of a generic Prisma write hook. See the
[Better Auth integration](/docs/integrations/better-auth) for that callback
shape.
## Full cookbook [#full-cookbook]
For the Server Action and Route Handler variants of this pattern, the Prisma Client Extension approach with its transaction caveats, the Prisma 6 vs. Prisma 7 driver-adapter split for edge runtimes, and an FAQ on idempotent retries and bulk sends, see the [Prisma cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/prisma.md).
* [Prisma documentation](https://www.prisma.io)
# React Email
URL: https://samva.app/docs/integrations/react-email
[React Email](https://react.email) lets you build HTML email as React components:
typed props, reusable pieces, and Tailwind, instead of hand-written table markup.
Render a template to HTML and send it with the `samva` SDK; Samva is the **"samva"**
entry in the React Email provider matrix.
React Email renders; Samva sends. The two are independent; Samva takes the
rendered `html` (and a `text` fallback) like any other email.
## Render and send [#render-and-send]
```sh
bun add react-email samva
```
```tsx
import { render, toPlainText } from "react-email";
import { createClient } from "samva";
import VerifyEmail from "./emails/verify-email";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const html = await render();
await samva.messages.send({
to: [{ email: "ada@example.com" }],
channel: "email",
email: { subject: "Verify your email", html, text: toPlainText(html) },
});
```
`render` is async; `await` it. `toPlainText` derives a plain-text fallback from
the HTML. There is **no `from`**: Samva sends from the verified sender configured
on your account.
## A Tailwind template [#a-tailwind-template]
Wrap the email in `` and style with utility classes; React Email inlines
them into email-safe CSS at render time:
```tsx
import { Body, Button, Container, Heading, Html, Tailwind, Text } from "react-email";
export const VerifyEmail = ({ url }: { url: string }) => (
Confirm your email
Confirm this address to activate your account.
);
```
## Render on the edge [#render-on-the-edge]
`render` needs no Node built-ins and the SDK is `fetch`-based, so you can render
and send end-to-end from a Cloudflare Worker, Vercel Edge function, or any edge
runtime, with no separate build step at runtime.
## Preview while you build [#preview-while-you-build]
With `react-email` installed, the `email` CLI previews templates locally and
hot-reloads as you edit:
```sh
bunx email dev # live preview at http://localhost:3000
```
## Full cookbook and example [#full-cookbook-and-example]
The [React Email cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/react-email.md)
goes deeper: the plain-text fallback with `toPlainText`, a full `` template
walkthrough, and why the edge runtime needs no CLI at send time. The
[`react-email-samva` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/react-email-samva)
is a runnable app with a polished `` template and a send script.
* [React Email cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/react-email.md), the complete walkthrough, including the plain-text fallback and edge rendering.
* [`react-email-samva` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/react-email-samva), a runnable app with a polished `` template and a send script.
# Supabase Auth
URL: https://samva.app/docs/integrations/supabase-auth
Use a Supabase Auth Send Email Hook to route signup, invite, magic link,
recovery, email change, and reauthentication emails through Samva. Supabase owns
auth; your hook verifies the signed request, renders the email, and calls
`samva.messages.send`.
Samva sends from the verified sender configured on your account. The hook does
not pass a `from` value.
## Hook config [#hook-config]
Enable the Send Email Hook in `supabase/config.toml`:
```toml
[auth.hook.send_email]
enabled = true
uri = "https://"
secrets = "env(SEND_EMAIL_HOOK_SECRET)"
```
For local Supabase CLI development, point the URI at your served Edge Function:
```toml
uri = "http://host.docker.internal:54321/functions/v1/send-email"
```
Set server-only secrets in the function environment:
```sh
SAMVA_API_KEY=samva_sk_live_...
SEND_EMAIL_HOOK_SECRET=v1,whsec_
SUPABASE_PROJECT_REF=your-project-ref
```
Serve Supabase Edge Functions with JWT verification disabled, because the Auth
hook fires before a user JWT exists:
```sh
supabase functions serve send-email --no-verify-jwt
```
Enabling the hook overrides Supabase's built-in email and Custom SMTP sending for
the covered Auth flows.
## Endpoint shape [#endpoint-shape]
The handler reads the raw body, verifies the Standard Webhooks signature, renders
by `email_action_type`, and returns an empty `200` on success.
```ts
import { Webhook } from "standardwebhooks";
import { createClient } from "samva";
const samva = createClient({ apiKey: Deno.env.get("SAMVA_API_KEY")! });
const secret = Deno.env.get("SEND_EMAIL_HOOK_SECRET")!.replace("v1,whsec_", "");
const webhook = new Webhook(secret);
Deno.serve(async (request) => {
const rawBody = await request.text();
let payload: SendEmailHookPayload;
try {
payload = webhook.verify(rawBody, Object.fromEntries(request.headers)) as SendEmailHookPayload;
} catch {
return new Response("invalid signature", { status: 401 });
}
const rendered = await renderForAction(payload.email_data);
await samva.messages.send({
to: [{ email: payload.user.email }],
channel: "email",
email: rendered,
});
return new Response(null, { status: 200 });
});
```
Read the body before parsing JSON. The signature covers the raw request body.
## Verify links [#verify-links]
Build Supabase confirmation links against the Auth verify endpoint with
`token_hash`, not the six-digit `token`:
```ts
function buildVerifyURL(emailData: EmailData) {
const params = new URLSearchParams({
token: emailData.token_hash,
type: emailData.email_action_type,
redirect_to: emailData.redirect_to || emailData.site_url,
});
return `https://${Deno.env.get("SUPABASE_PROJECT_REF")}.supabase.co/auth/v1/verify?${params}`;
}
```
`reauthentication` is OTP-only. `email_change` may send one or two emails
depending on your Secure Email Change setting; follow Supabase's token/hash
pairs for the current and new email addresses.
## Templates [#templates]
Render React Email components to HTML and derive a text fallback:
```tsx
import { render, toPlainText } from "react-email";
const html = await render();
const text = toPlainText(html);
```
The Supabase hook only needs small per-action templates. For Tailwind, previews,
and reusable email components, see [React Email](/docs/integrations/react-email).
## Failure behavior [#failure-behavior]
Return an empty `200` only after Samva accepts the send. Return `401` for bad
signatures and a non-200 response for unsupported `email_action_type` values or
send failures.
The example rejects notification action types and the bare `email` OTP sign-in
type until you add explicit templates for them.
## Cookbook and example [#cookbook-and-example]
For the full signature verification helper, the verify-link builder for each `email_action_type`, dispatch code covering all six action types (including the two-email `email_change` case), and the right status codes to return, see the [Supabase Auth cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/supabase-auth.md). The [Supabase Auth hook example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/supabase-auth-hook) is a runnable Edge Function.
* [Official Supabase Send Email Hook docs](https://supabase.com/docs/guides/auth/auth-hooks/send-email-hook)
* [Better Auth](/docs/integrations/better-auth), if your app uses Better Auth callbacks instead of Supabase Auth hooks.
# SvelteKit
URL: https://samva.app/docs/integrations/sveltekit
Use the [`samva`](https://github.com/AryaLabsHQ/samva/tree/main/packages/sdk)
SDK directly from SvelteKit server code. Form actions work well for in-app
forms, `+server.ts` endpoints work well for JSON clients, and both keep your API
key server-side.
Samva sends from the verified sender configured on your account, so the
payload has no `from` field.
## Install [#install]
```sh
bun add samva
```
Create a server-only client helper:
```ts title="src/lib/server/samva.ts"
import { env } from "$env/dynamic/private";
import { createClient } from "samva";
export function getSamva() {
const apiKey = env.SAMVA_API_KEY;
if (!apiKey) {
throw new Error("SAMVA_API_KEY is not set.");
}
return createClient({ apiKey });
}
```
`$env/*/private` and `src/lib/server` are server-only. Do not expose
`SAMVA_API_KEY` through a public env prefix.
## Form action quickstart [#form-action-quickstart]
Form actions let a SvelteKit page submit directly to server code:
```ts title="src/routes/contact/+page.server.ts"
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types";
import { getSamva } from "$lib/server/samva";
const escapeHtml = (value: string): string =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
const field = (formData: FormData, name: string): string =>
String(formData.get(name) ?? "").trim();
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
const email = field(formData, "email");
const message = field(formData, "message");
if (!email || !message) {
return fail(400, { error: "Email and message are required." });
}
await getSamva().messages.send({
to: [{ email }],
channel: "email",
email: {
subject: "Thanks for contacting us",
html: `
${escapeHtml(message).replaceAll("\n", " ")}
`,
text: message,
},
});
return { success: true };
},
} satisfies Actions;
```
In `+page.svelte`, render the action result from the `form` prop and add
[`use:enhance`](https://svelte.dev/docs/kit/form-actions#Progressive-enhancement)
when you want pending state.
## Endpoint quickstart [#endpoint-quickstart]
Use a `+server.ts` endpoint for raw HTTP:
```ts title="src/routes/api/send/+server.ts"
import { error, json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getSamva } from "$lib/server/samva";
const escapeHtml = (value: string): string =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
const isRecord = (value: unknown): value is Record =>
typeof value === "object" && value !== null && !Array.isArray(value);
const readString = (value: unknown): string => (typeof value === "string" ? value.trim() : "");
export const POST: RequestHandler = async ({ request }) => {
const body: unknown = await request.json().catch(() => null);
if (!isRecord(body)) {
error(400, "Expected a JSON object.");
}
const to = readString(body.to);
const subject = readString(body.subject);
const message = readString(body.message);
if (!to || !subject || !message) {
error(400, "to, subject, and message are required.");
}
await getSamva().messages.send({
to: [{ email: to }],
channel: "email",
email: {
subject,
html: `
${escapeHtml(message).replaceAll("\n", " ")}
`,
text: message,
},
});
return json({ ok: true });
};
```
The endpoint awaits the send. Invalid payloads fail loudly with `400`; missing
credentials throw from the server-only client module.
## Cloudflare Workers [#cloudflare-workers]
The SDK uses `fetch`, so the same send call works with
[`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare).
Prefer SvelteKit's `$env/dynamic/private` module for runtime secrets, or
`$env/static/private` when the key is available during build/typecheck. If the
key is only available as a Worker binding, construct the client per request from
`platform.env`:
```ts
import { error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { createClient } from "samva";
export const POST: RequestHandler = async ({ platform }) => {
const apiKey = platform?.env.SAMVA_API_KEY;
if (!apiKey) {
error(500, "SAMVA_API_KEY is not configured for this Worker.");
}
const samva = createClient({ apiKey });
// Read and validate request data, then call samva.messages.send().
};
```
The runtime remains edge-safe as long as the code around the send avoids
Node-only database drivers, filesystem access, SMTP sockets, and Node built-ins.
## React Email [#react-email]
Render React Email to HTML, then send the strings with Samva:
```tsx
import { render, toPlainText } from "react-email";
import WelcomeEmail from "$lib/emails/welcome";
import { getSamva } from "$lib/server/samva";
const html = await render();
await getSamva().messages.send({
to: [{ email: "ada@example.com" }],
channel: "email",
email: { subject: "Welcome", html, text: toPlainText(html) },
});
```
See the [React Email integration](/docs/integrations/react-email) for the full
templating workflow.
## Auth.js magic links [#authjs-magic-links]
For `@auth/sveltekit`, replace the email provider's
`sendVerificationRequest({ identifier, url })` body with a Samva send. Auth.js
still owns the verification token and requires a database adapter for email
sign-in.
```ts
async sendVerificationRequest({ identifier, url }) {
const { host } = new URL(url);
await getSamva().messages.send({
to: [{ email: identifier }],
channel: "email",
email: {
subject: `Sign in to ${host}`,
html: `
`,
text: `Sign in to ${host}\n${url}\n`,
},
});
}
```
Drop `provider.from` when porting from Resend or Nodemailer; Samva has no
`from` field.
## Full cookbook and example [#full-cookbook-and-example]
The [SvelteKit cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/sveltekit.md)
goes deeper: choosing a form action versus a `+server.ts` endpoint, `$env/static/private`
versus `$env/dynamic/private`, why sends should never run from `load`, and Auth.js
magic links. The [`sveltekit-transactional` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/sveltekit-transactional)
is a runnable project with both send paths wired up.
* [SvelteKit cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/sveltekit.md)
* [`sveltekit-transactional` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/sveltekit-transactional)
* [SvelteKit form actions](https://svelte.dev/docs/kit/form-actions)
* [SvelteKit routing and endpoints](https://svelte.dev/docs/kit/routing)
# TanStack Start
URL: https://samva.app/docs/integrations/tanstack-start
Send transactional email from TanStack Start with the `samva` SDK. Use a server
function for in-app flows, or a server route when an external system needs to
POST raw HTTP to your app.
The same integration shape works on edge runtimes: server-only API keys, a
fetch-based SDK, and React Email rendering when you need component templates.
## Install [#install]
```sh
bun add samva zod
```
Set a server-only API key. Do not use a `VITE_` prefix.
```sh
SAMVA_API_KEY=samva_sk_live_your_api_key
```
```ts title="src/lib/samva.ts"
import "@tanstack/react-start/server-only";
import { createClient } from "samva";
export function getSamva() {
const apiKey = process.env.SAMVA_API_KEY;
if (!apiKey) {
throw new Error("SAMVA_API_KEY is not set");
}
return createClient({ apiKey });
}
```
Build the client inside the handler. On Cloudflare Workers and other edge SSR
runtimes, `process.env` is injected per request, so module-scope env reads can
run too early.
## Server function quickstart [#server-function-quickstart]
```ts title="src/functions/send-email.ts"
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import { getSamva } from "~/lib/samva";
const escapeHtml = (value: string) =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
export const sendEmail = createServerFn({ method: "POST" })
.validator(
z.object({
to: z.string().email(),
subject: z.string().min(1),
message: z.string().min(1),
}),
)
.handler(async ({ data }) => {
const html = `
${escapeHtml(data.message).replaceAll("\n", " ")}
`;
await getSamva().messages.send({
to: [{ email: data.to }],
channel: "email",
email: {
subject: data.subject,
html,
text: data.message,
},
});
return { ok: true };
});
```
Call it from a component with `useServerFn`:
```tsx
import { useServerFn } from "@tanstack/react-start";
import { useState } from "react";
import { sendEmail } from "~/functions/send-email";
export function ContactForm() {
const send = useServerFn(sendEmail);
const [pending, setPending] = useState(false);
return (
);
}
```
There is no `from` field. Samva sends from the verified sender configured on
your account.
## Server route quickstart [#server-route-quickstart]
Use a server route for raw HTTP:
```ts title="src/routes/api/send.ts"
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
import { getSamva } from "~/lib/samva";
const sendRouteInput = z
.object({
to: z.string().email(),
subject: z.string().min(1),
html: z.string().min(1).optional(),
text: z.string().min(1).optional(),
})
.refine((value) => value.html || value.text, {
message: "html or text is required",
});
const escapeHtml = (value: string) =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
export const Route = createFileRoute("/api/send")({
server: {
handlers: {
POST: async ({ request }) => {
const payload = await request.json().catch(() => null);
const parsed = sendRouteInput.safeParse(payload);
if (!parsed.success) {
return Response.json({ error: "Invalid send payload" }, { status: 400 });
}
const body = parsed.data;
const text = body.text;
const html = body.html ?? (text ? `
${escapeHtml(text).replaceAll("\n", " ")}
` : undefined);
await getSamva().messages.send({
to: [{ email: body.to }],
channel: "email",
email: {
subject: body.subject,
...(html ? { html } : {}),
...(text ? { text } : {}),
},
});
return Response.json({ ok: true });
},
},
},
});
```
Server functions are RPC endpoints too, so enforce auth and rate limits inside
the server function, middleware, or server route handler.
## React Email [#react-email]
Add React Email when you want component templates:
```sh
bun add react-email react react-dom
```
```tsx
import { render, toPlainText } from "react-email";
import WelcomeEmail from "~/emails/welcome";
import { getSamva } from "~/lib/samva";
const html = await render();
await getSamva().messages.send({
to: [{ email: "ada@example.com" }],
channel: "email",
email: {
subject: "Welcome",
html,
text: toPlainText(html),
},
});
```
See the [React Email integration](/docs/integrations/react-email) for template
structure, previewing, and edge rendering details.
## Full cookbook and example [#full-cookbook-and-example]
The [TanStack Start cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/tanstack-start.md)
goes deeper: choosing a server function versus a server route, why `VITE_SAMVA_API_KEY`
is unsafe, what should fail loudly versus fail soft, and how to queue bulk sends. The
[`tanstack-start-transactional` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/tanstack-start-transactional)
is a runnable project with both send paths wired up.
* [TanStack Start cookbook](https://github.com/AryaLabsHQ/samva-integrations/blob/main/cookbooks/tanstack-start.md)
* [`tanstack-start-transactional` example](https://github.com/AryaLabsHQ/samva-integrations/tree/main/examples/tanstack-start-transactional)
* [TanStack Start server functions](https://tanstack.com/start/latest/docs/framework/react/guide/server-functions)
* [TanStack Start server routes](https://tanstack.com/start/latest/docs/framework/react/guide/server-routes)
# Verify webhooks
URL: https://samva.app/docs/integrations/webhooks
Samva signs every outbound webhook so you can confirm a request genuinely came
from Samva before acting on it. The `samva` SDK's
[`samva/webhooks`](/docs/developers/typescript-sdk) export verifies that signature
for you; it's edge-safe (WebCrypto), so the same code runs on Node, Bun,
Cloudflare Workers, and Vercel Edge.
`samva/webhooks` only **verifies** signatures. Create endpoints and rotate
signing secrets with the [webhooks API](/docs/api-reference/webhooks) or the SDK.
## The signature [#the-signature]
Each delivery is a `POST` with the signature in a header:
```http
POST /webhooks/samva
X-Webhook-Signature: sha256=
X-Webhook-Event: message.delivered
X-Webhook-Id:
{ "event": "message.delivered", "messageId": "msg_…", "timestamp": "…", "data": { … } }
```
The signature is `HMAC-SHA256(secret, rawBody)`, hex-encoded. Verify the **raw**
body; re-serializing it (for example, via JSON middleware) changes the bytes and
breaks verification.
## Verify a request [#verify-a-request]
```sh
bun add samva
```
A Web `Request` (Next.js Route Handlers, Hono, TanStack Start, Remix, Workers):
```ts
import { verifyRequest, WebhookVerificationError } from "samva/webhooks";
export async function POST(req: Request) {
try {
const event = await verifyRequest(req, process.env.SAMVA_WEBHOOK_SECRET!);
switch (event.event) {
case "message.delivered":
// mark delivered…
break;
case "message.bounced":
// suppress the address…
break;
}
return new Response(null, { status: 200 });
} catch (err) {
if (err instanceof WebhookVerificationError) {
return new Response("invalid signature", { status: 400 });
}
throw err;
}
}
```
A Node `IncomingMessage`: use the `/node` adapter and a raw-body parser, since
JSON middleware re-serializes the body.
```ts
import { verifyNodeRequest } from "samva/webhooks/node";
// app.post("/webhooks/samva", express.raw({ type: "application/json" }), handler)
```
## Tips [#tips]
* **Return `2xx` fast**, then do any slow follow-up work asynchronously.
* **Be idempotent.** Webhooks are delivered at least once. Dedupe on `event` + `messageId`
together; a single message emits several events (`sent`, `delivered`, `read`, …), so `messageId`
alone would drop valid ones.
For the full list of events and for endpoint management, see the
[webhooks API reference](/docs/api-reference/webhooks).
# Platform
URL: https://samva.app/docs/platform
The platform layer holds the organization-level capabilities around email: how delivery and inbound
events reach you, who you send to, what you send, and when a send starts. Start with the email guides
for task-oriented workflows, then use this section for shared configuration.
# Email messaging model
URL: https://samva.app/docs/platform/unified-messaging
Every email goes through the same message API and shares contacts, conversations, delivery events,
and templates. This model covers immediate email, scheduled email, and each recipient dispatched by
a campaign run.
## One send API [#one-send-api]
Every send is a message: a piece of content, a recipient, and the channel that carries it.
`POST /v1/messages` names those parts explicitly: a `channel` discriminator, a `to` array, and
email content nested under the `email` key.
```json
{
"to": [{ "contactId": "contact_123" }],
"channel": "email",
"email": { "subject": "Welcome", "html": "
Welcome!
" }
}
```
The `email.send()` SDK facade is that same call wearing email-shaped clothes. For the full model,
why both shapes exist and when to reach for each, see
[Email and the unified API](/docs/developers/email-and-the-unified-api).
## Shared primitives [#shared-primitives]
Email sends reuse the same organization-level primitives:
* **Contacts**: address a `{ contactId }` without re-supplying an email address per send.
* **Conversations**: inbound replies thread into the same conversation model as outbound email.
* **Events & webhooks**: delivery, status, and inbound events arrive in one signed stream; subscribe
once. See [Webhooks](/docs/platform/webhooks).
* **Templates**: versioned email content rendered at send time and referenced by `templateId` or
`templateSlug`.
Scheduled messages and campaigns use these same primitives and finish on the normal email delivery
path.
## Related [#related]
# Webhook event catalog
URL: https://samva.app/docs/platform/webhook-events
Samva delivers webhook events to the endpoints you register. This page catalogs the events the
platform emits, the type string each carries, when it fires, and the payload fields it delivers.
To register an endpoint and verify signatures, see [Receive webhooks](/docs/platform/webhooks).
## Envelope [#envelope]
Every delivery is a `POST` with a JSON body in a fixed envelope. The `event` field is the event
type string; `messageId` is the message the event is about; `timestamp` is the delivery time in
ISO 8601; and `data` carries the event-specific fields.
```json
{
"event": "message.delivered",
"messageId": "msg_01j8k3m5n7p9q2r4s6tv",
"timestamp": "2026-01-15T09:42:11.204Z",
"data": {
"channel": "email",
"status": "delivered",
"providerMessageId": "0100018f2a...-000000"
}
}
```
Each request also carries the `X-Webhook-Event` (event type), `X-Webhook-Id` (endpoint id), and
`X-Webhook-Signature` headers. Verify the signature before you trust the body, as described in
[Receive webhooks](/docs/platform/webhooks).
The `data` object is populated on a best-effort basis: a field is present only when the underlying
provider event supplied it. Treat every field inside `data` as optional and code defensively.
## Message events [#message-events]
Message events track a single message through its send and delivery lifecycle. Every message
event carries the message's `channel` and a `status` string inside `data`, plus email provider
fields when available.
### message.sent [#messagesent]
Fires when a provider accepts the message for delivery. This is the handoff to the upstream mail
or messaging provider, not confirmation that the recipient received it.
| Field | Type | Notes |
| ------------------- | ------ | ------------------------------------------------------- |
| `channel` | string | `email`. |
| `status` | string | `sent`. |
| `providerMessageId` | string | The upstream provider's id for the message, when known. |
| `metadata` | object | Metadata you supplied on the original send, when set. |
| `cost` | number | Provider-reported cost, when the provider reports it. |
```json
{
"event": "message.sent",
"messageId": "msg_01j8k3m5n7p9q2r4s6tv",
"timestamp": "2026-01-15T09:42:10.031Z",
"data": {
"channel": "email",
"status": "sent",
"providerMessageId": "0100018f2a...-000000",
"toEmails": ["ada@example.com"]
}
}
```
### message.delivered [#messagedelivered]
Fires when the recipient's mail server accepts the email.
| Field | Type | Notes |
| ------------------- | ------ | ----------------------------------------------------- |
| `channel` | string | `email`. |
| `status` | string | `delivered`. |
| `providerMessageId` | string | The upstream provider's id, when known. |
| `metadata` | object | Metadata you supplied on the original send, when set. |
```json
{
"event": "message.delivered",
"messageId": "msg_01j8k3m5n7p9q2r4s6tv",
"timestamp": "2026-01-15T09:42:14.882Z",
"data": {
"channel": "email",
"status": "delivered",
"providerMessageId": "0100018f2a...-000000",
"subject": "Welcome to Samva",
"toEmails": ["ada@example.com"]
}
}
```
### message.failed [#messagefailed]
Fires when a message cannot be delivered. The failure may originate from the provider, from a
suppressed recipient, or from an error while processing the send. `failureReason` is a
human-readable explanation; `errorCode` is a machine-readable code when the source provides one.
| Field | Type | Notes |
| --------------- | ------ | ------------------------------------------------------------ |
| `channel` | string | `email`. |
| `status` | string | `failed`. |
| `failureReason` | string | Human-readable reason for the failure. |
| `errorCode` | string | Machine-readable error code, when the provider supplies one. |
| `errorMessage` | string | Provider error detail, when supplied. |
| `metadata` | object | Metadata you supplied on the original send, when set. |
```json
{
"event": "message.failed",
"messageId": "msg_01j8k3m5n7p9q2r4s6tv",
"timestamp": "2026-01-15T09:42:12.507Z",
"data": {
"channel": "email",
"status": "failed",
"failureReason": "Permanent",
"errorCode": "General"
}
}
```
### message.bounced [#messagebounced]
Fires for email when the recipient's mail server rejects the message after acceptance, or when a
recipient marks it as spam. `failureReason` carries the bounce or complaint type.
| Field | Type | Notes |
| ------------------- | --------- | --------------------------------------- |
| `channel` | string | `email`. |
| `status` | string | `bounced` or `complained`. |
| `providerMessageId` | string | The upstream provider's id, when known. |
| `failureReason` | string | Bounce type or complaint type. |
| `errorCode` | string | Bounce sub-type, when supplied. |
| `subject` | string | The message subject, when available. |
| `toEmails` | string\[] | The recipient addresses on the message. |
```json
{
"event": "message.bounced",
"messageId": "msg_01j8k3m5n7p9q2r4s6tv",
"timestamp": "2026-01-15T09:43:01.117Z",
"data": {
"channel": "email",
"status": "bounced",
"providerMessageId": "0100018f2a...-000000",
"failureReason": "Permanent",
"errorCode": "General",
"subject": "Welcome to Samva",
"toEmails": ["ada@example.com"]
}
}
```
### message.received [#messagereceived]
Fires when Samva receives an inbound email and routing matches one of your endpoints. The
`direction` field is `inbound`.
| Field | Type | Notes |
| ---------------- | ------- | ------------------------------------------------------- |
| `channel` | string | `email`. |
| `direction` | string | `inbound`. |
| `from` | string | The sender's email address. |
| `to` | string | The email address that received the message. |
| `conversationId` | string | The conversation the inbound message was threaded into. |
| `subject` | string | Email subject. |
| `hasAttachments` | boolean | Whether an inbound email carried attachments. |
| `isAutoReply` | boolean | Whether an inbound email was detected as an auto-reply. |
```json
{
"event": "message.received",
"messageId": "msg_01j8k4n6p8q2r4s6t9vw",
"timestamp": "2026-01-15T10:05:33.900Z",
"data": {
"channel": "email",
"direction": "inbound",
"from": "ada@example.com",
"to": "support@your-domain.com",
"subject": "Re: Welcome to Samva",
"conversationId": "conv_01j8k5p7q9r2s4t6v8wx",
"hasAttachments": false,
"isAutoReply": false
}
}
```
## Subscribing to events [#subscribing-to-events]
You subscribe an endpoint to events with the `events` array when you create or update it. Pass the
type strings above, for example `["message.delivered", "message.failed"]`. Subscribe only to the
events your application acts on; you can register multiple endpoints to route different events to
different services. See [Receive webhooks](/docs/platform/webhooks) for the full registration and
verification flow.
## Next steps [#next-steps]
# Receive webhooks
URL: https://samva.app/docs/platform/webhooks
This guide shows you how to receive Samva email events in your own application: register a
webhook endpoint, choose which events you want, and verify each request's signature before you
trust it.
These are **customer webhooks**: Samva delivering events to an endpoint in *your* app (for
example, when an email is delivered or fails). They are distinct from provider ingress, which is
how Samva receives events from upstream email infrastructure. This guide is only about the
former.
## Before you start [#before-you-start]
* An API key with permission to manage webhooks. See [API keys](/docs/developers/authentication).
* A publicly reachable HTTPS endpoint in your app that can accept `POST` requests.
* A shared webhook secret stored in your environment (for example `WEBHOOK_SECRET`) so your
handler can verify signatures.
## 1. Register a webhook endpoint [#1-register-a-webhook-endpoint]
Create a webhook by sending a `POST` to `/v1/webhooks` with the public `url` Samva should call
and the list of `events` you want delivered to it.
```bash
curl -X POST https://api.samva.app/v1/webhooks \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Order events",
"url": "https://your-app.com/webhooks/samva",
"events": ["message.delivered", "message.failed"]
}'
```
Or with the TypeScript SDK:
```typescript
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const result = await samva.webhooks.create({
name: "Order events",
url: "https://your-app.com/webhooks/samva",
events: ["message.delivered", "message.failed"],
});
if (result.error) {
console.error("Failed to register webhook:", result.error);
} else {
console.log("Registered webhook:", result.data?.id);
}
```
## 2. Choose your events [#2-choose-your-events]
Subscribe only to the events your app acts on. The message lifecycle emits:
| Event | Fires when |
| ------------------- | ------------------------------------------------- |
| `message.sent` | A provider accepted the message for delivery. |
| `message.delivered` | The recipient's provider confirmed delivery. |
| `message.failed` | The message could not be delivered. |
| `message.bounced` | An accepted email was rejected or marked as spam. |
| `message.received` | Samva received an inbound message routed to you. |
Pass the events you want in the `events` array when you register or update the endpoint. You can
register multiple endpoints to route different events to different services. For each event's
payload fields and a representative JSON body, see the
[Webhook event catalog](/docs/platform/webhook-events).
## 3. Verify the signature in your handler [#3-verify-the-signature-in-your-handler]
Every webhook request includes an `X-Webhook-Signature` header: an HMAC-SHA256 of the request
body keyed with your webhook secret, formatted as `sha256=`. Samva also sends
`X-Webhook-Event` (the event name) and `X-Webhook-Id` (the endpoint id), which help you route
requests. Recompute the HMAC over the exact payload you received, prefix it with `sha256=`, and
compare it in constant time. Reject any request whose signature does not match before you process
it.
```typescript
import crypto from "crypto";
function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
const hmac = crypto.createHmac("sha256", secret);
const expected = `sha256=${hmac.update(payload).digest("hex")}`;
const signatureBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expected);
// timingSafeEqual throws on differing lengths; guard before comparing.
if (signatureBuffer.length !== expectedBuffer.length) return false;
return crypto.timingSafeEqual(signatureBuffer, expectedBuffer);
}
// Register the raw body parser on this route so `req.body` is the exact bytes
// Samva signed. A JSON parser would re-serialize the payload and break the HMAC.
app.post("/webhooks/samva", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-webhook-signature"];
const webhookSecret = process.env.WEBHOOK_SECRET;
const rawBody = req.body.toString("utf8");
if (typeof webhookSecret !== "string" || webhookSecret.length === 0) {
return res.status(500).send("Webhook secret is not configured");
}
// Reject when the header is missing or duplicated (string[]) before verifying;
// passing a non-string into the HMAC comparison would throw.
if (
typeof signature !== "string" ||
!verifyWebhookSignature(rawBody, signature, webhookSecret)
) {
return res.status(401).send("Invalid signature");
}
// Signature verified; parse and handle the event.
const event = JSON.parse(rawBody);
res.status(200).send("OK");
});
```
Compare against the **raw** request body bytes. If your framework re-serializes the body,
the HMAC will not match; capture the payload exactly as received.
Return a `2xx` status as soon as you have verified and accepted the event. Samva treats any
`2xx` as accepted; non-`2xx` responses are retried by the delivery queue. Returning `401` (or any
non-`2xx`) when the signature does not match is your handler's own choice: it signals rejection
and is not a Samva-defined response code.
## Next steps [#next-steps]
# Authoring in TSX
URL: https://samva.app/docs/sml/authoring-tsx
SML (the primitives + class-subset language documented on the rest of this
site) is deliberately a small, flat, closed grammar, no components, no props,
no composition. That's a good property for a compiled document format: it
keeps per-keystroke compile fast, keeps the editor's canvas round-trip
lossless, and keeps the built-in agent's grammar small enough to hold in a
system prompt. It's a worse property for hand-authoring anything with real
structure, reusable sections, typed inputs, composition.
TSX is the answer to that: **a typed authoring layer that compiles down to
SML**, not a second runtime format. You write idiomatic TypeScript/JSX
functions; a build step evaluates them once, on your machine, and emits plain
`.samva` source. The platform never sees your TSX, never runs your build tool,
and never executes your code. It only ever receives and recompiles SML, the
same closed grammar this whole reference documents.
## One default export per file [#one-default-export-per-file]
Each `.tsx` file owns at most one template, and that template is its `default`
export:
```tsx
export default function WelcomeEmail(v: Variables<{ firstName: string }>) {
return (
Welcome aboard, {v.firstName}
Welcome, {v.firstName}
);
}
```
The file is the template identity. Named exports are ignored by template
discovery, so you can use them for helpers, constants, and ordinary composition
without accidentally creating additional templates. A TSX file without a
default export is excluded with a warning; a default export that does not return
``, ``, or `` is a build diagnostic.
The components (``, ``, ``, ...) mirror the SML
primitives one for one, same tags, same attributes, same content model. There's
no new vocabulary to learn beyond what's already on the
[primitives reference](/docs/sml/primitives); TSX just gives that vocabulary
TypeScript's type-checking, editor autocomplete, and composition via ordinary
functions and imports.
## Evaluation, not parsing [#evaluation-not-parsing]
This is the core mechanical difference from SML: **TSX is evaluated, never
statically analyzed.** The build step runs your function with a real
JavaScript engine, any TypeScript that's valid to run at build time is
accepted (loops, conditionals over build-time data, helper functions, imports
from your own codebase, npm packages). The *output* of running that function
(the JSX tree it returns) is what gets validated against SML's closed grammar
The same schemas and checks validate hand-written `.samva` source.
Because it's evaluation and not parsing, there's nothing to sandbox: your code
runs on your machine, in your build, the same trust boundary as the rest of
your toolchain. Nothing executes on Samva's infrastructure, the platform's
only input is the emitted SML.
## `Variables` typed send-time holes [#variablest-typed-send-time-holes]
`export default function WelcomeEmail(v: Variables<{firstName: string}>)` declares the
template's variable schema as an ordinary TypeScript type, and the emitter
invokes your function with a recording proxy in place of real data. Reading
`v.firstName` doesn't return a string, it returns a reference that, when it
ends up somewhere in the JSX tree, prints as `{{firstName}}` in the emitted
SML. The declared type doubles as the variable schema the platform validates
sends against, so there's exactly one place variables are declared, not a
TypeScript type and a separate JSON schema that can drift apart.
## The build-time-vs-send-time rule [#the-build-time-vs-send-time-rule]
Variables are **send-time holes**, real values don't exist until a specific
email is being sent to a specific recipient, which is long after your build
ran. `v.firstName` is a placeholder, not a string, at the moment your function
executes.
This means ordinary JavaScript coercion of a variable reference is a
build-time error, not a build-time computation: comparing it (`v.plan ===
"pro"`), concatenating it (`` `Hi ${v.firstName}!` `` outside JSX text
position), or doing arithmetic on it throws immediately when the emitter runs
your function. There is no value yet to compare, concatenate, or compute
with, only a reference to where a value will eventually go. Letting plain JS
operators silently coerce that reference to `"[object Object]"` or `NaN`
would silently emit a broken template instead of failing loudly at the one
point (your local build) where you can actually see the mistake.
The fix is the same variable reference's typed predicate builders, which
construct a **predicate reference** instead of coercing to a real value:
```tsx
// Throws at build time, plain coercion, not a builder call:
if (v.plan === "pro") { /* ... */ }
// Correct, builder methods construct a PredicateRef, never coerce:
Your team plan covers {v.seats} seats.Thanks for being a pro customer, {v.firstName}.
```
`VariableRef` grows typed comparison methods per field (`eq`/`ne`/`gt`/`gte`/
`lt`/`lte`, typed by the field's declared type), presence checks
(`exists`/`notExists`), and boolean chaining (`and`/`or`/`not`) that compose
`PredicateRef`s. The emitter prints the resulting `PredicateRef` as an SML
`test="..."` string using the exact same [predicate grammar](/docs/sml/conditionals#the-predicate-grammar)
documented for hand-written SML, comparisons over flat names and literals,
`and`/`or`/`not`, no arithmetic, no paths. The builders exist specifically so
that grammar is reachable through typed, autocompletable TypeScript instead of
string-templating a predicate by hand.
This is the same shape as `{{}}`/`${}` interpolation always had, variables
were never usable in real `if` statements or template-string logic, because
"real" values don't exist at build time. ``/predicate builders are the
one expressiveness upgrade over plain interpolation: a typed, send-time-safe
way to branch, without ever pretending a variable is a build-time value.
## The pipeline [#the-pipeline]
```
template.tsx (default export)
│ (Vite JSX transform, evaluated at YOUR build time)
▼
SmlDocument AST (in memory)
│ print
▼
canonical .samva source
│ samva push → sends SOURCE ONLY, never compiled HTML
▼
platform: parses + compiles with the org's theme layers, server-side
▼
artifact HTML (+ plain-text alt), stored content-addressed
```
Two things worth being precise about:
* **Local preview runs the full chain**, including the SML → HTML compiler.
the same pure library the platform Worker, the editor's browser canvas, and
the test suite all call. A local dev server can show you real compiled
output, with hot reload, entirely offline.
* **Push sends SML, never HTML.** The platform always recompiles server-side,
against the organization's current brand-kit theme and target-client
profile. This is what keeps a template re-themeable after the fact and
keeps "what compiled this" a server-side, auditable fact, a local
toolchain that's a version behind can't ship a stale-looking artifact,
because it never ships an artifact at all.
## Composition lives here, not in SML [#composition-lives-here-not-in-sml]
SML's primitive set is permanently flat and closed, no ``, no
``, no user-defined macros, ever. Composition (a reusable "email
footer" block, a shared header with props, a library of on-brand sections) is
a TSX-authoring-layer concern: it's ordinary function composition, the same
way you'd share a React component. The emitter inlines the result, a
composed TSX template still emits one flat SML document, because that's the
only shape the platform (and the built-in agent, and the canvas) ever has to
understand.
## Determinism [#determinism]
The emitter runs your template function **twice** and diffs the two emitted
SML strings. Anything that makes those two runs differ, `Date.now()`,
`Math.random()`, reading mutable external state, is flagged as a build-time
warning, because a non-deterministic template makes diffing pushed versions
meaningless and breaks the emission-determinism guarantee the whole pipeline
relies on.
## Managed-from-code templates in the dashboard [#managed-from-code-templates-in-the-dashboard]
A template pushed from TSX carries `origin: tsx` on its stored version. The
dashboard canvas stays fully editable, dashboard edits aren't blocked, but
the editor shows a "managed from code" banner, and `samva push` warns (or, in
stricter setups, refuses) if the repo's version has diverged from what's
currently published, the same posture `git push --force-with-lease` takes
toward a moved remote. Whichever side is the source of truth for a given
template, the repo for a code-managed team, the dashboard for a
template authored natively there, the underlying version history (every save
is a commit, with rollback) makes the divergence visible either way.
## Who writes what [#who-writes-what]
* **The built-in template agent** always writes SML directly, the closed
grammar and span-precise diagnostics are exactly what makes its
write → validate → retry loop converge quickly. It has no reason to go
through a TSX layer.
* **A customer's own agent, working in their repo**, writes TSX idiomatically
, its iteration loop is `tsc`, the emitter's determinism/coercion checks,
and ultimately the same SML diagnostics, all through standard tooling a
code-focused agent already knows how to drive.
## Related [#related]
* [Conditionals](/docs/sml/conditionals), the predicate grammar the typed
builders emit into, and the full send-time-vs-build-time explanation from
the SML side.
* [Variables](/docs/sml/variables), `{{name}}`/`${name}` interpolation
semantics, unchanged underneath the typed `Variables` layer.
* [Primitives](/docs/sml/primitives), the tag vocabulary TSX components
mirror one for one.
# Class-subset reference
URL: https://samva.app/docs/sml/classes
{/* GENERATED FILE. Do not edit. Regenerate with `bun run codegen:docs` after
changing any `description`/`warn` field in packages/markup/src/registry.ts. */}
SML supports a fixed subset of Tailwind utility classes on every primitive's `class`
attribute, chosen for guaranteed rendering across Gmail, Outlook (classic and new),
Apple Mail, and Yahoo. Anything outside this subset is a compile error naming the
class and the reason, not a silent drop.
Two variants are supported: `sm:` (progressive enhancement above a mobile baseline)
and `dark:` (color-only, honored by clients that support `prefers-color-scheme`);
see [Responsive and dark-mode variants](/docs/sml/variants).
### Padding [#padding]
| Class | Description |
| ------ | ------------------------------------------------------------------------- |
| `p-*` | Padding on all sides (p-4 = 16px). Compiles onto table cells for Outlook. |
| `px-*` | Horizontal padding. |
| `py-*` | Vertical padding. |
| `pt-*` | Top padding. |
| `pr-*` | Right padding. |
| `pb-*` | Bottom padding. |
| `pl-*` | Left padding. |
### Margin [#margin]
| Class | Description |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `m-*` | Margin on all sides. Warn tier, some clients strip margins. **Fix:** Margin is stripped by some clients (Outlook.com dark mode, some webmail); prefer padding or \. |
| `mx-*` | Horizontal margin; mx-auto centers block elements. **Fix:** Margin is stripped by some clients (Outlook.com dark mode, some webmail); prefer padding or \. |
| `my-*` | Vertical margin. **Fix:** Margin is stripped by some clients (Outlook.com dark mode, some webmail); prefer padding or \. |
| `mt-*` | Top margin. **Fix:** Margin is stripped by some clients (Outlook.com dark mode, some webmail); prefer padding or \. |
| `mr-*` | Right margin. **Fix:** Margin is stripped by some clients (Outlook.com dark mode, some webmail); prefer padding or \. |
| `mb-*` | Bottom margin. **Fix:** Margin is stripped by some clients (Outlook.com dark mode, some webmail); prefer padding or \. |
| `ml-*` | Left margin. **Fix:** Margin is stripped by some clients (Outlook.com dark mode, some webmail); prefer padding or \. |
### Sizing [#sizing]
| Class | Description |
| --------- | ---------------------------------------------------------------------------- |
| `w-*` | Width from the spacing scale (w-32 = 128px), w-full, or w-\[600px]/w-\[50%]. |
| `max-w-*` | Max width from the container scale (max-w-lg = 512px) or max-w-\[600px]. |
| `h-*` | Height from the spacing scale (h-12 = 48px) or h-\[80px]. |
### Typography [#typography]
| Class | Description |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `text-*` | Font size with its paired line-height (text-sm = 14px/20px). |
| `text-*` | Text alignment: text-left, text-center, text-right. |
| `text-*` | Text color from the palette (text-gray-700) or text-\[#374151]. |
| `leading-*` | Line height: named ratios (leading-relaxed) or spacing steps (leading-6 = 24px). |
| `tracking-*` | Letter spacing: tracking-tighter … tracking-widest (em-based; inlines fine in Outlook). Pair tracking-wide/wider with uppercase for eyebrows. |
| `font-*` | Font weight: font-normal, font-medium, font-semibold, font-bold, ... |
| `font-*` | Font stack: font-sans, font-serif, font-mono. |
| `uppercase` | Uppercase text transform. |
| `underline` | Underlined text. |
| `no-underline` | Removes the underline (links render underlined by default in most clients). |
### Color [#color]
| Class | Description |
| ------ | --------------------------------------------------------------------------- |
| `bg-*` | Background color from the palette (bg-white, bg-blue-600) or bg-\[#0088cc]. |
### Background gradient [#background-gradient]
| Class | Description |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bg-gradient-*` | Linear gradient background for hero sections: bg-gradient-to-b/t/l/r with from-\{color} and to-\{color}. Modern base compiles a CSS linear-gradient (plus a solid from-color fallback); the outlook-safe base degrades to the solid from-color, Outlook for Windows never renders the gradient. |
| `from-*` | Gradient start color (from-indigo-600). Doubles as the solid fallback color on clients without gradient support (Outlook for Windows). |
| `to-*` | Gradient end color (to-purple-600). |
### Border [#border]
| Class | Description |
| ----------- | ---------------------------------------------------------------------------------------------------- |
| `border-*` | Solid border width: border (1px), border-2, border-0. |
| `border-*` | Border color from the palette (border-gray-200). |
| `rounded-*` | Border radius (rounded-md = 6px). Ignored by Outlook for Windows, buttons degrade to square corners. |
### Display [#display]
| Class | Description |
| -------------- | ---------------------------------------------------------- |
| `block` | display: block. |
| `inline-block` | display: inline-block. |
| `hidden` | display: none. Pair with sm:block for mobile-only content. |
## Forbidden utilities [#forbidden-utilities]
These compile to a typed error naming the failing client and the
recommended alternative. The compiler drops the feature rather than ship
something that silently breaks in that client.
| Class | Why it's rejected |
| ------------- | --------------------------------------------------------------------------------------------- |
| `flex` | display:flex is unsupported in Outlook for Windows (Word engine), use \/\ |
| `inline-flex` | display:inline-flex is unsupported in Outlook for Windows, use \/\ |
| `grid` | display:grid is unsupported in Outlook for Windows and Gmail, use \/\ |
| `inline-grid` | display:inline-grid is unsupported in Outlook for Windows, use \/\ |
| `gap-` | gap requires flex/grid, unsupported in Outlook for Windows, use padding on \ |
| `space-` | space-\* margins require selectors Gmail strips, use padding or \ |
| `items-` | flexbox alignment is unsupported in Outlook for Windows, use \/\ |
| `justify-` | flexbox alignment is unsupported in Outlook for Windows, use \/\ and text-center |
| `self-` | flexbox alignment is unsupported in Outlook for Windows, use \/\ |
| `content-` | flexbox alignment is unsupported in Outlook for Windows, use \/\ |
| `order-` | flex order is unsupported in Outlook for Windows, reorder the source instead |
| `absolute` | CSS position is ignored by Gmail and Outlook, use nested \s |
| `relative` | CSS position is ignored by Gmail and Outlook, use nested \s |
| `fixed` | CSS position is ignored by Gmail and Outlook, use nested \s |
| `sticky` | CSS position is ignored by Gmail and Outlook, use nested \s |
| `static` | CSS position is ignored by Gmail and Outlook, remove it |
| `inset-` | position offsets are ignored by Gmail and Outlook, use padding |
| `top-` | position offsets are ignored by Gmail and Outlook, use padding |
| `right-` | position offsets are ignored by Gmail and Outlook, use padding |
| `bottom-` | position offsets are ignored by Gmail and Outlook, use padding |
| `left-` | position offsets are ignored by Gmail and Outlook, use padding |
| `z-` | z-index is ignored by Gmail and Outlook, remove it |
| `float-` | float is unreliable across email clients, use \/\ |
| `shadow` | box-shadow is unsupported in Outlook for Windows and Gmail, use a border instead |
| `shadow-` | box-shadow is unsupported in Outlook for Windows and Gmail, use a border instead |
| `opacity-` | opacity is unsupported in Outlook for Windows, bake the tint into the color |
| `transform` | CSS transforms are unsupported in email clients, remove them |
| `rotate-` | CSS transforms are unsupported in email clients, remove them |
| `scale-` | CSS transforms are unsupported in email clients, remove them |
| `translate-` | CSS transforms are unsupported in email clients, remove them |
| `skew-` | CSS transforms are unsupported in email clients, remove them |
| `transition` | transitions don't run in email, remove them |
| `transition-` | transitions don't run in email, remove them |
| `duration-` | transitions don't run in email, remove them |
| `animate-` | animations don't run in email, remove them |
| `blur-` | CSS filters are unsupported in most email clients, remove them |
| `brightness-` | CSS filters are unsupported in most email clients, remove them |
| `grayscale` | CSS filters are unsupported in most email clients, remove them |
| `grayscale-` | CSS filters are unsupported in most email clients, remove them |
| `truncate` | text truncation is unsupported in Outlook for Windows, shorten the copy |
| `line-clamp-` | line clamping is unsupported in email clients, shorten the copy |
| `aspect-` | aspect-ratio is unsupported in Outlook and Gmail, set width/height on \ |
| `overflow-` | overflow is unreliable across email clients, restructure the layout |
## Arbitrary values [#arbitrary-values]
Most families accept an arbitrary value in brackets: `w-[320px]`, `bg-[#0088cc]`,
`max-w-[600px]`. Arbitrary values that reference `var()`, `calc()`, `oklch()`,
`rgb()`/`rgba()`, or `hsl()` are rejected: compiled email has no CSS custom
properties, and alpha channels render solid black in Outlook for Windows. Use a
literal hex color or px value instead.
# Conditionals
URL: https://samva.app/docs/sml/conditionals
SML has one control-flow primitive: ``, with an optional ``. It selects
between two branches of markup **per recipient, at send time**, not at compile
time. This is the only conditional logic SML has; there's no loop, no arbitrary
expression, and no per-item repetition (that's ``, deferred past v1).
`` is a child of ``, not a sibling, and it must come last:
```xml
Your team plan covers {{seats}} seats.Thanks for being a pro customer, {{firstName}}.
```
## Content model [#content-model]
`` accepts the same block children as ``/`` (``,
``, ``, `