Numbers
A number (a “channel”) is a WhatsApp line connected to your account. Connecting one is a fail-closed flow: you declare the number, render the QR we return and scan it from that phone, and Sendveo reads the paired number back before ever marking it connected. The QR is yours to display: there is no external page and no redirect.
The number object
{
"id": "chn_sales01",
"displayName": "Sales line",
"declaredNumber": "+212661234567",
"pairedNumber": "+212661234567",
"status": "CONNECTED",
"connectedAt": "2026-08-19T09:12:00Z",
"lastReconnectAt": null,
"disconnectReason": null,
"createdAt": "2026-08-16T10:00:00Z",
"updatedAt": "2026-09-09T14:24:00Z"
}Status lifecycle
Every number reports one of seven states. Read the status, not the presence of a paired number, to decide what to do next.
| Status | Meaning |
|---|---|
| Pending pairing | QR issued, waiting for the scan. |
| Connecting | Handshake in progress: normal, not a failure. |
| Connected | Live and billable; sending and receiving. |
| Reconnect needed | Session ended: reconnect the number. |
| Unverified | Scanned but the read-back failed: retry. |
| Wrong number | A different number paired: start over. |
| Disconnected | Not active. |
Connect a number
Create the channel with the number you intend to connect. You get back a qr object: qr.data is a string to encode and display as a QR code (use any QR library), and qr.expiresAt tells you when to fetch a fresh one. The number stays PENDING_PAIRING until it is scanned.
curl -X POST https://api.sendveo.com/v1/numbers \
-H "X-API-KEY: sv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "declaredNumber": "+212661234567", "displayName": "Sales line" }'{
"channel": { "id": "chn_sales01", "status": "PENDING_PAIRING", "declaredNumber": "+212661234567" },
"qr": {
"data": "2@r1x8...,K3f...,c9...,a2...",
"expiresAt": "2026-09-09T14:24:20Z"
}
}Refresh the QR
WhatsApp pairing codes rotate every few seconds. While the number is still PENDING_PAIRING, fetch a fresh qr at (or just before) expiresAt. Once the number is no longer awaiting a scan, this returns 409.
{
"channel": { "id": "chn_sales01", "status": "PENDING_PAIRING", "declaredNumber": "+212661234567" },
"qr": { "data": "2@n7q2...,P0d...,e4...,b8...", "expiresAt": "2026-09-09T14:24:40Z" }
}Verify the pairing
A scan alone is not a verified connection. Poll verify on a timer while the number is pending: Sendveo holds (returning the number unchanged) until the session settles, then reads the paired number back and promotes fail-closed: it becomes CONNECTED only if the scanned number matches the one you declared, otherwise WRONG_NUMBER_SCANNED or PAIRING_UNVERIFIED.
curl -X POST https://api.sendveo.com/v1/numbers/chn_sales01/verify \
-H "X-API-KEY: sv_live_your_key_here"Reconnect & disconnect
If a session later dies, the number moves to CREDENTIALS. Reconnect for a fresh qr to display. Disconnect stops the line immediately.
Health
Every number carries a health object alongside its status. lastSeenAt is when the messaging service last reported the phone reachable, and lastEventAt is when the newest health event happened (its own occurredAt, not when we stored it).
{
"id": "chn_sales01",
"declaredNumber": "+212661234567",
"status": "CONNECTED",
"health": {
"phoneOnline": null,
"lastSeenAt": null,
"lastEventAt": "2026-09-16T10:05:00Z"
}
}phoneOnline has three values, and null is not false. null means we have never been told anything about the handset, which is the state of most numbers: there is no dedicated phone-reachability signal to read, and Sendveo will not infer one from the session status. Render true as online, false as offline, and null as nothing at all. A dashboard that draws null as offline puts a warning on every healthy number it has.
Losing the session and losing the handset are different incidents with different answers. A dead session needs a person and a new scan; a phone on charge in the next room needs nothing. They are separate events, separate rows and separate fields for that reason.
Recent changes
The ledger of what has happened to one number, newest first. type is the same name the webhook and the dashboard stream use, so a handler written for the webhook reads this list unchanged. limit is 1 to 200 and defaults to 50.
{
"numberId": "chn_sales01",
"events": [
{ "id": "cev_m8k3x7q2", "numberId": "chn_sales01", "type": "number.reconnected",
"reason": null, "occurredAt": "2026-09-16T10:12:00Z" },
{ "id": "cev_m8k3wp41", "numberId": "chn_sales01", "type": "number.disconnected",
"reason": "SESSION_EXPIRED", "occurredAt": "2026-09-16T10:05:00Z" }
],
"cursor": null
}reason rides only on number.disconnected, and it is Sendveo’s own vocabulary rather than the messaging service’s status word: SESSION_EXPIRED (the link died and a new scan is needed) or REMOVED_UPSTREAM (the account was deleted on WhatsApp).
Paging is keyset, not offset: a ledger grows at the front, so an offset page two would re-show rows the moment a new event landed between the two requests. Pass the previous page’s last event id back as cursor. A full page offers a cursor; a short page is the end and offers null. A cursor that is not this number’s is 400 VALIDATION_FAILED rather than a silent first page.
Nothing is recorded for a number that has never finished linking: a failure there is a pairing that did not work, not a line going down. No event repeats itself, so a number does not look like it dropped three times because one webhook arrived three times. And the first time we are told a phone is reachable, nothing is announced, because a recovery from a fault nobody saw reads as noise.
The line profile
What your own line publishes about itself: the name your customers see, the status line under it, whether it is a Business account, and its picture. The reason to ask is concrete: you connected a number you were told is the sales line, and you want to see from Sendveo that it really is the account you think it is before pointing an integration at it.
{
"profile": {
"numberId": "chn_sales01",
"phone": "+212661234567",
"displayName": "Atlas Traiteur",
"about": "Deliveries 7 days a week",
"business": true,
"pictureUrl": "https://api.sendveo.com/v1/numbers/chn_sales01/picture",
"editable": false
}
}It is read-only, and editable: false says so on the resource. There is no PATCH, PUT or POST here and those paths are 404: the messaging service publishes no profile-write verb for a linked device, and an endpoint that either failed every time or succeeded and changed nothing would be worse than saying this plainly. Change the profile in WhatsApp on the phone.
phone is the verified paired number, echoed from what the read-back confirmed. pictureUrl is a Sendveo URL addressed by the channel id rather than by the phone number, so your own line never ends up in a browser history, a Referer or a proxy log. It is null when the line publishes no picture, and the picture route answers 404 for a line with none, a line hiding it, and an upstream hiccup alike.
A number that is not connected, or not yet verified, answers 409 CONFLICT.