# Submit an email template version

`POST /v1/email/templates/{template_ref}/versions/{version_id}/submit`

Submits the template's draft as a new immutable, numbered version and
makes it the live version used by sends. The draft remains editable.

Every language the draft has must have a subject and a body, and the
default language must be present. Submission is all or nothing: an
incomplete language rejects the request and the response reports every
language error. A submit freezes every language the draft carries, so it
cannot name a subset. The draft does not have to hold every language you
plan to support: add more in a later version.

Set `validate_only: true` to run the checks without creating a version.
Set `expected_revision` to reject a concurrent edit. An unchanged draft is
rejected. Submission is synchronous, and a returned `version` is live.

## Code samples

### CLI

```sh
bird email templates versions submit <template-ref> <version-id> \
  --expected-revision 3
```

### cURL

```sh
curl -X POST "https://us1.platform.bird.com/v1/email/templates/{template_ref}/versions/{version_id}/submit" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "expected_revision": 3
  }'
```

## Example response `200`

```json
{
  "errors": [
    {
      "language": "pt-BR",
      "field": "subject",
      "code": "E04051",
      "message": "A subject is required to submit."
    }
  ],
  "version": {
    "id": "emv_01krdgeqcxet5s7t44vh8rt9mg",
    "template_id": "emt_01krdgeqcxet5s7t44vh8rt9mg",
    "status": "published",
    "variables": [
      {
        "type": "code"
      }
    ],
    "default_language": "pt-BR",
    "updated_by": {
      "id": "usr_01krdgeqcxet5s7t44vh8rt9mg",
      "type": "user"
    }
  },
  "template_ref": "emt_01krdgeqcxet5s7t44vh8rt9mg",
  "version_id": "emv_01krdgeqcxet5s7t44vh8rt9mg",
  "compatibility_severity": "warning",
  "compatibility": [
    {
      "rule_id": "css_display_flex_grid",
      "severity": "warning",
      "language": "pt-BR",
      "field": "html",
      "message": "`display: flex` and `grid` don't work in Outlook (Windows/Windows Mail).",
      "fix": "Use tables for layout.",
      "partial": "Partial support for `display: flex` and `grid` in Gmail (iOS/Android).",
      "line": 14,
      "column": 6,
      "match": "display: flex",
      "unsupported_clients": [
        {
          "family": "outlook",
          "platforms": [
            "windows",
            "windows_mail"
          ]
        }
      ],
      "partial_clients": [
        {
          "family": "outlook",
          "platforms": [
            "windows",
            "windows_mail"
          ]
        }
      ]
    }
  ]
}
```

## Path parameters

- `template_ref` (string): The template's id (`emt_…`) or slug. Submit freezes a draft, and a built-in `system` template has no draft, so its `bird_` slug returns `404` `not_found_error` here.
- `version_id` (string)

## Request body

