- Proxy / always fresh: read endpoints have no caching — data comes straight from the source (Meta Graph API / ClickHouse), so you don’t deal with Meta’s token/credential complexity.
- Org-scoped & secure: every request is isolated to the organization resolved from your secret key; you cannot reach another organization’s data.
- Simple contract: all endpoints share the same response envelope, error model, pagination, and
Preferbehavior.
Authentication
Secret key authentication for the Public API.
Templates
Create, list, inspect, update, and delete message templates.
Business Profile
Read and update the WhatsApp business profile.
Analytics
Message volume: summary cards + time series.
Media
Upload media and get the single-use handle for template headers and the profile picture.
Developers
Configure webhooks and create API keys for an application.
Error Codes
Every code the Public API can return, and how to handle each class.
Base URL
All Public API paths in this reference are relative to a single base URL:/wa/{app_id}/templates means {base_url}/api/v1/wa/{app_id}/templates.
On-premises installations are served from your own domain and carry an extra
/public segment: https://{your_domain}/public/api/v1/wa/{app_id}/templates. Everything else — authentication, request bodies, responses, error codes — is identical.Authentication
Every endpoint in this reference requires a secret key:- Use it server-to-server; never embed the secret key in a browser/client.
organization_idis resolved automatically from the key — you don’t pass it.- The
app_idyou operate on must belong to your organization, otherwise you get404.
Shared conventions
These apply to every endpoint:- Response envelope — success:
{ "data": ..., "isSuccess": true }; error:{ "errors": { "code", "group", "description" }, "isSuccess": false }, plusfieldson field-validation errors — detailed in Error envelope below. Preferheader (on updates) —return=representation(default, returns the current record) orreturn=minimal(no body). The response reports it viaPreference-Applied.- Cursor pagination (on lists) —
pagingMetadata.nextCursor/previousCursor+hasNext/hasPrevious/pageSize. - Dates — ISO-8601 (
2026-06-08T00:00:00). - Null fields are dropped from responses (e.g. an unset profile field, or
componentswithoutexpand).
Error envelope
Every failure shares one shape.code identifies the error, group classifies it, and description is a human-readable message with any params already interpolated into it.
string
The error code —
VAL_*, WA_*, META_*, APPLICATION_*, ORGANIZATION_SECRET_*, REQ_001, or SYS_001.string
The category:
VALIDATION, NOT_FOUND, CONFLICT, AUTHENTICATION, NOT_ALLOWED, LIMIT_EXCEEDED, SERVICE_UNAVAILABLE, or SYSTEM.string
The message, with
params already substituted in.object
The raw values that were interpolated into
description (e.g. { "max": 2 }). Present only when the message is parametric — useful if you localise messages yourself.object
Field-level validation errors only. A map of field name →
{ code, description, params }. Absent on every other kind of error.Field validation: 422 with errors.fields
When one or more submitted values break a rule, the response is 422 with code: "VAL_000" and a fields map naming each offending field. Nested fields are dot-joined (body.text), and list indices are omitted.
Malformed request: 400 with REQ_001
A missing required field, an unparseable number or UUID, or invalid JSON is answered with 400 and REQ_001 — not 422, and with no fields map, because the request could not be read well enough to validate it.
The one exception: if the missing field carries its own error code, it is reported as a normal
422 field error instead.Generic validation codes
Standard constraint failures (VAL_000–VAL_019) appear inside errors.fields[...].code on any endpoint. The full list, together with every other code the Public API can return, is on Error Codes. Endpoint pages list only their own domain-specific codes.
Endpoint quick reference
Path parameters are shown in{braces}; query parameters are listed separately so the route stays readable.
/wa/{app_id}/templates/metrics is matched before /wa/{app_id}/templates/{source_id}, so metrics is never treated as a template ID.Media upload is the one endpoint that is not JSON. It takes a
multipart/form-data body with a single file part; everything else on this list sends and receives JSON.Quickstart
An end-to-end walkthrough. Every request below uses the headerAuthorization: Bearer sk_... and a path relative to the Base URL given above.
Step 1 — See the current state (analytics)
summary (total / sent / received / template messages) plus a daily series. Omit appId for the whole organization, or repeat it (?appId=a&appId=b) for specific apps. See Get Messages.
Step 2 — Create a template
PENDING (under review). See Create Template.
Step 3 — List templates / track status
status / category / language, search with search, and page with the returned cursors. See Get Templates.
Step 4 — Inspect a template (with components)
expand=components adds the header/body/footer/buttons detail. See Get Template Detail.
Step 5 — Update an APPROVED template (send the full set!)
body), or the missing ones are removed. After a successful edit the status returns to PENDING. See Update Template.
Step 6 — Update the business profile
Step 7 — Measure how the template performed
summary, a per-template summary, and a gap-filled daily series with deliveredRate, readRate, clickRate, and spend already computed. Repeat templateId for up to 10 templates, and keep the range within 90 days. Days Meta has not aggregated yet come back zeroed with isPending: true. See Get Template Metrics.
Step 8 — Retire a template
source_id, and it cannot be undone — re-creating the template starts a fresh review and yields a new sourceId. See Delete Template.
Common errors & tips
Next steps
Templates
The full template lifecycle.
Business Profile
Read and update the business profile.
Analytics
Message volume analytics.
Template Metrics
Per-template delivery, read, click, and cost metrics.
Media
Upload media for template headers and the profile picture.