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

> ## Agent Instructions
> AhaSend stores all message data in the EU. See /facts.
> The API base URL is https://api.ahasend.com. Send the API key in the Authorization: Bearer header. See /api-reference/authentication.
> Use send.ahasend.com for SMTP. Ports 25, 587 and 2525 use STARTTLS. Port 465 is not supported. See /smtp.
> For send-only integrations, create an API Key v2 under Credentials → Add and scope it to messages:send:{your-domain}. See /send-api/credentials and /api-reference/scopes.

# Get a Published Transactional Template via the API

> Retrieve the published copy of an AhaSend transactional template, the one sends use, with its content, sender and the variables a send must give.

The AhaSend API returns the published copy of a template. [Create Template Message](https://ahasend.com/docs/api-reference/messages/create-template-message.md) sends this copy, and each recipient must give a value for every required name in `variables`. Each variable's `kind` says what to send for it; see [kinds of variables](https://ahasend.com/docs/send-api/templates.md#kinds-of-variables). Read this copy to build a send. Access requires `templates:read`.

When `has_draft` is `true`, the template has changes that are not published yet. [Get Template](https://ahasend.com/docs/api-reference/templates/get-template.md) returns them. A template that was never published has `content: null`.

Campaign templates have their own endpoints, and the ID of a campaign template answers `404` here.

## Related Guides

* [API authentication](https://ahasend.com/docs/api-reference/authentication.md), [Permission scopes](https://ahasend.com/docs/api-reference/scopes.md), [Get Template](https://ahasend.com/docs/api-reference/templates/get-template.md).
* [API errors](https://ahasend.com/docs/api-reference/errors.md), [API rate limits](https://ahasend.com/docs/api-reference/rate-limits.md).


## OpenAPI

````yaml openapi.yaml GET /v2/accounts/{account_id}/templates/{template_id}/published
openapi: 3.1.0
info:
  title: AhaSend API v2
  description: >
    The AhaSend API v2 allows you to send transactional emails, manage domains,
    webhooks, routes, API keys, and view statistics.


    ## Authentication

    All API requests must be authenticated using a Bearer token in the
    Authorization header:

    ```

    Authorization: Bearer aha-sk-64-CHARACTER-RANDOM-STRING

    ```


    ## Scopes

    API keys have specific scopes that control access to different resources and
    actions:


    ### Message Scopes

    - `messages:send:all` - Send messages from any domain in the account

    - `messages:send:{domain}` - Send messages from a specific domain

    - `messages:cancel:all` - Cancel messages from any domain

    - `messages:cancel:{domain}` - Cancel messages from a specific domain

    - `messages:read:all` - Read messages from any domain

    - `messages:read:{domain}` - Read messages from a specific domain


    ### Domain Scopes

    - `domains:read` - Read all domains

    - `domains:write` - Create and update domains

    - `domains:delete:all` - Delete any domain

    - `domains:delete:{domain}` - Delete a specific domain


    ### Account Scopes

    - `accounts:read` - Read account information

    - `accounts:write` - Update account settings

    - `accounts:billing` - Access billing information

    - `accounts:members:read` - Read account members

    - `accounts:members:add` - Add account members

    - `accounts:members:update` - Update account members

    - `accounts:members:remove` - Remove account members


    ### Webhook Scopes

    - `webhooks:read:all` - Read all webhooks

    - `webhooks:read:{domain}` - Read webhooks for a specific domain

    - `webhooks:write:all` - Create and update webhooks

    - `webhooks:write:{domain}` - Create and update webhooks for a specific
    domain

    - `webhooks:delete:all` - Delete any webhook

    - `webhooks:delete:{domain}` - Delete webhooks for a specific domain


    ### Route Scopes

    - `routes:read:all` - Read all routes

    - `routes:read:{domain}` - Read routes for a specific domain

    - `routes:write:all` - Create and update routes

    - `routes:write:{domain}` - Create and update routes for a specific domain

    - `routes:delete:all` - Delete any route

    - `routes:delete:{domain}` - Delete routes for a specific domain


    ### Suppression Scopes

    - `suppressions:read` - Read suppressions

    - `suppressions:write` - Create suppressions

    - `suppressions:delete` - Delete suppressions

    - `suppressions:wipe` - Delete the account's suppressions except protected
    ones (dangerous)


    ### Template Scopes

    - `templates:read` - Read transactional templates, their drafts and their
    published versions, and campaign templates

    - `templates:write` - Create and update transactional templates, and
    publish, discard and restore their drafts; create and update campaign
    templates

    - `templates:delete` - Delete transactional templates and campaign templates


    ### Campaign Scopes

    - `campaigns:read` - Read campaigns, their stats and their recipients

    - `campaigns:write` - Create, update and duplicate campaigns

    - `campaigns:delete` - Delete draft campaigns

    - `campaigns:send` - Send, unschedule and test-send campaigns


    ### Contact Scopes

    - `contacts:read` - Read contacts

    - `contacts:write` - Create and update contacts

    - `contacts:delete` - Delete contacts


    ### Contact lifecycle and attributes

    Contacts are account-global. A contact created through this API joins no

    list on its own; campaigns target lists, so add it to one with the Lists

    endpoints before a campaign can reach it.

    Contact subscription state and suppression records are independent:

    creating, updating, unsubscribing, resubscribing, or deleting a contact

    never creates, changes, or removes complaint, permanent, or protected

    suppressions. Prefer unsubscribe over delete when the goal is only to stop

    marketing mail, because unsubscribe is reversible and preserves contact

    history.


    Writable attribute keys and types come from the account's contact-attribute

    definitions managed in the dashboard; this API does not expose property

    definition CRUD. Writes accept only canonical values for those definitions,

    while reads remain legacy-safe and can return any recursively nested JSON

    value previously stored.


    An email used as `{id_or_email}` is a raw SDK argument and must be

    percent-encoded exactly once as a URL path segment by an HTTP client. For

    example, do not pre-encode `user+tag@example.com` before passing it to an

    SDK that encodes path parameters.


    ### List Scopes

    - `lists:read` - Read lists and list membership

    - `lists:write` - Create and update lists, and add or remove their contacts

    - `lists:delete` - Delete lists


    ### Lists and membership

    A list is a static, private, single opt-in container of contacts, and it is

    what a campaign targets. A membership carries its own `subscription_status`

    (`unconfirmed`, `confirmed`, `unsubscribed`, or `complained`), which is

    per-list consent and is independent of the contact's account-wide

    `unsubscribed` flag. A membership created without a status named is

    `confirmed`, because that is what a campaign reaches. `complained` is the

    mailbox provider's verdict, never writable, and a membership holding it is

    frozen.


    Deleting a list keeps its memberships, so an unsubscribe survives it.

    Removing a contact from a list deletes the membership row and discards that

    record with it, which is why unsubscribing is the reversible way to stop

    marketing mail on one list. Expanding a list's members with

    `include_contacts=true` requires `contacts:read` in addition to

    `lists:read`.


    ### Segment Scopes

    - `segments:read` - Read segments, their members and a contact's segments,
    and preview a definition

    - `segments:write` - Create and update segments

    - `segments:delete` - Delete segments


    ### Segments

    A segment is a saved set of rules over the account's contacts: their

    fields, their list memberships, and the campaigns they received, opened

    or clicked. AhaSend keeps its members up to date as that data changes,

    usually within a minute; rules about campaigns follow within an hour of a

    send, and relative times such as `30 days ago` are read again on a

    schedule. Membership is the rule match only, whatever the contact's

    consent: a campaign to a segment reaches the members who are enabled, not

    unsubscribed, not found invalid, and either confirmed on at least one list

    or on no list at all. A contact who unsubscribed from or complained on

    every list they were on, or has not confirmed a double opt-in, is not

    reached. A contact who unsubscribes from a segment send, or reports it as

    spam, gets no more marketing email from the account.


    ### SMTP Credentials Scopes

    - `smtp-credentials:read:all` - Read all SMTP credentials

    - `smtp-credentials:read:{domain}` - Read SMTP credentials for a specific
    domain

    - `smtp-credentials:write:all` - Create SMTP credentials

    - `smtp-credentials:write:{domain}` - Create SMTP credentials for a specific
    domain

    - `smtp-credentials:delete:all` - Delete any SMTP credentials

    - `smtp-credentials:delete:{domain}` - Delete SMTP credentials for a
    specific domain


    ### Statistics Scopes

    - `statistics-transactional:read:all` - Read all transactional statistics

    - `statistics-transactional:read:{domain}` - Read transactional statistics
    for a specific domain


    ### API Key Scopes

    - `api-keys:read` - Read API keys

    - `api-keys:write` - Create and update API keys

    - `api-keys:delete` - Delete API keys


    ### Sub-Account Scopes

    - `sub-accounts:read` - List and read sub accounts under the parent

    - `sub-accounts:write` - Create and update sub accounts

    - `sub-accounts:delete` - Soft-delete sub accounts

    - `sub-accounts:suspend` - Pause and resume sub accounts, and unpause their
    domains

    - `sub-accounts:usage` - Read per-sub-account usage and allocated cost

    - `sub-account-api-keys:read` - List and read API keys owned by sub accounts

    - `sub-account-api-keys:write` - Create and update API keys owned by sub
    accounts

    - `sub-account-api-keys:delete` - Delete API keys owned by sub accounts


    ## Rate Limiting

    - General API endpoints: 100 requests per second, 200 burst

    - Statistics endpoints: 1 request per second, 1 burst


    A request over the limit gets HTTP 429 with a `Retry-After` header: the

    number of seconds to wait before retrying.


    ## Pagination

    List endpoints use cursor-based pagination with the following parameters:

    - `limit`: Maximum number of items to return (default: 100, max: 100)

    - `after`: Pass `pagination.next_cursor` to fetch the next page

    - `before`: Pass `pagination.previous_cursor` to fetch the previous page


    `after` and `before` are mutually exclusive. A legacy `cursor` query may be

    accepted by parts of the server for backwards compatibility, but it is not

    part of the v2 public contract.


    ## Time Formats

    All timestamps must be in RFC3339 format, e.g., `2023-12-25T10:30:00Z`


    ## Idempotency

    The operations that explicitly declare the optional `Idempotency-Key`

    header support idempotency. Other POST operations do not gain idempotency

    implicitly. When the header is provided:

    - A fresh execution does not return `Idempotent-Replayed`.

    - A completed stored response is replayed with its original status and body
      and `Idempotent-Replayed: true`.
    - An unexpired in-progress execution returns HTTP 409 with
      `Idempotent-Replayed: false` and a positive integer `Retry-After` header.
    - Reusing a key with a different HTTP method, resolved request path, or
      request body returns HTTP 422. That response has neither
      `Idempotent-Replayed` nor `Retry-After`.
    - For ordinary idempotent operations, deterministic 2xx and 4xx outcomes
      produced after the idempotency middleware are stored and replayed.
      Authentication and IP-allow-list failures happen before idempotency and
      are therefore not stored.
    - The two secret-bearing API-key create operations use manual encrypted
      completion. They store and replay a successful 201, including the same
      one-time `secret_key`; a controller-produced 4xx is not stored and leaves
      the key in progress until its execution lease expires.
    - Create Segment and Test Campaign store and replay a 2xx only. A 4xx is
      not stored: it releases the key, so a retry with the same key runs
      again.
    - Server errors (5xx), handler failures, and panics are not stored. The key
      is released so retrying the same request with the same key re-executes it.
      The v2 API does not use an HTTP 412 failed-key tombstone.
    - Idempotency keys for non-secret responses expire after 24 hours.

    - Encrypted API-key creation replay responses expire after 5 minutes.


    ## IP Allow Lists

    Each API key can carry an `ip_allow_list`: a set of source IPs that restrict
    where the key may authenticate from.

    - Entries are CIDR blocks (e.g. `203.0.113.0/24`) or bare IPv4/IPv6
    addresses (stored as a `/32` or `/128`). They are canonicalized and
    de-duplicated; the allow-all prefixes `0.0.0.0/0` and `::/0` are rejected,
    and at most 100 entries are allowed after de-duplication (a longer list is
    rejected with HTTP 400).

    - An empty list (the default) places no restriction.

    - When the list is non-empty, any authenticated request whose client IP is
    not covered by an entry is rejected with HTTP 403 on every v2 endpoint,
    regardless of the key's scopes.

    - Manage the list with the `ip_allow_list` field on the API-key create and
    update endpoints (including the sub-account API-key endpoints).

    - When a key updates its own `ip_allow_list` to a value that excludes the
    caller's current IP, the update is rejected with HTTP 409 to prevent
    self-lockout. Updating a sub-account key from a parent key has no such
    guard.
  version: 2.0.0
  contact:
    email: support@ahasend.com
  license:
    name: MIT
    identifier: MIT
servers:
  - url: https://api.ahasend.com
    description: Production server
security:
  - BearerAuth: []
tags:
  - name: Utility
    description: Utility endpoints for health checks and diagnostics
  - name: API Keys
    description: Manage API keys for authentication and access control
  - name: Domains
    description: Manage sending domains
  - name: Messages
    description: Send and manage transactional messages
  - name: Accounts
    description: Manage account settings and members
  - name: Contacts
    description: Read, create, update, batch upsert, and delete account-global contacts
  - name: Templates
    description: >-
      Create, change, publish and delete the account's transactional templates,
      and read them with their drafts, their published versions and the
      variables a send must supply
  - name: Campaign Templates
    description: >-
      Create, change, read and delete the account's campaign templates, the
      designs a campaign copies when it is made from one
  - name: Campaigns
    description: >-
      Create, change, read, duplicate, delete, test and send the account's
      marketing campaigns, and read their stats and recipients
  - name: Lists
    description: Manage contact lists and the contacts on them
  - name: Segments
    description: >-
      Manage segments, the saved rules that keep a set of contacts up to date,
      and read their members
  - name: Suppressions
    description: Manage email suppressions
  - name: Routes
    description: Manage inbound email routing
  - name: Webhooks
    description: Manage webhook notifications
  - name: SMTP Credentials
    description: Manage SMTP authentication credentials
  - name: Statistics
    description: Access transactional email statistics
  - name: Message Events
    description: Webhooks for outbound message delivery events
  - name: Suppression Events
    description: Webhooks for suppression list changes
  - name: Domain Events
    description: Webhooks for domain configuration issues
  - name: Route Events
    description: Webhooks for inbound email routing
paths:
  /v2/accounts/{account_id}/templates/{template_id}/published:
    get:
      tags:
        - Templates
      summary: Get Published Template
      description: |
        Returns a transactional template's published copy: the one a send
        uses, with its content and the variable list a send has to satisfy.
        Read it to build a send. Get Template returns the working copy
        instead, which is the draft while one exists. `has_draft` says
        whether changes are waiting to be published. A campaign design's ID
        is not addressable here and answers 404.
      operationId: getPublishedTemplate
      parameters:
        - name: account_id
          in: path
          required: true
          description: Account ID
          schema:
            type: string
            format: uuid
        - name: template_id
          in: path
          required: true
          description: Template ID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The template's published copy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Template'
        '400':
          description: Invalid template_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Missing `templates:read` scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many requests
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth:
            - templates:read
      x-code-samples:
        - lang: go
          label: AhaSend Go SDK
          source: |
            package main

            import (
              "context"
              "fmt"
              "log"
              "os"

              "github.com/AhaSend/ahasend-go/api"
              "github.com/google/uuid"
            )

            func main() {
              client := api.NewAPIClient(api.WithAPIKey(os.Getenv("AHASEND_API_KEY")))
              accountID := uuid.MustParse(os.Getenv("AHASEND_ACCOUNT_ID"))
              ctx := context.Background()

              templateID := uuid.MustParse("11111111-1111-4111-8111-111111111111")

              // The published copy is the one a send uses, so build a send from its
              // variables. GetTemplate returns the draft instead while one exists.
              response, _, err := client.TemplatesAPI.GetPublishedTemplate(ctx, accountID, templateID)
              if err != nil {
                log.Fatalf("Error getting published template: %v", err)
              }
              fmt.Printf("Published subject: %s, draft waiting: %t\n", response.Subject, response.HasDraft)
              for _, variable := range response.Variables {
                fmt.Printf("%s required=%t kind=%s\n", variable.Name, variable.Required, variable.Kind)
              }
            }
        - lang: javascript
          label: Node.js 22+ (AhaSend SDK)
          source: >
            import { AhaSendClient } from "@ahasend/sdk";


            const client = AhaSendClient.fromEnv();

            const templateId = "00000000-0000-4000-8000-000000000009";

            // The published copy is the one a send uses; get() returns the
            draft while one exists.

            const template = await client.templates.getPublished(templateId);

            console.log("Published template found.", {
              subject: template.subject,
              hasDraft: template.has_draft,
              variables: template.variables.length,
            });
components:
  schemas:
    Template:
      type: object
      description: |
        A transactional template, as one of its copies. Get Template and the
        answer of every template write give its working copy: its draft while
        it has one, and its published copy otherwise. Get Published Template
        and List Templates give its published copy, the one a send uses.
        `has_draft` says whether a draft exists.
      required:
        - object
        - id
        - created_at
        - updated_at
        - name
        - subject
        - preheader
        - variables
        - from
        - reply_to
        - editor
        - has_draft
      properties:
        object:
          type: string
          enum:
            - template
          description: Object type identifier
        id:
          type: string
          format: uuid
          description: >-
            Pass as `template_id` to [Create Template
            Message](https://ahasend.com/docs/api-reference/messages/create-template-message)
            to send this template
        created_at:
          type: string
          format: date-time
          description: When the template was created
        updated_at:
          type: string
          format: date-time
          description: When the template's name or this copy last changed
        name:
          type: string
          description: Template name, as it appears in the dashboard
        subject:
          type: string
          description: >-
            This copy's subject, set on the template page. Empty until one is
            set. A send uses the published subject, unless it names its own
            `subject`.
        preheader:
          type: string
          description: >-
            This copy's preview text, which a send of the published copy injects
            into the delivered HTML when it is set
        variables:
          type: array
          items:
            $ref: '#/components/schemas/TemplateVariable'
          description: |
            Every variable this copy's design uses. A send of this template
            uses the published copy's variables, which Get Published Template
            returns, and takes the values only from each recipient's
            `substitutions`. Every recipient must give a value for each
            required variable; if one does not, the request is rejected and no
            message is sent. Each variable's `kind` says what shape of value to
            send.
        from:
          oneOf:
            - $ref: '#/components/schemas/Address'
            - type: 'null'
          description: >-
            This copy's default sender. A send that names no `from` uses the
            published copy's. Null when the copy has none; a send of a published
            copy with none must name one.
        reply_to:
          oneOf:
            - $ref: '#/components/schemas/Address'
            - type: 'null'
          description: >-
            This copy's default reply-to address. A send that sets no reply-to,
            also one that names its own `from`, uses the published copy's. Null
            when the copy has none. It has the shape of a send request's
            `reply_to`; its `name` is always empty.
        editor:
          $ref: '#/components/schemas/TemplateEditor'
        has_draft:
          type: boolean
          description: >-
            Whether the template has a draft, changes saved in the dashboard or
            through the API that are not published yet. Get Template returns the
            draft while one exists.
        content:
          oneOf:
            - $ref: '#/components/schemas/TemplateContent'
            - type: 'null'
          description: >-
            This copy's content. Null when it has no body, as for the published
            copy of a template that was never published. List Templates leaves
            this field out; every other operation that returns a Template sets
            it.
        markup_warnings:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/MarkupProblem'
          description: >-
            Warnings about the AhaML in `content.markup`, up to the first 100;
            empty when there are none. Present only in the answer to a write
            that carried `content.markup`.
    ErrorResponse:
      type: object
      description: |
        Human-readable API error. The server currently sends no stable machine
        error code. In particular, clients must not parse `message` to
        distinguish IP-allow-list, scope, ownership, plan, self-lockout,
        suppression-duplicate, or other errors that share an HTTP status.
      additionalProperties: false
      required:
        - message
      properties:
        message:
          type: string
          description: Error description
      example:
        message: Error message
    TemplateVariable:
      type: object
      required:
        - name
        - required
        - kind
      properties:
        name:
          type: string
          description: Variable name as the design writes it, for example `first_name`
        required:
          type: boolean
          description: |
            Whether each recipient of a send has to supply a value for this
            variable. A variable the design wraps in a fallback is not required.

            `email` and `unsubscribe_url` are never required: the send
            supplies them for each recipient and replaces a value the
            recipient gives. `email` is the recipient's address.
            `unsubscribe_url` is a link made for the recipient, and the
            message then carries `List-Unsubscribe` and
            `List-Unsubscribe-Post` headers for it; a `List-Unsubscribe` or
            `List-Unsubscribe-Post` in the request's `headers` is dropped.
        kind:
          type: string
          enum:
            - value
            - condition
            - list
          description: |
            What kind of value to send for this variable.

            - `value`: text or a number that appears in the email, such as a
              first name.
            - `condition`: used only to decide whether a part of the email is
              shown, for example "show this section if `vip` is set". It never
              appears in the email. To make it set, send a value that is not
              empty, such as `true` or `"yes"`. To make it not set, leave it
              out or send `""`, `false`, `0`, `null`, `[]` or `{}`. The strings
              `"false"` and `"0"` are not empty, so they make it set. A rule
              can also compare it, for example "show this section if `plan`
              equals `gold`"; then send a value that meets the rule to show
              the section.
            - `list`: an array. A section of the email is repeated once for
              each element, for example one row per product in an order.
              `item_fields` names the fields each element should have.
        item_fields:
          type: array
          items:
            type: string
          description: |
            Only for a `list` variable. The fields the repeated section uses
            from each element. Send each element as an object with these
            fields. A field an element does not have is taken from the
            variable of the same name in `substitutions`, if there is one.

            For example, if `items` has `item_fields` `["name", "price"]`,
            send:

            ```json
            {
              "items": [
                {"name": "Linen shirt", "price": "€49"},
                {"name": "Canvas tote", "price": "€19"}
              ]
            }
            ```

            A section that shows each element as it is, such as a list of
            tags, uses the field `value`. If `item_fields` is `["value"]`, you
            can send plain values, such as `["new", "sale"]`. Empty when the
            section uses nothing from its elements: send one element, of any
            kind, for each time the section repeats.
    Address:
      type: object
      required:
        - email
      properties:
        email:
          type: string
          format: email
          description: >-
            The email address. A `from` address must be on a domain defined in
            your account with valid DNS records.
        name:
          type: string
          description: >-
            The display name shown with the address. It can hold any Unicode
            text; the API encodes it for the header. On a send with
            substitutions or a template, a name with non-ASCII text can use `{{
            name }}` references only; other template syntax in it is refused.
      example:
        email: noreply@example.com
        name: Example Corp
    TemplateEditor:
      type: string
      enum:
        - advanced
        - simple
        - html
      description: |
        The editor the template is designed with. It never changes after the
        template is created.

        - `advanced`: designed in MJML, with the dashboard's advanced editor.
        - `simple`: designed with the dashboard's simple editor. Its HTML is
          made from the design and can be read but not changed through the API.
          A read or write answer of one item gives the design as AhaSend
          Markup (AhaML) in `content.markup`; a list does not. A write changes
          the design with `content.markup`.
        - `html`: an HTML template, uploaded or written by hand.
    TemplateContent:
      type: object
      required:
        - text_is_custom
      description: |
        A template's content. A key that does not apply to the template's
        editor is left out, and so are `mjml`, `html` and `text` when they are
        empty.

        An answer that holds one `simple` design also gives that design as
        AhaSend Markup (AhaML) in `markup`. When AhaML cannot be made, the
        answer still succeeds: `markup` is null and `markup_error` says why.
        List operations give no content, so they give no `markup`.
      properties:
        mjml:
          type: string
          description: The MJML source. Only for `advanced` templates.
        html:
          type: string
          description: >-
            The HTML of an `html` template, or the HTML made from the design of
            a `simple` template.
        text:
          type: string
          description: The plain text version of the email.
        text_is_custom:
          type: boolean
          description: >-
            Whether the text was written rather than made from the HTML. A text
            made from the HTML is made again whenever the design changes.
        markup:
          type:
            - string
            - 'null'
          description: >-
            The design as AhaSend Markup (AhaML). Only for `simple` designs, and
            then always present. Null when AhaML cannot be made; `markup_error`
            then says why.
          example: |
            <Email version="1">
              <Heading level="3">Welcome</Heading>
              <Paragraph>Thanks for signing up.</Paragraph>
            </Email>
        markup_error:
          type: object
          required:
            - code
            - message
          description: >-
            Why `markup` is null. Only present when it is. In the answer to a
            write, it says only that the answer holds no AhaML; the write itself
            succeeded. A write answer replayed for its `Idempotency-Key` gives
            the `markup_error` of the first answer; read the object again to get
            its AhaML.
          properties:
            code:
              type: string
              enum:
                - no_design
                - html_block
                - unmapped_block
                - bad_stored_value
                - renderer_unavailable
                - rate_limited
              description: |
                - `no_design`: the design has no blocks yet.
                - `html_block`: the design holds an HTML block, which AhaML
                  cannot show. Remove the block in the dashboard to read and
                  write the design as AhaML.
                - `unmapped_block`: the design holds a block or a mark that
                  AhaML has no tag for. The message names its type.
                - `bad_stored_value`: the design holds a value that AhaML
                  cannot write. The message names the block and the attribute.
                - `renderer_unavailable`: AhaML cannot be made now. Read the
                  design again later.
                - `rate_limited`: the account reads and writes AhaML more
                  often than its limit allows. Read the design again in a
                  second. A caller that does not use AhaML is never slowed
                  by this limit.
            message:
              type: string
              description: What went wrong, in words for a person.
          example:
            code: html_block
            message: >-
              The design holds an HTML block, which AhaML cannot show; remove
              the block in the dashboard to read and write the design as AhaML
    MarkupProblem:
      type: object
      description: >-
        One problem of AhaSend Markup (AhaML) that a write refused, or one
        warning about AhaML a write stored.
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: |
            What is wrong, as a code that stays the same. The AhaML reference
            lists every code. A warning's code is `jinja_in_text`: text holds
            `{{`, which stays text; use a `<Var>` for a variable.
        message:
          type: string
          description: What is wrong, in words for a person.
        line:
          type: integer
          minimum: 1
          description: >-
            The line where the problem starts, counted from 1. Left out when the
            problem has no place in the AhaML, as for a size limit.
        column:
          type: integer
          minimum: 1
          description: >-
            The column where the problem starts, counted from 1 in Unicode
            characters. Left out with `line`.
        path:
          type: string
          description: >-
            The element of the problem, each tag with its position among its
            siblings of the same tag, such as `Email > Section[2] >
            Paragraph[1]`. Left out with `line`.
        hint:
          type: string
          description: How to fix the problem, when a fix is known. Warnings have none.
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: aha-sk-64-CHARACTER-RANDOM-STRING
      description: |
        API key for authentication. Non-empty Security Requirement values are
        AhaSend API-key roles. Roles listed within one requirement object are
        jointly required; separate requirement objects are alternatives.

````

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