> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lettr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Send Email

> Send a transactional email with optional attachments, tracking, and personalization. At least one of `html` or `text` content must be provided. Supply `Idempotency-Key` for safe retries using SparkPost native transmission idempotency. Keys are namespaced per Lettr API key and retained by SparkPost for 24 hours. Repeating the same transmission payload returns its original result without another delivery; conflicting payloads or in-progress transmissions return 409. Retry transport failures with the same key. Lettr still validates authorization, domains and sending quota before contacting SparkPost, so an exhausted quota returns 429 even for a retry. Replayed responses do not increment sending counters again. Requests without this header are sent independently.

Sends an email to one or more recipients. Supports HTML, plain text, AMP content, templates, merge tags, attachments, and tracking options.

Open and click tracking are enabled by default. Set `options.open_tracking` or `options.click_tracking` to `false` in the request body to disable either. The `X-Lettr-Track-*` control headers are SMTP-only; see [Tracking](/learn/sending/tracking) for examples.

Available to [sandbox API keys](/learn/api-keys/sandbox): all recipients are redirected to the key owner's inbox and the sender domain is replaced with the pre-verified sandbox domain.


## OpenAPI

````yaml POST /emails
openapi: 3.1.0
info:
  title: Lettr API
  version: 1.0.0
  description: >-
    Lettr Email API - Send transactional emails with tracking, attachments, and
    personalization.
  contact:
    name: Lettr Support
    url: https://lettr.com
  license:
    name: Proprietary
    url: https://lettr.com/terms
servers:
  - url: https://app.lettr.com/api
    description: Production
security: []
tags:
  - name: Emails
    description: Email sending operations
  - name: Templates
    description: Email template management operations
  - name: Domains
    description: Domain management operations
  - name: Webhooks
    description: Webhook management operations
  - name: Audience
    description: 'Audience management: lists, contacts, topics, properties, and segments'
  - name: Campaigns
    description: >-
      Campaign operations: listing, stats, engagement events, and
      dispatch/scheduling
