Notifications
Send an SMS when something happens to an order — booked, on its way, delivered.
Three objects, created independently and joined by a subscription:
| Object | Decides |
|---|---|
| Channel | where a message goes. Carries the recipient. |
| Template | what it says. |
| Subscription | when — ties a channel and a template to an event, and switches the whole thing on or off. |
All three are owner-scoped: you only ever see and send for your own project.
The one thing to get right
A channel's type_config.to is a Handlebars template, rendered against the order at
send time. It is the single most common misconfiguration, because it fails silently:
- an empty
torenders to an empty recipient, and the message is dropped - a path that does not exist renders to an empty string — Handlebars does not raise
Neither produces an error you will see. If a notification "did not arrive", check to
first.
A literal number is also valid, and does exactly what it says: every notification on that channel goes to that one number. That is occasionally what you want — an internal alert — and almost never what you want for a customer notification.
Creating a channel
POST /v1/channels
Authorization: Bearer <token>
{
"name": "SMS to recipient",
"type": "zoopit-sms",
"type_config": {
"to": "{{delivery.phone_number}}"
},
"owner": "<project id>"
}
type is zoopit-sms. That is the only channel type today.
Template variables
Rendered against the order, with its pickup and delivery locations resolved. The same
variables work in to, in the template subject and in its content.
| Variable | Is |
|---|---|
{{order.id}} | The order's id — use it to build a tracking link |
{{order.reference_id}} | Your own reference, carried through unchanged |
{{delivery.name}} | Recipient name |
{{delivery.phone_number}} | Recipient phone number |
{{delivery.street_address}}, {{delivery.zip_code}}, {{delivery.city}} | Delivery address |
{{pickup.name}} | Sender / pickup point name |
{{pickup.phone_number}} | Sender phone number |
delivery and pickup are the first place in the order's delivery.oneOf and
pickup.oneOf. The full paths, such as {{order.delivery.oneOf.[0].location.phone_number}},
also work; use them with another index for an order that offers several places.
delivery.oneOf, not from tasksPlanning works from the order's tasks, but templates read the order's delivery and
pickup. An order created with tasks only renders {{delivery.phone_number}} empty, and
gets no SMS. Put the recipient in delivery.oneOf as well — see
Creating orders.
Creating a template
POST /v1/templates
{
"name": "On its way",
"subject": "",
"content": "Your delivery is on its way. Follow it here: https://<your tracking host>/{{order.id}}",
"content_type": "text/plain",
"owner": "<project id>"
}
content_type is a free-text field; text/plain is the convention for SMS.
subject and content are joined with a newline into the SMS body. For SMS, leave
subject empty and put everything in content — a two-line message costs the same as a
one-line one only until it crosses a segment boundary.
Message length
A GSM-7 SMS is 160 characters, or 153 per part once it splits into several.
æ, ø and å are in the standard set and cost one character each, like any letter.
One character outside that set — a curly apostrophe ’, an em dash —, an emoji —
switches the whole message to UCS-2, where the limit is 70 characters. If a message
splits unexpectedly, look for a smart quote before you look at the length.
Creating a subscription
POST /v1/subscriptions
{
"template_id": "<template id>",
"channel_id": "<channel id>",
"event": "orders.reserved",
"enabled": true,
"owner": "<project id>"
}
enabled: false keeps the wiring in place without sending. That is the safe way to set a
flow up and review it before it reaches anyone.
Events
| Event | Fires when |
|---|---|
orders.reserved | An order is created as RESERVED (the default), or an update sets status to RESERVED |
orders.delivered | An update sets task_status to DELIVERY_COMPLETED — normally the driver completing the delivery |
orders.routed | A route is saved that moves the order into ROUTED — for example a driver scans orders and creates the route in the Go app, or a dispatcher saves a route |
An event fires on the change, not on the state: editing an order that is already
RESERVED does not send the booking message again.
orders.routed fires once, when the route save moves the order into ROUTED. It does not
fire when an already-routed order is moved to another route, or when an order reaches
ROUTED any other way than a route save. Use it for a message that the delivery is on its
way today. Subscriptions use the same channel, template and variables as the other events.
Before you switch one on
Real messages cost real money and reach real people. A short checklist:
- Render
toagainst a real order and confirm you get a number, not an empty string. - Create the subscription with
enabled: false, check the channel and template read the way you expect, then enable it. - Count the characters of the finished message, including whatever
{{order.reference_id}}expands to at its longest. - Check who the recipient actually is. A literal number in
tosends every message on that channel to one person.
What is not here yet
There is no delivery log. The API cannot currently tell you what was sent, to whom, or when — so build your own record if you need one, and treat an enabled subscription as something to verify rather than something to assume.
There is also no test-send endpoint. Any send is a real one.
Messages are sent through Zoopit's SMS provider and sender. Sending through your own SMS gateway or provider account is coming.