Templates
10 endpoints in this family.
List message templates
GET/api/v1/templates
Returns the templates of the client you act on (the Nodium-Tenant header, or your default client). A template belongs to one client and is deployed to its WhatsApp accounts; status is where it stands at Meta: only approved can be sent. With scope=account (a key that sees every client), the templates of all your clients, each with its clientId and clientName.
A template is identified at Meta by its name AND its language — the same name in two languages is two templates. To send one, its name is enough: Nodium picks the approved version in the WhatsApp account of the number that sends.
Query parameters
scopestringaccount: the templates of every client of your account.account
Response
The templates, with where each stands at Meta.
data.templatesTemplate[]The templates, recently changed first.
data.summaryobjectCounts:
total,draft(never deployed),deployed.
Refusals
- 403
all_clients_required`scope=account` needs a key that sees every client.
curl https://nodium.io/api/v1/templates \ -H "Authorization: Bearer $NODIUM_KEY"{ "data": { "templates": [ {} ], "summary": {} }}Draft a template
POST/api/v1/templates
Writes a template for your account. Nothing is sent to Meta yet — drafting and deploying are two steps on purpose, because deploying reserves the name at Meta in each client's WhatsApp account. Deploy it with POST /templates/{id}/deploy.
The template belongs to the client you act on (the Nodium-Tenant header, or your default client) and is deployed to its WhatsApp accounts, including numbers it connects later.
Body
namestringrequiredLowercase letters, digits and underscores. Reserved at Meta on deployment.
languagestringrequiredLanguage tag, for example
froren_US.categorystringrequiredWhat Meta charges it as.
UTILITYMARKETINGAUTHENTICATIONbodystringrequiredThe text, with
{{1}}style placeholders. Not used for anAUTHENTICATIONtemplate: Meta writes its text.authenticationobjectFor an
AUTHENTICATIONtemplate only:{ "securityRecommendation": true, "codeExpirationMinutes": 10, "buttonText": "Copy code" }. Meta writes the text (“*123456* is your verification code.”); the template gets one copy-code button. Send it with the code as variable1.headerstringAn optional one-line text header.
headerMediaobjectAn image, video or document header instead of a text one:
{ "sampleId": "…", "filename": "menu.pdf" }, the sample uploaded withPOST /templates/samplesfor the same client. Meta reviews the template with it; it is also the header sent when a message gives no other.footerstringAn optional footer, without placeholders.
examplesobjectOne sample value per numbered placeholder, by number:
{ "1": "Marie" }. Meta needs them to review a template that has placeholders; they are never used at send time.buttonsobject[]Up to ten buttons: quick reply, url (two at most), phone number (one), copy code (one, marketing only), and one button that opens a WhatsApp Flow:
{ "type": "FLOW", "text": "Book", "flowId": "<Flow id>", "screen": "SLOTS" }—flowIdfromGET /flows,screenoptional (the first screen by default; ignored for a dynamic Flow). The Flow must be published when the template is deployed; in each WhatsApp account of the client, Nodium uses the Flow of the same name.
Response
The template as drafted, not yet deployed.
data.templateTemplatedata.holesobjectThe placeholders to fill at send time:
numberedandnamed.
Refusals
- 409
template_existsThis client already has a template with this name and language. - 422
template_header_invalidThe `headerMedia` sample is unknown, the template has both a text header and a media header, or an `AUTHENTICATION` template has a header. - 422
template_buttons_invalidA button is not allowed in this category (a copy-code button outside a marketing template), or the Flow button is invalid: unknown or deprecated Flow, more than one Flow button, label over 25 characters. - 422
template_variables_invalidThe footer has a placeholder: Meta refuses it.
curl -X POST https://nodium.io/api/v1/templates \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "…", "language": "fr", "category": "UTILITY", "body": "…" }'{ "data": { "template": { "id": "…", "name": "…", "language": "…", "category": "UTILITY", "status": "draft", "clientId": "…", "deployedAt": "…", "headerMedia": {}, "authentication": {}, "body": "…", "createdAt": "…", "updatedAt": "…" }, "holes": {} }}Delete a template
DEL/api/v1/templates/{id}
Deletes a template of the client you act on that Meta holds nowhere: a draft, or one refused or never deposited in each of its WhatsApp accounts. A template in review or approved cannot be deleted: sends refer to it. Nothing is deleted at Meta.
Path parameters
idstringThe template id.
Response
Confirmation that it is gone.
data.deletedbooleanAlways
true.
Refusals
- 409
template_lockedMeta holds it (in review, approved, paused or disabled).
curl -X DELETE https://nodium.io/api/v1/templates/<id> \ -H "Authorization: Bearer $NODIUM_KEY"{ "data": { "deleted": true }}Get one template
GET/api/v1/templates/{id}
Returns a template of the client you act on (the Nodium-Tenant header, or your default client), its placeholders, and where it stands in each of that client's WhatsApp accounts: one line per account, including those it has not reached yet (not_deployed: a number is there but the template is not; no_number: the client has no number yet). A template of another client answers 404.
Path parameters
idstringThe template id.
Response
The template, and its state in each WhatsApp account of its client.
data.templateTemplatedata.holesobjectThe placeholders to fill at send time:
numberedandnamed.data.deploymentsobject[]Its state in each WhatsApp account of its client.
Show fields
wabaIdstringThe client's WhatsApp Business account, or
null.statusstringIts state there.
submittedapprovedrejectedpauseddisabledfailednot_deployedno_numbercategorystringThe category Meta kept there, when it differs from the template's; otherwise
null.rejectionReasonstringWhy Meta refused it there, or why the deposit failed; otherwise
null.submittedAtstringWhen it was last sent to Meta there, or
null.reviewedAtstringWhen Meta last decided there, or
null.
data.editablebooleanWhether its text can change: not while Meta reviews it, nor once Meta disabled it. Approved or paused, it can be corrected (it then goes back to review).
data.categoryLockedbooleanWhether its category is fixed: Meta holds it approved or paused.
curl https://nodium.io/api/v1/templates/<id> \ -H "Authorization: Bearer $NODIUM_KEY"{ "data": { "template": { "id": "…", "name": "…", "language": "…", "category": "UTILITY", "status": "draft", "clientId": "…", "deployedAt": "…", "headerMedia": {}, "authentication": {}, "body": "…", "createdAt": "…", "updatedAt": "…" }, "holes": {}, "deployments": [ { "wabaId": "…", "status": "submitted", "category": "…", "rejectionReason": "…", "submittedAt": "…", "reviewedAt": "…" } ], "editable": true, "categoryLocked": true }}Edit a template
PATCH/api/v1/templates/{id}
Changes a template of the client you act on, except while Meta reviews it or once Meta disabled it. After a rejection, fix it here and deploy again: the rejected copies go back to review. Approved or paused, it can also be corrected: the new text is sent to Meta first and the template goes back to review. Meta limits these corrections (one per 24 hours, ten per 30 days) and does not let the category change: its reason comes back as an error. Once deployed, the name and language are fixed — write a new template instead.
Path parameters
idstringThe template id.
Body
namestringA new name, before its first deployment only.
languagestringA new language, before its first deployment only.
bodystringThe text.
headerstringThe text header line.
headerMediaobjectAn image, video or document header:
{ "sampleId": "…", "filename": "…" }fromPOST /templates/samples;nullremoves it. Kept as it is when omitted.footerstringThe footer, without placeholders.
authenticationobjectFor an
AUTHENTICATIONtemplate:{ securityRecommendation, codeExpirationMinutes, buttonText }. Kept as they are when omitted.examplesobjectOne sample value per numbered placeholder, by number:
{ "1": "Marie" }. Kept as they are when omitted.buttonsobject[]The buttons, as in
POST /templates— including the one that opens a Flow:{ "type": "FLOW", "text", "flowId", "screen" }.categorystringWhat Meta charges it as.
UTILITYMARKETINGAUTHENTICATION
Response
The template, updated, and the corrections sent to Meta.
data.templateTemplatedata.holesobjectIts placeholders:
numberedandnamed.data.correctionsobject[]When it was approved or paused: one line per WhatsApp account,
{ clientId, clientName, wabaId, status, reason }—reasonis set when Meta refused the correction there.
Refusals
- 409
template_lockedMeta is reviewing it, Meta disabled it, or its name or language changed after deployment. - 409
template_category_lockedApproved or paused at Meta, its category cannot change. - 409
template_edit_refusedMeta refused the correction everywhere (limit reached, text refused…); `data.corrections` says why. Nothing changed. - 409
template_existsAnother template of this client already has this name and language. - 422
template_header_invalidThe `headerMedia` sample is unknown, or the template would have both a text header and a media header.
curl -X PATCH https://nodium.io/api/v1/templates/<id> \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "…", "language": "…" }'{ "data": { "template": { "id": "…", "name": "…", "language": "…", "category": "UTILITY", "status": "draft", "clientId": "…", "deployedAt": "…", "headerMedia": {}, "authentication": {}, "body": "…", "createdAt": "…", "updatedAt": "…" }, "holes": {}, "corrections": [ {} ] }}Deploy a template to Meta
POST/api/v1/templates/{id}/deploy
External and final. Deposits a template of the client you act on in each WhatsApp account of its numbers, where Meta reviews it. Where it is missing it is deposited; where Meta rejected it, it goes back to review with its current text; where the deposit failed, it is retried; where it is in review or approved, nothing moves. Call it again at any time: it only does what is left to do.
Numbers the client connects later receive it automatically. Meta almost always answers submitted and reviews later: follow template.status_changed, or POST /templates/{id}/refresh.
Path parameters
idstringThe template id.
Response
The template and its state in each WhatsApp account.
data.templateTemplatedata.resultsobject[]One line per WhatsApp account of the client.
Show fields
clientIdstringThe client.
clientNamestringIts name.
wabaIdstringIts WhatsApp Business account, or
nullwhen it has no number yet.statusstringWhere the template stands there now.
submittedapprovedrejectedpauseddisabledfailedno_numberreasonstringWhy the deposit failed, or
null.
Refusals
- 503
meta_token_missingThe WhatsApp connection is not configured on this instance.
curl -X POST https://nodium.io/api/v1/templates/<id>/deploy \ -H "Authorization: Bearer $NODIUM_KEY"{ "data": { "template": { "id": "…", "name": "…", "language": "…", "category": "UTILITY", "status": "draft", "clientId": "…", "deployedAt": "…", "headerMedia": {}, "authentication": {}, "body": "…", "createdAt": "…", "updatedAt": "…" }, "results": [ { "clientId": "…", "clientName": "…", "wabaId": "…", "status": "submitted", "reason": "…" } ] }}Ask Meta where a template stands
POST/api/v1/templates/{id}/refresh
Re-reads at Meta the state of the template in each WhatsApp account of its client and stores it. Returns changed: 0 when nothing moved, so polling is cheap. Every change is also sent as template.status_changed, so you rarely need to poll.
Path parameters
idstringThe template id.
Response
The template and how many WhatsApp accounts changed.
data.templateTemplatedata.changedintegerHow many WhatsApp accounts changed state.
Refusals
- 503
meta_token_missingThe WhatsApp connection is not configured on this instance.
curl -X POST https://nodium.io/api/v1/templates/<id>/refresh \ -H "Authorization: Bearer $NODIUM_KEY"{ "data": { "template": { "id": "…", "name": "…", "language": "…", "category": "UTILITY", "status": "draft", "clientId": "…", "deployedAt": "…", "headerMedia": {}, "authentication": {}, "body": "…", "createdAt": "…", "updatedAt": "…" }, "changed": 0 }}Import the templates already at Meta
POST/api/v1/templates/import
Copies into Nodium the templates found in the WhatsApp Business accounts of the client you act on (the Nodium-Tenant header, or your default client). With scope=account (a key that sees every client), those of all your clients. Reads at Meta, writes on the Nodium side only. Each template found becomes a template of the client where it was found; one with the same name and language there is recorded as deployed instead.
Query parameters
scopestringaccount: every client of your account.account
Response
How many templates were read, and what could not be.
data.countintegerHow many templates were read.
data.skippedobject[]What was left aside, and why.
Refusals
- 409
no_wabaNo WhatsApp number is connected here yet. - 403
all_clients_required`scope=account` needs a key that sees every client.
curl -X POST https://nodium.io/api/v1/templates/import \ -H "Authorization: Bearer $NODIUM_KEY"{ "data": { "count": 0, "skipped": [ {} ] }}Prepare a header sample upload
POST/api/v1/templates/samples
Starts the upload of the sample of an image, video or document header. Send what the file is; you get a sampleId and an uploadUrl. Upload the file there yourself with PUT (the raw bytes, with its Content-Type), then confirm it with POST /templates/samples/{id}/confirm. Cite the sampleId as headerMedia.sampleId when you write the template (POST /templates or PATCH /templates/{id}) for the same client.
Meta reviews the template with this sample. It is also the header sent with each message, unless the message gives its own (headerMediaId on POST /messages). Accepted: JPEG or PNG images up to 5 MB, MP4 videos up to 16 MB, PDF documents up to 100 MB. The upload URL expires after two hours. Nothing is sent to Meta by this call.
Body
filenamestringrequiredThe file name, as shown with a document header.
mimeTypestringrequiredimage/jpeg,image/png,video/mp4orapplication/pdf.sizeintegerrequiredIts size in bytes.
Response
The `sampleId` and the URL to upload the file to.
data.sampleIdstringConfirm it, then pass it as
headerMedia.sampleId.data.kindstringThe header it makes.
imagevideodocumentdata.uploadUrlstringUpload the file here with
PUT, its bytes as the body and itsContent-Type.
Refusals
- 413
file_too_largeOver 5 MB for an image, 16 MB for a video, 100 MB for a document. - 415
file_type_refusedNot a JPEG or PNG image, an MP4 video or a PDF document. - 402
storage_quotaThe media storage included in your plan is full and your plan does not bill extra gigabytes. - 429
rate_limitedMore than 30 uploads in a minute for this key and client.
curl -X POST https://nodium.io/api/v1/templates/samples \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "…", "mimeType": "…", "size": 0 }'{ "data": { "sampleId": "…", "kind": "image", "uploadUrl": "…" }}Confirm a header sample
POST/api/v1/templates/samples/{id}/confirm
Confirms the header sample you uploaded to the uploadUrl of POST /templates/samples. Nodium reads the stored file itself — its real size and type — and counts it against your media storage. Then cite the sampleId as headerMedia.sampleId; an unconfirmed sample is refused there and removed after a day. A file over the limits is removed and refused. Confirming again returns the same sample and counts it once.
Path parameters
idstringThe
sampleId.
Body
filenamestringThe file name, as shown with a document header.
Response
The sample, ready to cite in a template.
data.sampleIdstringPass it as
headerMedia.sampleIdwhen writing the template.data.kindstringThe header it makes.
imagevideodocumentdata.mimeTypestringIts media type, as stored.
data.filenamestringIts name.
data.sizeintegerIts size in bytes, as stored.
Refusals
- 404
not_foundNothing was uploaded under this id for this client. - 413
file_too_largeOver 5 MB for an image, 16 MB for a video, 100 MB for a document. - 415
file_type_refusedNot a JPEG or PNG image, an MP4 video or a PDF document. - 402
storage_quotaThe media storage included in your plan is full and your plan does not bill extra gigabytes.
curl -X POST https://nodium.io/api/v1/templates/samples/<id>/confirm \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "…" }'{ "data": { "sampleId": "…", "kind": "image", "mimeType": "…", "filename": "…", "size": 0 }}