Edit templates

Edit a template through a draft, then publish your changes as a new version.

The Templates API reads a template's structure and edits its content. Use it to keep template
wording in step with a system of record, to roll the same change across many templates at once, or
to let an internal tool rename questions without anyone opening the web editor.

Editing works the same way it does in the web editor. Your changes go into a draft that inspectors
never see. When the draft is ready, you publish it, and it becomes the template's next version.

The Templates API supports a defined set of edits: the template's name and description, the labels
of items that already exist, and one setting. See
What the API does not support.

Requirements

Requests run as the token owner. Reading a template's definition needs view access. Everything
else in this guide needs edit access.

How drafts work

Published   ──[ version 1 ]───────────────────────────[ version 2 ]──►
                  │ open draft                             ▲ publish
                  ▼                                        │
Draft             ●────edit────●────edit────●──────────────┘
                  (not visible to inspectors until you publish)

A template has at most one draft, and it is shared. If a colleague has the template open in the
web editor, your API calls edit the same draft they are working in. Opening a draft that already
exists returns the existing one rather than replacing it.

Publishing consumes the draft. Once you publish, the template has no draft again, and the next edit
starts a new one. Discarding a draft throws away every unpublished change and leaves the published
template exactly as it was.

The endpoints

EndpointPurpose
GET /templates/integration/v1/templates/{template_id}/definitionReads a published template.
GET /templates/integration/v1/templates/{template_id}/draftReads a draft.
POST /templates/integration/v1/templates/{template_id}/draftOpens a draft.
PUT /templates/integration/v1/templates/{template_id}/draftApplies edits to a draft.
DELETE /templates/integration/v1/templates/{template_id}/draftDiscards a draft.
POST /templates/integration/v1/templates/{template_id}/publishPublishes a draft.

There is no separate endpoint for each kind of edit. Every edit is an operation sent to the same
update endpoint.

The shortest useful example

Renaming a template takes two calls. The update endpoint creates the draft if the template has
none, so there is nothing to open first.

Rename the template, then publish it

curl -X PUT "https://api.mitti.com/templates/integration/v1/templates/template_abc123/draft" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{ "operations": [ { "set_template_name": { "name": "Site inspection v2" } } ] }'

curl -X POST "https://api.mitti.com/templates/integration/v1/templates/template_abc123/publish" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{ "if_draft_revision_is": "{revision from the first response}" }'

Run that by hand once before you automate anything. The rest of this guide covers finding item IDs,
editing a template someone else is also editing, and reading the errors.

1. Read the template

Start with the published definition. It gives you the template's name, description, and a flat list
of every item, and it is the only way to get the item IDs you need to change a label.

If you do not know the template's ID, find it with SearchTemplates.

Read the published template

curl "https://api.mitti.com/templates/integration/v1/templates/template_abc123/definition" \
  -H "Authorization: Bearer {api_token}"

The above command returns the template's metadata and items

{
  "template": {
    "template_identity": {
      "template_id": "template_abc123",
      "organization_id": "role_def456"
    },
    "template_name": "Site inspection",
    "description": "Weekly walkthrough of the plant floor.",
    "items": [
      {
        "item_id": "6a0f2e11-9c34-4c1a-8f0e-2b7d5a91c004",
        "type": "ITEM_TYPE_PAGE",
        "label": "Loading dock",
        "parent_id": "",
        "page_item": {}
      },
      {
        "item_id": "b3d17c58-4a2e-49f7-9c11-8e5f0a6d2b73",
        "type": "ITEM_TYPE_QUESTION",
        "label": "Is the dock plate secured?",
        "parent_id": "6a0f2e11-9c34-4c1a-8f0e-2b7d5a91c004",
        "question_item": {
          "response_set_id": "e2c4a880-1f3b-4d6e-a057-9b8c7d6e5f40"
        }
      }
    ]
  }
}

items is flat. Each item carries a parent_id, so you rebuild the page and section hierarchy
yourself.

2. Open a draft

Open the template for editing

curl -X POST "https://api.mitti.com/templates/integration/v1/templates/template_abc123/draft" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{}'

The above command returns the draft's revision token

{
  "revision": "1-3f9c1d0a7b624e859a13c04d8f27e6b1",
  "created": true
}

revision identifies this exact state of the draft. Pass it back on later calls, unmodified.
created tells you whether you opened a new draft or received one that already existed.

Revision and version tokens share one shape: a number, a hyphen, then a UUID with the dashes
stripped, such as 12-50c9fbf262794be1bc60d0c2f3ec7c0b. In a draft revision the number counts
edits to that draft. In a published version it counts the template's published versions. The two
sequences are independent, so never compare a draft revision against a published version. The UUID
changes on every revision. Compare whole tokens for equality and read nothing else into them.

