Templates
A template is reusable message content — a name, a title, a body, and optionally an image, a deep link and a custom data payload. Reference it from a send and it fills in whatever the send does not supply itself. Liquid inside a template is validated when you save it, so a broken expression is caught by the person editing the copy rather than by the campaign that uses it.
All examples use https://app.openpush.ai, the OpenPush API base URL.
When to use this page
Use templates for content you send repeatedly or content someone other than an engineer maintains: a welcome message, a win-back nudge, a receipt. Journeys require them — a journey's send-push step references a template by id and will not go live without one.
If the content is one-off, just put the title and body in the send.
Prerequisites
- The app's REST API key.
- If the template references dynamic content, the tables must already exist. Saving a template that references a missing table fails.
Template fields
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Human label. Also usable as the reference on a send |
title | string | yes | Liquid-enabled |
body | string | yes | Liquid-enabled |
image_url | string | no | Must be https:// or a media path OpenPush hosts. Liquid-enabled |
deep_link | string | no | Liquid-enabled |
data | object | no | Custom payload, merged into the send |
Read-only fields on a stored template:
| Field | Meaning |
|---|---|
id | tpl_…, assigned on create |
created_at | Creation timestamp |
sends_count | How many messages have used this template |
src_id | Set on templates pulled in from a OneSignal history import; null on native templates |
Creating a template
Code
Code
All three of name, title and body are required — omitting any is
400 name, title and body required.
Validation at save time
Two checks run before the row is written, and both fail the request with a 400:
- Liquid compiles. Every Liquid-enabled field is parsed. A syntax error comes back naming the field, the line and the column.
- Referenced dynamic content exists. If the copy mentions
dynamic_content.offersand there is noofferstable on this app, the save is rejected. This is the same check the send path runs, moved earlier.
image_url is validated too — it must be https:// with a host or a media path OpenPush hosts,
no whitespace, at most 2048 characters.
Creating one in the console
The console's template editor writes the same rows through the same validation. Use it when the person maintaining copy is not the person calling the API. There is nothing a console-created template can do that an API-created one cannot.
Listing templates
Code
Code
Newest first. The builtin array lists the built-in templates that ship with the server — see
below.
Using a template in a message
Reference it with template or template_id. Either the template's id or its name works.
Code
How merging works
The send body wins, field by field. Anything you leave out falls back to the template.
Code
That send uses your title and the template's body, deep link and data. There is no partial merge
inside data — supplying data on the send replaces the template's data object entirely.
Because Liquid is kept as source until per-device rendering, a template's {{ nm }} still resolves
against each recipient's tags, not against anything fixed at compose time.
The same referencing works on POST /v1/apps/{app_id}/send-test.
Every delivery that used a stored template increments its sends_count.
Errors
| Response | Cause |
|---|---|
404 unknown template '<ref>' | The reference matched neither a stored template id or name, nor a built-in |
400 need title+body or a known template | The resolved template did not supply both a title and a body, and the send body did not fill the gap |
Built-in templates
Three templates ship with the server and are referenceable by name without creating anything:
| Name | Purpose |
|---|---|
welcome_flock | A first-run welcome |
comeback_1 | A win-back nudge, with Liquid placeholders |
first_push_test | An integration smoke test |
They supply only a title and a body. A built-in cannot carry an image, a deep link or a data
payload — if you need those, create a real template. Built-ins are not editable and do not appear in
the templates array, only in builtin.
Updating and deleting
Code
Code
Any of name, title, body, image_url, deep_link and data can be patched, and the same
Liquid and dynamic-content validation runs again. An empty patch is 400 nothing to update; an
unknown id is 404 unknown template.
Code
Code
Deleting a template does not rewrite history. Messages already sent keep their rendered content and their
template_idreference. But a journey whose send-push node points at a deleted template will fail validation the next time it is activated, and templates you send by name will start returning404 unknown templatefrom your backend. Check both before deleting.
Imported OneSignal templates
Templates pulled in by the console's OneSignal history import carry a src_id: the provider's own
stable identifier for that template.
That field exists so a re-run of the import updates the existing row rather than creating a duplicate. If you pull your OneSignal archive twice — which you will, because the first pull is never quite complete — you end up with one template per source template, not two.
Things to know about src_id:
- It is read-only. There is no way to set or change it through the API; it is written only by the importer.
- Native templates created through the API or the console have
src_id: null. - Imported templates are ordinary templates in every other respect: editable, deletable, usable in sends and journeys.
The OneSignal history import itself is a console action under the app's migration screens, not a
/v1 route. See migrate from OneSignal.
Limits
- Per-language content is stored on the template. A send that references the template uses
the template's
languages(titles, bodies, iOS subtitles and button labels) unless the send has its ownlanguages. That map replaces the template's whole map; entries are not merged. A send that overrides the template'stitleorbodygets none of the template's translations, because the override goes to every device. - Templates have no A/B variants, no TTL, no priority, no collapse key and no scheduling. All of those are message-level.
- No template versioning or history. A patch overwrites; there is no rollback.
- No duplicate-template route. Read one and create a new one.
- No folders, tags or categories on templates.
dataon a template is replaced wholesale bydataon a send, never merged key by key.image_urlfollows the same rules as on a message:https://only, 2048 characters, no whitespace.
FAQ
Should I reference templates by id or by name? By id from code — names are editable and someone will rename one. By name is convenient for quick manual sends and for the built-ins.
Can two templates share a name? Nothing prevents it, and reference-by-name then resolves ambiguously. Keep names unique.
Does editing a template change messages already scheduled with it? No. A template is resolved into the message when you create it, so a scheduled send carries the copy the template held at compose time. Editing the template afterwards affects only messages created after the edit. (Dynamic-content values are the exception: those are snapshotted when delivery begins — see personalization.)
Why did my template save fail with a dynamic content error? The copy references a table that does not exist on this app. Create the table first, then save the template. See personalization.
Can a journey use a built-in template?
No. A journey's send-push node needs a template_id that resolves to a stored template row on the
app.
Where do I see how often a template is used?
sends_count on the template row counts messages delivered with it.
Related
- Sending messages — how a template merges into a send
- Personalization — Liquid and dynamic content inside a template
- Journeys — where templates are mandatory
- Migrate from OneSignal — imported templates and
src_id - Templates and dynamic content API reference