Skip to content
Join our Slack

Private Templates

A Private Template is a form your organization keeps as a starting point for new forms. In the API it is the same resource as a form, flagged is_template: true: the same form resource, the same Draft Document, and the same URLs. It just never collects answers.

GET /v1/forms lists forms by default. Ask for is_template=true to list Private Templates instead:

Terminal window
curl -s -H "Authorization: Bearer $FOMR_TOKEN" \
"https://platform.fomr.io/v1/forms?is_template=true"
GET /v1/forms?is_template=true
{
"data": [
{
"public_id": "Onb0ardT01",
"name": "Client onboarding",
"is_template": true,
"status": "draft",
"has_unpublished_changes": true,
"published_version": null,
"published_at": null,
"source_template_public_id": null,
"workspace_public_id": "Mn1w",
"access": {
"is_closed": false,
"close_at": null,
"open_at": null,
"accepting_submissions": false,
"duplicate_response_rule": null
},
"submissions": {
"completed_count": 0,
"partial_count": 0
},
"urls": {
"editor": "https://fomr.io/app/s/Onb0ardT01/_/editor",
"share": null
},
"domain": null,
"preview_image_url": null,
"created_at": "2026-07-02T11:30:00.000Z",
"updated_at": "2026-08-19T16:20:00.000Z"
}
],
"next_cursor": null
}

A page holds either forms or Private Templates, never both. Filters, sorting, and the cursor work as they do for forms; Listing forms documents them. Reading one template, its draft, or its published document uses the same URLs as a form.

FormPrivate Template
Listed byGET /v1/formsGET /v1/forms?is_template=true
Permissionformstemplates
Collects submissionsYesNo — the counts stay at zero
urls.shareSet once publishedStays null
publish, unpublish, validateYes400 INVALID_INPUT at is_template
Counted in the plan’s form limitYesNo
Becomes the other withPOST …/mark-template— a template is never turned back into a form; start a form from it instead

Permissions follow the flag. A member who can read forms but not templates gets 403 FORBIDDEN with required_permission: "templates:read" on a template, and the other way around. Permissions lists what each call needs.

POST /v1/forms with is_template: true in the Form Blueprint creates a Private Template. Everything else works as in Creating a form, including dry_run, except that the call needs templates:write and does not count against the form limit.

Terminal window
curl -s -X POST https://platform.fomr.io/v1/forms \
-H "Authorization: Bearer $FOMR_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @private-template.json

Marking flips the flag on the form you already hold and copies nothing. The public_id, the Draft Document, and every page and block ID stay the same, so IDs you stored earlier keep resolving. The conversion goes one way only, as in the app: a Private Template is never turned back into a form. To get a form from a template, start a form from it.

Marking needs forms:write and templates:write together. A token missing either gets 403 FORBIDDEN naming it.

Terminal window
curl -s -X POST -H "Authorization: Bearer $FOMR_TOKEN" \
"https://platform.fomr.io/v1/forms/Ev3ntReg01/mark-template"

There is no body. The response is the form resource with is_template: true. A custom domain bound to the form is unbound, since a template is never served to respondents. A resource that is already a Private Template answers 200 and writes nothing.

Only a form that is unpublished and has no submissions, including Partial Responses, can be marked. See When a conversion is refused.

Nothing is written in any of these cases.

StatusCodereasonWhat to do
409FORM_CONVERSION_BLOCKEDpublishedUnpublish the form, then mark it.
409FORM_CONVERSION_BLOCKEDhas_submissionsThe form holds answers, which a template must not. Duplicate it as a template instead and leave the original alone.
409FORM_ARCHIVED—The resource is archived. Restore it in the app first.
409 FORM_CONVERSION_BLOCKED
{
"code": "FORM_CONVERSION_BLOCKED",
"message": "This form cannot be converted.",
"request_id": "0f3c2a9e-6b1d-4e57-9a1c-2d8e4f6b7c90",
"reason": "has_submissions"
}

Branch on reason. You can clear published with one more call, but not has_submissions. New reasons may be added, so treat any other reason as one you cannot clear.

POST /v1/forms/{form_public_id}/duplicate with is_template: false copies the template into a new form. It answers 201 with the new form, its stored draft under draft, and the ETag response header:

Terminal window
curl -s -X POST "https://platform.fomr.io/v1/forms/Onb0ardT01/duplicate" \
-H "Authorization: Bearer $FOMR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Acme onboarding","is_template":false}'

The copy is an ordinary form with its own public_id and draft, nothing published, and no submissions. Its source_template_public_id holds the template’s public_id and never changes. The template also counts the use, as it does in the app, though that count is not exposed in /v1.

  • Permissions. The call needs templates:read on the template and forms:write.
  • Plans. The copy counts against the form limit, so 402 PLAN_LIMIT_REACHED is possible. A template that uses a Pro feature your plan does not include is refused with 402 PRO_FEATURE_REQUIRED.
  • is_template matters. Leave it out and the copy keeps the source’s kind, so a plain duplicate of a Private Template is another Private Template. Either way a copy of a template records it in source_template_public_id, as the app does.

Duplicating a form covers what a copy carries over. The same call in the other direction, with is_template: true against a form, saves a copy as a template and leaves the original alone.

The template gallery on fomr.io holds Public Templates, a library Fomr publishes for everyone. They are not resources in your organization: GET /v1/forms?is_template=true never lists them, and every /v1 call on one answers 404 NOT_FOUND.

A form started from the gallery in the app is an ordinary form in your organization, and /v1 reads it like any other. Its source_template_public_id may name the gallery template it came from, but reading that ID answers 404 NOT_FOUND.

Form templates covers the gallery, and saving and reusing templates, from the app’s side.

StatusCodeWhen
400INVALID_INPUTThe duplicate body carries a field other than name and is_template, or a name outside 1 to 100 characters, or the call is publish, unpublish, or validate, which a Private Template never takes. issues[] names it by path.
402PLAN_LIMIT_REACHEDA duplicate that produces a form would pass the plan’s form limit. limit carries the key, the maximum, and the current count.
402PRO_FEATURE_REQUIREDA duplicate’s source uses a Pro feature in its settings or properties that the organization’s plan does not include. feature and path name it.
403FORBIDDENThe member’s role lacks the templates or forms access the call needs. required_permission names it.
404NOT_FOUNDThe resource does not exist, is in Trash, or belongs to another organization. The three are not told apart.
409FORM_CONVERSION_BLOCKEDA form cannot become a template because it is published or holds submissions. reason says which: published or has_submissions.
409FORM_ARCHIVEDThe resource is archived, so neither conversion applies.
503SERVICE_UNAVAILABLEWrites are briefly saturated. Retry-After says how long to wait.

The full catalog, with the extra fields each code carries, is on Errors.

Your next form could be the best-looking thing on your site.

Free forever. Unlimited responses. No signup to start.