📘

A created value of false means the template already had a draft. Check the revision number
before you continue. Revision 1 means the draft was opened but never edited, so it still matches
the published template. A higher number means someone has unpublished changes here. Read the draft
before you edit it, and take care before you publish, because publishing makes their changes live
along with yours.

Opening a draft is safe to retry. Calling it twice returns the same draft.

3. Apply your edits

Send your changes to the update endpoint as a list of operations. Each operation names the thing it
changes. Operations apply in order, and the whole list either succeeds or fails together, so a
request never leaves the draft half-edited.

Rename the template and reword a question in one request

curl -X PUT "https://api.mitti.com/templates/integration/v1/templates/template_abc123/draft" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      { "set_template_name": { "name": "Site inspection v2" } },
      { "set_item_label": {
          "item_id": "b3d17c58-4a2e-49f7-9c11-8e5f0a6d2b73",
          "label": "Is the dock plate secured and pinned?"
        }
      }
    ],
    "if_draft_revision_is": "1-3f9c1d0a7b624e859a13c04d8f27e6b1"
  }'

The above command returns a new revision token

{
  "revision": "2-7e02b4c91d384f6ab5c72098e3af1d64",
  "previous_revision": "1-3f9c1d0a7b624e859a13c04d8f27e6b1",
  "side_effects": null
}

Carry the new revision into your next call. previous_revision reports the revision your
operations landed on, which is empty when the request created the draft.

The update endpoint creates the draft when one does not exist, so you skip step 2 when your
integration is the only editor.

if_draft_revision_is is optional on this endpoint. Omit it, and your operations apply to the draft
as it stands, which is what you want on a first call because there is no revision yet to match
against. Include it, and the API applies your operations only if the draft is still at that exact
revision. See Editing alongside other people.

Available operations

OperationChanges
set_template_nameThe template's name. Required, 1 to 255 characters.
set_template_descriptionThe template's description. Up to 1,000 characters. An empty string clears it.
set_item_labelThe label of one item, named by item_id. An empty string clears it.
set_template_settingOne named setting, leaving the others untouched.

Send 1 to 100 operations per request. Set exactly one field per operation.

Length limits count characters, not bytes, so an emoji or an accented letter counts as one.
Leading and trailing whitespace is trimmed before the limit is applied.

The maximum length of an item label depends on the item's type. When a label is too long, the error
names the limit that applied. An item_id the draft does not contain is rejected with a 400.

set_template_setting changes a single setting. skip_completion_confirmation controls whether
inspectors see the confirmation dialog when they complete an inspection.

Skip the completion confirmation dialog

{
  "operations": [
    { "set_template_setting": { "skip_completion_confirmation": true } }
  ]
}

More than 100 operations

Split the list into chunks of 100. Each chunk is atomic on its own, but the chunks are not atomic
together, so chain them. Take the revision from each response and send it as the next request's
if_draft_revision_is. A later chunk then fails if someone edited the draft between chunks.

Apply a long list in chunks (python example)

revision = None
for start in range(0, len(operations), 100):
    body = {"operations": operations[start:start + 100]}
    if revision:
        body["if_draft_revision_is"] = revision
    response = requests.put(f"{api}/draft", headers=auth, json=body, timeout=30)
    response.raise_for_status()
    revision = response.json()["revision"]

If a chunk fails partway through the run, discard the draft rather than leaving the earlier chunks
sitting in it. Whoever opens that template next inherits a half-applied change with no way to tell
what was meant to happen.

Side effects

Some edits require the API to change something you did not ask about. The side_effects field
reports those changes, with the items affected and a message you display to a user. Read
message even when you do not recognize the kind, so your integration handles kinds it was not
written for.

When an edit changes nothing beyond what you asked for, side_effects is null. Check for null
before you read changes.

4. Review the draft

Read the draft back before you publish. Because inspectors never see a draft, it waits as long as
your review takes.

Read the draft

curl "https://api.mitti.com/templates/integration/v1/templates/template_abc123/draft" \
  -H "Authorization: Bearer {api_token}"

The response has the same shape as the published definition, plus the draft's current revision.
A 404 means the template has no draft, which is the normal response for a template nobody is
editing.

5. Publish

Publish the draft as a new version

curl -X POST "https://api.mitti.com/templates/integration/v1/templates/template_abc123/publish" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{ "if_draft_revision_is": "2-7e02b4c91d384f6ab5c72098e3af1d64" }'

The above command returns the new published version