paths:
  /emails:
    post:
      tags:
        - Emails
      summary: Send Email
      description: >-
        Send a transactional email with optional attachments, tracking, and
        personalization. At least one of `html` or `text` content must be
        provided. Supply `Idempotency-Key` for safe retries using SparkPost
        native transmission idempotency. Keys are namespaced per Lettr API key
        and retained by SparkPost for 24 hours. Repeating the same transmission
        payload returns its original result without another delivery;
        conflicting payloads or in-progress transmissions return 409. Retry
        transport failures with the same key. Lettr still validates
        authorization, domains and sending quota before contacting SparkPost, so
        an exhausted quota returns 429 even for a retry. Replayed responses do
        not increment sending counters again. Requests without this header are
        sent independently.
      operationId: sendEmail
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            A unique key for one logical send. Reuse it with the same payload
            and API key when retrying. SparkPost retains the key for 24 hours.
            If using a template, keep the resolved template content unchanged
            across retries.
          schema:
            type: string
            minLength: 1
            maxLength: 255
            pattern: ^[A-Za-z0-9._-]{1,255}$
          example: order-confirmation-12345
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendEmailRequest'
            examples:
              minimal:
                summary: Minimal request (required fields only)
                description: >-
                  The absolute minimum required to send an email: from, to,
                  subject, and either html or text content. Subject is required
                  when not using a template.
                value:
                  from: sender@example.com
                  to:
                    - recipient@example.com
                  subject: Hello
                  html: <p>Hello!</p>
              html-and-text:
                summary: HTML and plain text (recommended)
                description: >-
                  Sending both HTML and plain text ensures email renders
                  correctly in all email clients.
                value:
                  from: sender@example.com
                  from_name: Sender Name
                  to:
                    - recipient@example.com
                  subject: Welcome to Lettr
                  html: <h1>Hello</h1><p>Welcome to our service!</p>
                  text: |-
                    Hello

                    Welcome to our service!
                  options:
                    click_tracking: true
                    open_tracking: true
              html-only:
                summary: HTML only
                description: >-
                  Send HTML content only. Some email clients may not render
                  properly without a text fallback.
                value:
                  from: sender@example.com
                  to:
                    - recipient@example.com
                  subject: Welcome to Lettr
                  html: <h1>Hello</h1><p>Welcome to our service!</p>
              text-only:
                summary: Plain text only
                description: Send plain text content only. Ideal for simple notifications.
                value:
                  from: sender@example.com
                  to:
                    - recipient@example.com
                  subject: Order Confirmation
                  text: |-
                    Your order #12345 has been confirmed.

                    Thank you for your purchase!
              with-attachments:
                summary: Email with attachments
                description: >-
                  Send an email with file attachments. Attachments must be
                  base64 encoded.
                value:
                  from: billing@example.com
                  from_name: Billing Department
                  to:
                    - customer@example.com
                  subject: Your Invoice
                  html: <p>Please find your invoice attached.</p>
                  text: Please find your invoice attached.
                  attachments:
                    - name: invoice.pdf
                      type: application/pdf
                      data: JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9UeXBlL1BhZ2U=
              with-personalization:
                summary: Personalized email with substitution
                description: >-
                  Use substitution_data to personalize email content with
                  dynamic variables.
                value:
                  from: hello@example.com
                  from_name: Acme Inc
                  to:
                    - john@example.com
                  subject: Welcome, {{first_name}}!
                  html: <h1>Hello {{first_name}}</h1><p>Welcome to {{company}}!</p>
                  text: |-
                    Hello {{first_name}},

                    Welcome to {{company}}!
                  substitution_data:
                    first_name: John
                    company: Acme Inc
                  metadata:
                    user_id: '12345'
                    campaign: onboarding
              with-template:
                summary: Email using a template
                description: >-
                  Send an email using a pre-defined template. The template's
                  HTML content and subject will be used. You can optionally
                  override the subject.
                value:
                  from: hello@example.com
                  to:
                    - john@example.com
                  template_slug: welcome-email
                  substitution_data:
                    first_name: John
              with-template-and-project:
                summary: Email using a template from a specific project
                description: >-
                  Send an email using a template from a specific project with a
                  specific version. Subject is taken from the template unless
                  explicitly provided.
                value:
                  from: hello@example.com
                  to:
                    - john@example.com
                  project_id: 123
                  template_slug: welcome-email
                  template_version: 2
                  substitution_data:
                    first_name: John
      responses:
        '200':
          description: >-
            Email queued for delivery. Free tier teams receive quota headers
            indicating their usage and limits.
          headers:
            Idempotency-Replayed:
              description: >-
                Present with value true when SparkPost returns an existing
                transmission. Quota headers describe current usage before this
                attempt.
              schema:
                type: string
                enum:
                  - 'true'
            X-Monthly-Limit:
              description: >-
                Total monthly email limit for this team. Only present for free
                tier teams.
              schema:
                type: integer
                example: 3000
            X-Monthly-Remaining:
              description: >-
                Remaining emails allowed this month. Only present for free tier
                teams.
              schema:
                type: integer
                example: 2500
            X-Monthly-Reset:
              description: >-
                Unix timestamp when the monthly quota resets (start of next
                month). Only present for free tier teams.
              schema:
                type: integer
                example: 1740787200
            X-Daily-Limit:
              description: >-
                Total daily email limit for this team. Only present for free
                tier teams.
              schema:
                type: integer
                example: 100
            X-Daily-Remaining:
              description: >-
                Remaining emails allowed today. Only present for free tier
                teams.
              schema:
                type: integer
                example: 75
            X-Daily-Reset:
              description: >-
                Unix timestamp when the daily quota resets (tomorrow midnight
                UTC). Only present for free tier teams.
              schema:
                type: integer
                example: 1739600000
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendEmailSuccessResponse'
              example:
                message: Email queued for delivery.
                data:
                  request_id: '12345678901234567890'
                  accepted: 1
                  rejected: 0
        '400':
          description: Failed to send email (domain errors)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalid_domain:
                  summary: Invalid sender domain
                  value:
                    message: >-
                      The sender domain could not be determined from the email
                      address.
                    error_code: invalid_domain
                unconfigured_domain:
                  summary: Unconfigured sender domain
                  value:
                    message: >-
                      The sender domain is not configured or approved for
                      sending.
                    error_code: unconfigured_domain
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          description: Template or project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                template_not_found:
                  summary: Template not found
                  value:
                    message: Template with slug 'welcome-email' was not found.
                    error_code: template_not_found
                project_not_found:
                  summary: Project not found
                  value:
                    message: >-
                      Project with ID '123' was not found or you don't have
                      access to it.
                    error_code: template_not_found
                no_default_project:
                  summary: No default project configured
                  value:
                    message: No default project is configured for this team.
                    error_code: template_not_found
        '409':
          description: >-
            SparkPost idempotency conflict: the key was used with a different
            transmission payload, or the original transmission is still
            processing.
          headers:
            Retry-After:
              description: >-
                Present for idempotency_in_progress. Retry with the same key
                after this many seconds.
              schema:
                type: integer
                example: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                conflict:
                  value:
                    message: >-
                      This Idempotency-Key was already used with a different
                      request payload.
                    error_code: idempotency_key_conflict
                in_progress:
                  value:
                    message: >-
                      A request with this Idempotency-Key is still processing.
                      Retry with the same key.
                    error_code: idempotency_in_progress
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorResponse'
              example:
                message: Validation failed.
                error_code: validation_error
                errors:
                  from:
                    - The sender email address is required.
                  to:
                    - At least one recipient email address is required.
        '429':
          description: >-
            Sending quota exceeded. The response includes quota headers to help
            determine current usage and when the limit resets.
          headers:
            X-Monthly-Limit:
              description: Total monthly email limit for this team.
              schema:
                type: integer
                example: 3000
            X-Monthly-Remaining:
              description: Remaining emails allowed this month (0 when limit is hit).
              schema:
                type: integer
                example: 0
            X-Monthly-Reset:
              description: >-
                Unix timestamp when the monthly quota resets (start of next
                month).
              schema:
                type: integer
                example: 1740787200
            X-Daily-Limit:
              description: Total daily email limit for this team.
              schema:
                type: integer
                example: 100
            X-Daily-Remaining:
              description: Remaining emails allowed today (0 when limit is hit).
              schema:
                type: integer
                example: 0
            X-Daily-Reset:
              description: >-
                Unix timestamp when the daily quota resets (tomorrow midnight
                UTC).
              schema:
                type: integer
                example: 1739600000
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                monthly_quota_exceeded:
                  summary: Monthly sending quota exceeded
                  value:
                    message: >-
                      Sending quota exceeded. Upgrade your plan to continue
                      sending.
                    error_code: quota_exceeded
                daily_quota_exceeded:
                  summary: Daily sending quota exceeded
                  value:
                    message: Daily sending quota exceeded. Please try again tomorrow.
                    error_code: daily_quota_exceeded
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                message: Failed to send email. Please try again later.
                error_code: send_error
        '502':
          description: >-
            Email transmission failed (upstream provider error). Retry with the
            same Idempotency-Key so SparkPost can resolve an uncertain outcome
            without creating a duplicate transmission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                message: Unconfigured Sending Domain <sender.example.com>
                error_code: transmission_failed
        '503':
          $ref: '#/components/responses/ServiceUnavailableError'
      security:
        - bearerAuth: []
