# 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: "

See you tomorrow.

" }, }, scheduledFor: "2026-08-01T09:00:00Z", }, }); return yield* samva.scheduledMessages.get(scheduled.id); }).pipe(Effect.provide(FetchHttpClient.layer)); await Effect.runPromise(program); ``` ## Use the REST API [#use-the-rest-api] ```bash curl -X POST https://api.samva.app/v1/messages/scheduled \ -H "X-API-Key: $SAMVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "send": { "to": [{ "email": "ada@example.com" }], "channel": "email", "email": { "subject": "Reminder", "html": "

See you tomorrow.

" } }, "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: "

August updates

" }, }, audience: { includeTags: ["newsletter"] }, }, }); return yield* samva.campaigns.scheduleRun(campaign.id, { payload: { scheduledFor: "2026-08-01T09:00:00Z", idempotencyKey: "august-newsletter-v1", }, }); }).pipe(Effect.provide(FetchHttpClient.layer)); await Effect.runPromise(program); ``` ## Use the REST API [#use-the-rest-api] ```bash curl -X POST https://api.samva.app/v1/campaigns \ -H "X-API-Key: $SAMVA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "August newsletter", "channel": "email", "content": { "channel": "email", "email": { "subject": "What is new", "html": "

August updates

" } }, "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: `

Sign in to ${host}

`, 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 (
{ "use server"; await signIn("samva", formData); }} >
); } ``` ## 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: `

Sign in to ${host}

`, 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 (
{ event.preventDefault(); const form = new FormData(event.currentTarget); setPending(true); try { await send({ data: { to: String(form.get("to") ?? ""), subject: String(form.get("subject") ?? ""), message: String(form.get("message") ?? ""), }, }); } finally { setPending(false); } }} >