{ "version": "2-8d4f0a1b6c2e49573a8b0c1d2e3f4a5b" }

Your changes are now live, the draft is gone, and its revision token no longer works. The returned
version is the template's new published version, and its leading number is the version number.

if_draft_revision_is is required when you publish, so draft content you have not read never
reaches inspectors.

Editing alongside other people

Two editors can work on the same template at once, and often one is a person in the web editor
while the other is your integration. Send if_draft_revision_is with the revision you last saw, and
the API applies your change only if the draft is still in that state. If someone edited in between,
the API rejects the request instead of overwriting their work.

🚧

Important: if_draft_revision_is is optional on PUT .../draft, and required on
POST .../publish and DELETE .../draft. Publishing makes changes live, and discarding destroys
unpublished work. Neither is reversible.

A rejected request comes back as a 409 carrying the draft's current revision. See
A rejected revision for the shape and what to do with it.

Discarding a draft

Discarding throws away every unpublished change. The published template is untouched, so there is
nothing to roll back.

Discard the draft

curl -X DELETE "https://api.mitti.com/templates/integration/v1/templates/template_abc123/draft?if_draft_revision_is=2-7e02b4c91d384f6ab5c72098e3af1d64" \
  -H "Authorization: Bearer {api_token}"

Because a DELETE request carries no body, if_draft_revision_is travels as a query parameter.

🚧

Important: A draft holds the unpublished work of everyone who edited it, including changes
made in the web editor. Read the draft before you discard it.

Errors

StatusMeaning
400A field or an operation failed validation.
401The token is missing, expired, or invalid.
403The token owner lacks edit access to the template.
404The template does not exist, is archived, or has no draft.
409The draft changed since the revision you sent. Read the draft again before retrying.

When a request fails, none of its operations apply.

Two error shapes

A 400 arrives in one of two shapes depending on which layer caught the problem, and a client that
parses only one of them breaks on the other.

A malformed request never reaches the Templates API. The layer in front of it rejects the request
and returns a message with an empty details.

{
  "code": 3,
  "message": "invalid field if_draft_revision_is: value must have a length between 34 and 65",
  "details": []
}

The Templates API rejects a request that is well formed but asks for something the template cannot
do, and names the field it rejected.

{
  "code": 3,
  "message": "validation failed",
  "details": [
    {
      "@type": "type.googleapis.com/google.rpc.BadRequest",
      "field_violations": [
        {
          "field": "operations[0].set_item_label.item_id",
          "reason": "VALIDATION_FAILED",
          "description": "item not found: b3d17c58-4a2e-49f7-9c11-8e5f0a6d2b73"
        }
      ]
    },
    {
      "@type": "type.googleapis.com/google.rpc.ErrorInfo",
      "reason": "VALIDATION_FAILED",
      "domain": "templates.safetyculture.io"
    }
  ]
}

Read details as optional and fall back to message.

Reason codes

Errors from the Templates API carry an ErrorInfo detail with a reason and the domain
templates.safetyculture.io. Match on reason. Message text can change and is not part of the
contract.

ReasonMeaning
STALE_DRAFT_REVISIONif_draft_revision_is does not match the draft's current revision.
VALIDATION_FAILEDA field value was rejected.
VALUE_EMPTYA required field was empty.
OPERATION_NOT_SETAn entry in operations set none of its fields.
SETTING_NOT_SETA set_template_setting operation named no setting.
TOO_MANY_OPERATIONSMore than 100 operations in one request.
TEMPLATE_INCOMPATIBLEThe template cannot be edited through this API.

A rejected revision

A 409 means the draft moved after the revision you sent. The error metadata carries the draft's
current revision, so you can tell whether the draft moved or disappeared before you read it again.

{
  "code": 10,
  "message": "template draft has been modified",
  "details": [
    {
      "@type": "type.googleapis.com/google.rpc.ErrorInfo",
      "reason": "STALE_DRAFT_REVISION",
      "domain": "templates.safetyculture.io",
      "metadata": {
        "current_draft_revision": "3-cc3d7635a1f04e2b8d90e5c7412a6f83"
      }
    }
  ]
}

current_draft_revision is empty when the draft is gone, which is what you see when someone
published or discarded it while you were working.

🚧

Important: do not retry with the revision from the metadata. It belongs to whatever the other
editor just did, and publishing or discarding against it makes their unreviewed work live or
throws it away. Read the draft, see what changed, then decide.

What the API does not support

The Templates API lets you change the template's name and description, rename items that already
exist, and turn the completion confirmation dialog off or on.

The Templates API does not support adding, moving, or removing items and pages, creating a template
from scratch, or changing template permissions. Use the web editor for those changes.