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.
Listing them
Section titled “Listing them”GET /v1/forms lists forms by default. Ask for is_template=true to list Private Templates instead:
curl -s -H "Authorization: Bearer $FOMR_TOKEN" \ "https://platform.fomr.io/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.
What is different
Section titled “What is different”| Form | Private Template | |
|---|---|---|
| Listed by | GET / | GET / |
| Permission | forms | templates |
| Collects submissions | Yes | No — the counts stay at zero |
urls.share | Set once published | Stays null |
publish, unpublish, validate | Yes | 400 INVALID_INPUT at is_template |
| Counted in the plan’s form limit | Yes | No |
| Becomes the other with | POST …/ | — 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.
Creating a Private Template
Section titled “Creating a Private Template”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.
curl -s -X POST https://platform.fomr.io/v1/forms \ -H "Authorization: Bearer $FOMR_TOKEN" \ -H "Content-Type: application/json" \ --data-binary @private-template.jsonConverting in place
Section titled “Converting in place”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.
Marking a form as a template
Section titled “Marking a form as a template”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.
When a conversion is refused
Section titled “When a conversion is refused”Nothing is written in any of these cases.
| Status | Code | reason | What to do |
|---|---|---|---|
409 | FORM_CONVERSION_BLOCKED | published | Unpublish the form, then mark it. |
409 | FORM_CONVERSION_BLOCKED | has_submissions | The form holds answers, which a template must not. Duplicate it as a template instead and leave the original alone. |
409 | FORM_ARCHIVED | — | The resource is archived. Restore it in the app first. |
{ "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.
Starting a form from a template
Section titled “Starting a form from a template”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:
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:readon the template andforms:write. - Plans. The copy counts against the form limit, so
402 PLAN_LIMIT_REACHEDis possible. A template that uses a Pro feature your plan does not include is refused with402 PRO_FEATURE_REQUIRED. is_templatematters. 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 insource_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.
Public Templates are not a resource
Section titled “Public Templates are not a resource”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.
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
400 | INVALID_INPUT | The 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. |
402 | PLAN_LIMIT_REACHED | A duplicate that produces a form would pass the plan’s form limit. limit carries the key, the maximum, and the current count. |
402 | PRO_FEATURE_REQUIRED | A 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. |
403 | FORBIDDEN | The member’s role lacks the templates or forms access the call needs. required_permission names it. |
404 | NOT_FOUND | The resource does not exist, is in Trash, or belongs to another organization. The three are not told apart. |
409 | FORM_CONVERSION_BLOCKED | A form cannot become a template because it is published or holds submissions. reason says which: published or has_submissions. |
409 | FORM_ARCHIVED | The resource is archived, so neither conversion applies. |
503 | SERVICE_UNAVAILABLE | Writes are briefly saturated. Retry-After says how long to wait. |
The full catalog, with the extra fields each code carries, is on Errors.