- `validate_only` (boolean): Check the draft without actually submitting it. Every language gets checked and every problem gets reported back to you, but nothing is frozen and no new version gets created. Give a validation run its own `Idempotency-Key`, separate from the real submit that follows it. You can also send no key. The validation request and real submit have different bodies, so using the same key for both is rejected as key reuse.
- `expected_revision` (integer): The draft revision you last read (from the template's `revision` field). A stale value returns a conflict so you can reload and retry.
- `languages` (array of string): Languages to process when submitting an already-published version. Email templates accept submissions only for drafts, so setting this field for an email template is rejected.

## Response body

- `valid` (boolean, required): Whether the version passed every check.
- `errors` (array of object, required): Every problem found across the draft's languages. Empty when `valid` is `true`.
- `errors.language` (nullable string): The language this problem is about. Null when the problem is about the whole version rather than one language, for example an empty draft, or a default language the draft does not have.
- `errors.field` (nullable string): Which field within that language has the problem, such as `subject` or `html`. Null when the problem is not about one particular field.
- `errors.code` (string, required): The error code a real submit would fail with. Look it up in the error catalog to see what it means and what to do about it.
- `errors.message` (string, required): What is wrong, worded so you can show it directly to whoever is authoring the template.
- `version` (nullable object): The version this submit created, or null when it was only a validation run and nothing got frozen. As soon as this is not null, sends already use that version. No further action is required to make it live.
- `version.id` (string, required): Template version ID.
- `version.template_id` (string, required): The template this version belongs to.
- `version.version_number` (nullable integer): Sequential published-version number (1, 2, 3…). Null while the version is a draft.
- `version.status` (string, required)

  Whether this version is still being edited or has been published. It records
  the version's publication history: a version that a later one replaced stays
  `published`. The template's `live_version_id` names the version a send
  resolves to now.

  `archived` is reserved and no version carries it yet. Version retirement will
  produce it, so it is declared here ahead of that feature: a client written
  against this list today keeps working when the first archived version arrives,
  rather than the value's arrival being a breaking change.

  Possible values: `draft`, `published`, `archived`
- `version.revision` (integer, required): The version's revision counter.
- `version.variables` (array of object, required)

  Every variable this version's content uses. You supply a value for each of them when you send.

  The list combines all the languages, because languages do not have to use the same variables: if the English body uses `discount_code` and the French body uses `shipping_date`, both appear here. Send a value for every variable in the list rather than only the ones you expect the language you are sending to use. A language that does not use a variable ignores the value you sent for it, and a variable the sent language does use but you left out is rejected with a `422` naming it.

  Variables under the reserved `bird.` namespace are not listed here. We fill those in ourselves from the recipient's contact record.
- `version.variables.key` (string, required): The key this slot is filled by. On email and SMS it is the key you set in the send's `parameters` object. On WhatsApp it is the `name` you repeat on the matching parameter inside `components`, or, for a template whose placeholders are positional, the position itself as `1`, `2` and so on.
- `version.variables.type` (string, required)

  The value type this slot accepts. SMS templates use the typed slots (`code`, `amount` and the rest), each of which rejects a value that does not match its `constraint`. Email and WhatsApp templates use `text`, which accepts any value. Open enum: treat an unrecognized value as a future type rather than an error.

  Possible values (may grow over time): `code`, `ttl`, `count`, `ref`, `date`, `date_time`, `amount`, `currency`, `text`
- `version.variables.required` (boolean, required): Whether the send must supply this variable. Omitting a required value returns `422` on email, SMS, and WhatsApp sends.
- `version.variables.constraint` (string, required): A plain-language description of what values this variable accepts.
- `version.variables.sensitive` (boolean): Whether this slot's value is kept out of durable storage. A sensitive slot's rendered value never appears in message content read back through the API: a stand-in placeholder is stored instead.
- `version.languages` (object, required): The content this version holds, keyed by language tag in BCP-47 form such as `en` or `pt-BR`. Publishing freezes every language together, so a version shows exactly what it would send in each of them. On a published version this is the send content.
- `version.default_language` (string, required): The language this version treats as its default: the one a send uses when it names none, and the last resort when a requested language is not available.
- `version.created_at` (string, required): When this version was created.
- `version.published_at` (nullable string): When this version was published, or null if it has not been published.
- `version.updated_by` (nullable object): Who last saved this version: a member's own session, an OAuth token delegated from one, or a workspace API key. Publishing freezes a version, so on a published one this is whoever published it. Null means no actor is on record: a built-in template, which is code-defined rather than stored, or a version last saved by an API key before this field existed. Every other version has one, even when its display_name could not be resolved (a member whose account is gone, say).
- `version.updated_by.id` (string, required): Actor identifier.
- `version.updated_by.type` (string, required)

  Who or what performed the action: `user` for a member's own session, `oauth_token` for a token issued to a caller on a member's behalf, `api_key` for a workspace API key, `system` for our own automation, `sso` for an organization's SSO connection, and `service_account` for a workspace's connected Integration acting with no member behind it. Open enum: new actor types may be added over time, so treat any unrecognized value as a future type rather than an error.

  Possible values (may grow over time): `user`, `api_key`, `oauth_token`, `system`, `sso`, `service_account`
- `version.updated_by.display_name` (nullable string): The label the actor is shown under: typically a member's name or email address, or the API key's name. Null when it could not be resolved.
- `template_ref` (string, required): The template this submit addressed.
- `version_id` (string, required): The draft this submit addressed: the one it froze, or on a validation run the one it checked and left as it was. The frozen version, when there is one, is `version`.
- `compatibility_severity` (string, required)

  The worst severity across every finding the response was computed from, which
  is the authoritative reading: a response that caps how many findings it lists
  still accounts here for the ones it left out. Each response's `compatibility`
  says which content it covered.

  - `problem`: at least one finding is a `problem`.
  - `warning`: every finding is a `warning`.
  - `none`: there are no findings.

  Possible values: `problem`, `warning`, `none`
- `compatibility` (array of object, required): What the draft's HTML uses that mail clients remove, ignore, or render inconsistently, across every language, in alphabetical order of language tag and then the order the patterns appear. Empty when nothing is worth reporting. Advisory, and separate from `errors`: a finding never fails a submit, so the version froze either way. Each finding names the `language` it is in, and its line and column count in that language's HTML, which the language read returns as `content.html`. At most 200 findings come back, the first 200 in that order, so a draft that reaches the cap can omit a later language's findings entirely rather than trimming each language: read a language's own findings from its read or its write. `compatibility_severity` is derived from every finding the draft produced, including any beyond those 200.
- `compatibility.rule_id` (string, required)

  The rule that produced this finding.

  Possible values (may grow over time): `html_script`, `html_event_handlers`, `html_embedded_content`, `html_linked_stylesheet`, `css_at_import`, `html_form`, `html_svg`, `html_media`, `css_display_flex_grid`, `css_position_fixed_sticky`, `css_variables_no_fallback`, `css_viewport_units`, `html_button`, `css_math_functions`, `css_modern_color`, `html_web_page_markup`
- `compatibility.severity` (string, required)

  What a finding costs you.

  - `problem`: the pattern does nothing at all. The client removes the markup, never loads the stylesheet carrying it, or will not operate the control. Where a finding names clients, that is what happens in those clients.
  - `warning`: it does something, but not what you wrote.

  Neither one refuses a save, a submit, or a send.

  Possible values: `problem`, `warning`
- `compatibility.language` (nullable string, required): Which language's content this finding is in. Null when the call covered a single language.
- `compatibility.field` (string, required): Which field of that language the finding is in. Always `html`; the subject and the plain-text body are not checked.
- `compatibility.message` (string, required): What is wrong and which clients it affects, worded to show to whoever is authoring the template. It covers the rule's whole category rather than the exact text that matched, so a rule covering `<video>` and `<audio>` names both whichever one is on the line. Show `fix` and then `partial` after it.
- `compatibility.fix` (nullable string, required): What to use instead. Null when there is no drop-in alternative and the fix is a restructure.
- `compatibility.partial` (nullable string, required): Which clients support the feature only partly. Null when no client's support is partial.
- `compatibility.line` (integer, required): The 1-based line the pattern is on, in the HTML the containing response's `compatibility` says it covered.
- `compatibility.column` (integer, required): The 1-based column the pattern starts at, on that line.
- `compatibility.match` (string, required): The source text that matched, starting at `line` and `column`: the smallest span that identifies what is wrong. Never the enclosing line. For a finding on a whole element, the span runs from the opening tag through the close tag, because the client drops the element's content along with its markup. Cut at 256 characters, so an element holding a long body is quoted from its start rather than in full.
- `compatibility.unsupported_clients` (array of object, required): Every client family that does not support the feature at all, in alphabetical order by each entry's `family`. Empty on a finding whose `message`, `fix`, and `partial` name no client. `message` names at most four families; this names all of them.
- `compatibility.unsupported_clients.family` (string, required)

  Which mail client a finding applies to. A finding's `message` names at most
  Apple Mail, Gmail, Outlook, and Yahoo; its `unsupported_clients` and
  `partial_clients` name every client affected.

  - `gmail`: Gmail
  - `outlook`: Outlook
  - `yahoo`: Yahoo
  - `apple_mail`: Apple Mail
  - `aol`: AOL
  - `thunderbird`: Mozilla Thunderbird
  - `samsung_email`: Samsung Email
  - `sfr`: SFR
  - `orange`: Orange
  - `protonmail`: ProtonMail
  - `hey`: HEY
  - `mail_ru`: Mail.ru
  - `fastmail`: Fastmail
  - `laposte`: LaPoste.net
  - `gmx`: GMX
  - `web_de`: WEB.DE
  - `ionos_1and1`: 1&1
  - `wp_pl`: WP.pl

  Possible values (may grow over time): `gmail`, `outlook`, `yahoo`, `apple_mail`, `aol`, `thunderbird`, `samsung_email`, `sfr`, `orange`, `protonmail`, `hey`, `mail_ru`, `fastmail`, `laposte`, `gmx`, `web_de`, `ionos_1and1`, `wp_pl`
- `compatibility.unsupported_clients.platforms` (array of string, required): Which of the family's platforms this applies to, in alphabetical order.
- `compatibility.partial_clients` (array of object, required): Every client family that renders something other than what you wrote, in alphabetical order by each entry's `family`. Empty when no client's support is partial, which is also when `partial` is null. `partial` names at most four families; this names all of them.
- `compatibility.partial_clients.family` (string, required)

  Which mail client a finding applies to. A finding's `message` names at most
  Apple Mail, Gmail, Outlook, and Yahoo; its `unsupported_clients` and
  `partial_clients` name every client affected.

  - `gmail`: Gmail
  - `outlook`: Outlook
  - `yahoo`: Yahoo
  - `apple_mail`: Apple Mail
  - `aol`: AOL
  - `thunderbird`: Mozilla Thunderbird
  - `samsung_email`: Samsung Email
  - `sfr`: SFR
  - `orange`: Orange
  - `protonmail`: ProtonMail
  - `hey`: HEY
  - `mail_ru`: Mail.ru
  - `fastmail`: Fastmail
  - `laposte`: LaPoste.net
  - `gmx`: GMX
  - `web_de`: WEB.DE
  - `ionos_1and1`: 1&1
  - `wp_pl`: WP.pl

  Possible values (may grow over time): `gmail`, `outlook`, `yahoo`, `apple_mail`, `aol`, `thunderbird`, `samsung_email`, `sfr`, `orange`, `protonmail`, `hey`, `mail_ru`, `fastmail`, `laposte`, `gmx`, `web_de`, `ionos_1and1`, `wp_pl`
- `compatibility.partial_clients.platforms` (array of string, required): Which of the family's platforms this applies to, in alphabetical order.

## Related resources

- [How to build an email template](/learn/email/how-to-build-an-email-template) (video)
- [Email templates](/products/email/templates) (product)
- [Build your first integration](/learn/paths/integration) (course)
- [Email templates](/docs/guides/email/templates) (docs)

[Get an implementation brief](/learn/workspace?topic=email-templates)
