/v1 behaviours you’d otherwise find out the hard way.
Read state, not status
Conversation reads return the lifecycle field twice:
state is canonical. status is a deprecated alias, kept so integrations built before the rename keep working.
The write side has already moved on. PATCH /v1/conversations/{id} accepts state only — sending status in the body does nothing and the request is rejected for having no state. Its response returns state and previous_state, with no status alias at all.
The values Paige itself uses are unstructured, handover, awaiting_form and done. A project’s own bot code can introduce others, and those are accepted too; a value the project has never used is rejected with INVALID_STATE and a message listing the ones that are known.
Message ids and wamid
A successful POST /v1/messages returns:
id is the real WhatsApp message id — the wamid Meta assigned — not a Paige placeholder and not a locally generated stub. It’s the same identifier WhatsApp uses in its own delivery callbacks, so you can store it and track the message from the moment the call returns.
GET /v1/messages/{id} accepts either form:
- the
wamidyou got back from the send, or - the numeric message id from a conversation’s message list.
wamid.
The lookup echoes back the numeric message id in its
id field, whichever form you queried with. Don’t compare the id you sent with the id you got back and conclude you’ve been given the wrong message — key your own records on the wamid from the send.Sends land in the conversation
Both kinds of send become part of the thread.Template sends
A template sent through the API is written into the conversation like any other message, and the conversation moves to the top of the list as recently active. Your team sees the same history in the Conversations tab whether the message came from the dashboard, from your bot, or from your own code calling/v1.
Session sends
Free-form (session) sends are platform-native. Paige delivers them itself rather than routing them through your bot, which has three consequences worth knowing:No redeploy
Sending through the API doesn’t depend on your bot’s code and doesn’t need a deploy. It works on a project you haven’t touched in months.
They persist
The message is stored on the conversation with its
wamid, so it appears in the thread and in GET /v1/conversations/{id}/messages.They're trackable
Because the row is keyed on the real
wamid, WhatsApp’s delivery and read callbacks attach to it. GET /v1/messages/{id} reflects the current status.No surprise handover
Sending no longer flips the conversation into human handover as a side effect. A send is just a send — if you want the bot to stop replying, change the state deliberately.
These behaviours describe standard Paige-hosted projects. A project running its own self-hosted backend handles delivery and storage on its own side, so what comes back from a send is whatever that backend reports.
Protected files
PUT /v1/files/{path} will refuse to write to some files and return protected_file with HTTP 403. The rejection happens before anything is touched, so the file is guaranteed unchanged.
The protected set is the platform’s own wiring rather than your bot’s logic:
- the app’s entry point and dependency manifest
- the WhatsApp webhook and send endpoints
- the runtime helpers Paige injects into every project — messaging, media, identifiers, logging, connector clients
- auto-generated manifests, which are rewritten from your project’s real state and would be silently overwritten anyway
- Paige’s own internal project directory
Contact custom attributes
PATCH /v1/contacts/:id accepts an attributes object — custom CRM fields you define, stored per contact.
null. Nested objects and arrays are rejected — keep values flat.
Attributes merge. You send only the keys you’re changing. Keys you don’t mention are left alone, so a partial update is safe and there’s no read-modify-write dance.
At least one field is required. The request must contain at least one of name, tags or attributes. An empty body is rejected rather than treated as a no-op.
Custom attributes are API-only today. There is no dashboard UI for them — you can’t view, add, or edit a contact’s attributes anywhere in Paige’s interface, and the contacts CSV import doesn’t carry them either (it accepts
number, name, tags and opted_in only). The PATCH response returns the contact’s full attributes after the merge, so that’s where you read them back; treat your own system as the place a human looks at them. Tags, by contrast, are visible and usable throughout the dashboard.