components:
  schemas:
    SendEmailRequest:
      type: object
      description: >-
        Email sending request. **Required:** `from`, `to`, and at least one of
        `html`, `text`, or `template_slug`. The `subject` is required unless
        `template_slug` is provided — when using a template, the subject
        defaults to the template's subject (or template name as fallback), but
        can be overridden. When using `template_slug`, if `project_id` is not
        provided, the team's default project will be used.
      required:
        - from
        - to
      properties:
        from:
          type: string
          format: email
          maxLength: 255
          description: Sender email address
          example: sender@example.com
        from_name:
          type:
            - string
            - 'null'
          maxLength: 255
          description: Sender display name
          example: Sender Name
        subject:
          type:
            - string
            - 'null'
          maxLength: 998
          description: >-
            Email subject line. Required when not using a template. When using
            `template_slug`, defaults to the template's subject (or template
            name as fallback). If provided alongside a template, overrides the
            template subject.
          example: Welcome to Lettr
        to:
          type: array
          description: Recipient email addresses
          minItems: 1
          maxItems: 50
          items:
            type: string
            format: email
            maxLength: 255
          example:
            - recipient@example.com
        cc:
          type: array
          description: Carbon copy recipient email addresses
          items:
            type: string
            format: email
            maxLength: 255
          example:
            - cc@example.com
        bcc:
          type: array
          description: Blind carbon copy recipient email addresses
          items:
            type: string
            format: email
            maxLength: 255
          example:
            - bcc@example.com
        reply_to:
          type:
            - string
            - 'null'
          format: email
          maxLength: 255
          description: Reply-To email address
          example: reply@example.com
        reply_to_name:
          type:
            - string
            - 'null'
          maxLength: 255
          description: Reply-To display name
          example: Reply Name
        html:
          type:
            - string
            - 'null'
          description: >-
            HTML content of the email. At least one of `html` or `text` is
            required, but providing both is recommended for best compatibility
            across email clients.
          example: <h1>Hello</h1><p>Welcome!</p>
        text:
          type:
            - string
            - 'null'
          description: >-
            Plain text content of the email. At least one of `html` or `text` is
            required, but providing both is recommended for best compatibility
            across email clients.
          example: |-
            Hello

            Welcome!
        amp_html:
          type:
            - string
            - 'null'
          description: AMP HTML content for supported email clients
        project_id:
          type:
            - integer
            - 'null'
          description: >-
            Project ID containing the template. If not provided when using
            template_slug, the team's default project will be used.
          example: 123
        template_slug:
          type:
            - string
            - 'null'
          maxLength: 255
          description: >-
            Template slug to use for email content. When provided, the
            template's HTML will be used instead of the html field.
          example: welcome-email
        template_version:
          type:
            - integer
            - 'null'
          minimum: 1
          description: >-
            Specific template version to use. If not provided, the active
            version will be used.
          example: 1
        tag:
          type: string
          maxLength: 64
          description: >-
            Tag for tracking and analytics. Automatically set from template_slug
            if not provided.
          example: welcome-series-2024
        metadata:
          type: object
          description: Custom metadata attached to the email for tracking purposes
          additionalProperties:
            type: string
          example:
            user_id: '12345'
            campaign: onboarding
        headers:
          type: object
          description: >-
            Custom email headers. Up to 10 headers allowed. Standard envelope
            headers (From, To, Subject, etc.), SparkPost internal headers
            (X-MSYS-API), and List-Unsubscribe and List-Unsubscribe-Post headers
            are managed by the system and cannot be set manually.
          maxProperties: 10
          additionalProperties:
            type: string
            maxLength: 998
          example:
            X-Mailer: MyApp/1.0
            X-Custom-ID: abc-123
        substitution_data:
          type: object
          description: Variables for template substitution in email content
          additionalProperties:
            type: string
          example:
            first_name: John
            company: Acme Inc
        options:
          type: object
          description: Email delivery options
          properties:
            click_tracking:
              type: boolean
              description: Enable click tracking for links in the email
            open_tracking:
              type: boolean
              description: Enable open tracking via tracking pixel
            transactional:
              type: boolean
              default: true
              description: >-
                Mark email as transactional (not marketing). **Defaults to
                `true`.** Transactional mail bypasses unsubscribe suppression,
                so any send a recipient can opt out of must explicitly set this
                to `false`.
            inline_css:
              type: boolean
              description: Inline CSS styles in HTML content
            perform_substitutions:
              type: boolean
              description: Perform variable substitutions in content
        attachments:
          type: array
          description: >-
            File attachments. When provided, each attachment must include name,
            type, and data.
          items:
            $ref: '#/components/schemas/Attachment'
    SendEmailSuccessResponse:
      type: object
      required:
        - message
        - data
      properties:
        message:
          type: string
          description: Human-readable success message
          example: Email queued for delivery.
        data:
          type: object
          required:
            - request_id
            - accepted
            - rejected
          properties:
            request_id:
              type: string
              description: Unique identifier for this email transmission
              example: '12345678901234567890'
            accepted:
              type: integer
              minimum: 0
              description: Number of recipients accepted for delivery
              example: 1
            rejected:
              type: integer
              minimum: 0
              description: Number of recipients rejected
              example: 0
    ErrorResponse:
      type: object
      required:
        - message
        - error_code
      description: Error response for non-validation errors (400, 409, 500, 502).
      properties:
        message:
          type: string
          description: Human-readable error message
          example: The sender domain could not be determined from the email address.
        error_code:
          $ref: '#/components/schemas/ErrorCode'
    ValidationErrorResponse:
      type: object
      required:
        - message
        - error_code
        - errors
      properties:
        message:
          type: string
          description: Human-readable error message
          example: Validation failed.
        error_code:
          type: string
          description: Error code (always `validation_error` for 422 responses)
          const: validation_error
          example: validation_error
        errors:
          type: object
          description: Field-specific validation errors
          additionalProperties:
            type: array
            items:
              type: string
          example:
            from:
              - The sender email address is required.
            to:
              - At least one recipient email address is required.
    Attachment:
      type: object
      description: Email attachment
      required:
        - name
        - type
        - data
      properties:
        name:
          type: string
          maxLength: 255
          description: Filename of the attachment
          example: invoice.pdf
        type:
          type: string
          maxLength: 255
          description: MIME type of the attachment
          example: application/pdf
        data:
          type: string
          pattern: ^[A-Za-z0-9+/=]+$
          description: Base64 encoded file content (no line breaks)
          example: JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9UeXBlL1BhZ2U=
    ErrorCode:
      type: string
      description: >-
        Error codes returned by the API.


        | Code | HTTP Status | Description |

        |------|-------------|-------------|

        | `validation_error` | 422 | Request validation failed. Check the
        `errors` object for field-specific messages. |

        | `invalid_domain` | 400 | The sender domain could not be determined
        from the email address. |

        | `unconfigured_domain` | 400 | The sender domain is not configured or
        approved for sending. |

        | `send_error` | 400, 500 | General error during email send preparation
        or domain registration. |

        | `retrieval_error` | 400, 500 | Failed to retrieve resources (e.g.,
        sandbox key without associated user, upstream error). |

        | `transmission_failed` | 502 | Email transmission to the upstream
        provider failed. |

        | `resource_already_exists` | 409 | The resource (e.g., domain) already
        exists. |

        | `not_found` | 404 | The specified resource was not found. |

        | `template_not_found` | 404 | The specified template, project, or
        template version was not found. |

        | `insufficient_scope` | 403 | The API key does not have the required
        scope for this endpoint. |

        | `schedule_cancellation_failed` | 409, 500 | The scheduled email can no
        longer be cancelled (already sent, already cancelled, or being injected
        right now), or the cancellation itself failed. |

        | `quota_exceeded` | 429 | Monthly sending quota exceeded. Upgrade your
        plan to continue sending. |

        | `daily_quota_exceeded` | 429 | Daily sending quota exceeded. Try again
        tomorrow. |

        | `campaign_not_sendable` | 422 | The campaign cannot be sent in its
        current state (not a draft, or missing subject/sender/content). |

        | `campaign_not_scheduled` | 422 | The campaign is not scheduled, so it
        cannot be unscheduled. |
      enum:
        - validation_error
        - idempotency_key_conflict
        - idempotency_in_progress
        - invalid_domain
        - unconfigured_domain
        - send_error
        - retrieval_error
        - transmission_failed
        - resource_already_exists
        - not_found
        - template_not_found
        - insufficient_scope
        - schedule_cancellation_failed
        - quota_exceeded
        - daily_quota_exceeded
        - campaign_not_sendable
        - campaign_not_scheduled
      example: invalid_domain
    UnauthorizedResponse:
      type: object
      required:
        - message
      description: Error response for authentication failures (401).
      properties:
        message:
          type: string
          description: Human-readable error message
          example: API key is required.
  responses:
    UnauthorizedError:
      description: Unauthorized - Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/UnauthorizedResponse'
          examples:
            missing_key:
              summary: Missing API key
              value:
                message: API key is required.
            invalid_key:
              summary: Invalid API key
              value:
                message: Invalid API key.
    ServiceUnavailableError:
      description: >-
        Service unavailable - the API key could not be verified, so the request
        was not processed. This does not mean the key is invalid; retry after
        the delay given in the Retry-After header.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
            example: 5
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            message: Unable to verify your API key right now. Please retry shortly.
            error_code: auth_unavailable
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key for authentication

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.