---
title: "Create a template from a draft envelope"
method: POST
path: "/templates/from-envelope"
tags: ["Templates"]
---

# Create a template from a draft envelope

`POST /templates/from-envelope`

## Request

- Base URL: `https://inkseal.net/api/v1`
- URL: `https://inkseal.net/api/v1/templates/from-envelope`
- Auth: HTTP bearer

Turns a draft envelope into a reusable template. Its signers become roles, its fields become template fields keyed by name, and its subject, message, signing order, and email verification become the template's defaults, so the request carries only which draft and what to call the result.

**The draft is consumed.** It is deleted and its document is re-labelled as a template's, which is why this costs `envelopes:read` as well as `templates:write`. Only a `draft` envelope can be converted; anything else, or a document already backing something, answers `409` `document_in_use`.

Send an `Idempotency-Key`. A retry without one cannot find the envelope the first attempt already consumed, and answers `404` instead of the template it made.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `envelopeId` string, required: Public id of the draft envelope to convert.
  - `name` string, required: What to call the template.
  - `description` string, nullable
  - `expectedVersion` integer: Optimistic lock on the draft's editor version. Omit it to convert the draft as it stands; pass one to have the conversion answer `409` `version_conflict` if the draft changed since you read it.

## Response `201`

The created template. The draft envelope no longer exists.

- object
  - `data` TemplateDetail, required
    - `id` string, required: Public template id. This is the `templateId` in the `/templates/{templateId}` routes and the `templateId` a template envelope is created from.
    - `name` string, required
    - `description` string, nullable, required
    - `document` object, required
      - `id` string, required
      - `filename` string, required
      - `pageCount` integer, required
      - `pageDimensions` object[], required
        - `width` number, required
        - `height` number, required
    - `signingOrder` 'parallel' | 'sequential', required
    - `defaultSubject` string, nullable, required
    - `defaultMessage` string, nullable, required
    - `defaultExpiresInDays` integer, required
    - `emailVerification` boolean, required
    - `version` integer, required
    - `complete` boolean, required
    - `archivedAt` string, date-time, nullable, required
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required
    - `roles` TemplateRole[], required
      - `key` string, required
      - `label` string, required
      - `order` integer, required
      - `id` string, required
    - `fields` TemplateField[], required
      - `name` string, required
      - `type` 'signature' | 'initials' | 'text' | 'date' | 'checkbox' | 'number' | 'email' | 'select' | 'radio' | 'textarea', required: How the field is filled and rendered. Values are stored as strings; signature and initials use images.
      - `assignee` union, required
        - 'sender'
        - object
          - `role` string, required
      - `pageIndex` integer, required
      - `xRatio` number, required
      - `yRatio` number, required
      - `widthRatio` number, required
      - `heightRatio` number, required
      - `required` boolean, required
      - `readonly` boolean, required
      - `defaultValue` string, nullable, required
      - `validation` FieldValidation, required
        - `minLength` integer
        - `maxLength` integer
        - `pattern` string
        - `min` number
        - `max` number
        - `minDate` string, date
        - `maxDate` string, date
      - `options` FieldOption[], nullable, required
        - `value` string, required
        - `label` string, required
      - `label` string, nullable, required
      - `id` string, required

## Other responses

- `400`: The request is malformed or the resource is in the wrong state for a synchronous check. Codes: `validation_error`, `invalid_body`, `invalid_parameter`, `invalid_idempotency_key`, `invalid_event_id`, `invalid_file`, `invalid_url`, `invalid_status`, `key_mismatch`, `no_signers`, `not_completed`, `envelope_exists`, `document_not_ready`, `upload_failed`.
- `401`: The credential is missing, malformed, unknown, expired, revoked, or past its rotation grace window. Code: `unauthorized`. Every authentication failure is deliberately indistinguishable.
- `403`: The credential is valid but may not do this. Codes: `insufficient_scope` (the route's scopes are not all held; `details` names `requiredScopes` and `missingScopes`), `forbidden` (the capability check on the resolved resource failed), `scope_escalation` (a grant wider than the caller's own was requested).
- `404`: The resource does not exist, was deleted, or belongs to another workspace. Code: `not_found`. The three cases are answered identically on purpose, so a 404 never confirms that an identifier exists elsewhere.
- `409`: The resource's state forbids this transition, or another request owns the work. Codes: `invalid_state`, `invalid_status`, `credential_state`, `limit_exceeded`, `envelope_missing`, `not_retryable`, `owner_unavailable`, `workspace_unavailable`, `detection_not_completed`, `document_in_use`, `version_conflict`, `document_processing`, `document_processing_failed`, `normalized_pdf_unavailable`, `pages_not_rendered`, `document_not_ready`, `idempotency_in_progress` (which also carries `Retry-After`), `payment_replayed` (a USDC payment signature presented twice), `payment_pending` (a USDC settlement whose outcome is not yet known).
- `422`: The request parsed but its content is not acceptable. Codes: `validation_error` (envelope configuration, with an `issues` array in `details`), `invalid_envelope`, `envelope_not_ready`, `document_page_limit`, `idempotency_key_reuse`.
- `429`: A limit was reached. Codes: `rate_limited` (the credential's per-bucket HTTP limit, or the per-workspace detection throttle), `workspace_limit_exceeded` (a workspace content or send limit; `details.limit` names it), `daily_budget_exceeded` (the workspace's daily detection budget). The four rate-limit headers are sent on this response and on no successful one, so a client cannot read its remaining budget without being refused. Retrying after `Retry-After` is always safe: a rate-limit refusal happens before the handler runs.
- `503`: A dependency the request needs is unreachable and nothing was changed. Codes: `queue_unavailable` (a background job could not be enqueued; retry the call), `billing_unavailable` (Stripe would not open a checkout session; nothing was charged, retry the call), `service_unavailable` (the rate-limit store is down, so the request was refused before running).

---

[API](https://skmtc.dev/inkseal/apis/inkseal-api.md) · [All operations](https://skmtc.dev/inkseal/apis/inkseal-api/llms.txt) · [OpenAPI document](https://skmtc.dev/inkseal/apis/inkseal-api/revisions/c440c981d578?raw)
