REST API
Send notification
POST /api/v1/notifications sends a push to one audience of one app, now or at a set time.
POST https://api.xulo.io/api/v1/notifications
Sends a push to one app. Without send_after the send starts at once and finishes in the background. With send_after it sends when that time arrives. There is no approval step.
Request
Headers:
| Header | Value |
|---|---|
Authorization |
Key followed by the app's REST API key. See Authentication. |
Content-Type |
application/json |
Idempotency-Key |
Optional. See Idempotency. |
Body:
| Field | Type | Notes |
|---|---|---|
app_id |
string | Required. The App ID. It must belong to the key's app. |
contents |
object | Required. The message per language code, such as {"en": "Hello", "tr": "Merhaba"}. Up to 2,000 characters. |
headings |
object | The title per language code. Up to 120 characters. |
included_segments |
array | One audience name. See Targeting. |
tag |
object | A tag to match. See Targeting. |
include_subscription_ids |
array | Up to 500 Subscription IDs. See Targeting. |
include_external_ids |
array | Up to 500 External IDs. See Targeting. |
send_after |
string | A future time. See Scheduling. |
delayed_option |
string | timezone, to send at the same clock time on each device. See Scheduling. |
url |
string | The link opened on tap: https, or an address your app handles such as exampleapp://offer/1. Up to 500 characters. app_url is accepted too. If both are sent, they must be the same. |
big_picture |
string | An https image address. ios_attachments with an id is accepted too. |
data |
object | A dictionary your app reads on tap. Values are sent as text. |
buttons |
array | At most one button, {"id": "track", "text": "Track"}. Id and text up to 40 characters each. |
bundle_id |
string | Optional. If present, it must be the app's bundle id. |
throttle_rate_per_minute |
integer | Optional, from 1 to 100,000. The most devices a minute a segment or tag send reaches. See Throttling a send. |
idempotency_key |
string | Optional. The same as the Idempotency-Key header. If both are sent, they must be the same. See Idempotency. |
Name exactly one target: included_segments, tag, include_subscription_ids or include_external_ids. None, or more than one, returns 422.
The default language is en when contents has it, otherwise the first language in contents. Each device gets its own language when there is a match. See Notifications.
Example
cURL
curl https://api.xulo.io/api/v1/notifications \
-H "Authorization: Key $XULO_REST_KEY" \
-H "Content-Type: application/json" \
-d '{
"app_id": "YOUR_APP_ID",
"headings": {"en": "Your order shipped"},
"contents": {"en": "It arrives tomorrow.", "tr": "Yarın teslim edilecek."},
"include_external_ids": ["USER_ID"],
"url": "exampleapp://orders/1",
"data": {"order": "1"}
}'
PHP
<?php
$payload = [
'app_id' => 'YOUR_APP_ID',
'headings' => ['en' => 'Your order shipped'],
'contents' => ['en' => 'It arrives tomorrow.', 'tr' => 'Yarın teslim edilecek.'],
'include_external_ids' => ['USER_ID'],
'url' => 'exampleapp://orders/1',
'data' => ['order' => '1'],
];
$curl = curl_init('https://api.xulo.io/api/v1/notifications');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Key '.getenv('XULO_REST_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$result = json_decode(curl_exec($curl), true);
Laravel
use Illuminate\Support\Facades\Http;
$result = Http::withHeaders([
'Authorization' => 'Key '.config('services.xulo.rest_key'),
])
->post('https://api.xulo.io/api/v1/notifications', [
'app_id' => 'YOUR_APP_ID',
'headings' => ['en' => 'Your order shipped'],
'contents' => ['en' => 'It arrives tomorrow.', 'tr' => 'Yarın teslim edilecek.'],
'include_external_ids' => ['USER_ID'],
'url' => 'exampleapp://orders/1',
'data' => ['order' => '1'],
])
->throw()
->json();
Node
const response = await fetch("https://api.xulo.io/api/v1/notifications", {
method: "POST",
headers: {
Authorization: `Key ${process.env.XULO_REST_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
app_id: "YOUR_APP_ID",
headings: { en: "Your order shipped" },
contents: { en: "It arrives tomorrow.", tr: "Yarın teslim edilecek." },
include_external_ids: ["USER_ID"],
url: "exampleapp://orders/1",
data: { order: "1" },
}),
});
const result = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.xulo.io/api/v1/notifications",
headers={"Authorization": f"Key {os.environ['XULO_REST_KEY']}"},
json={
"app_id": "YOUR_APP_ID",
"headings": {"en": "Your order shipped"},
"contents": {"en": "It arrives tomorrow.", "tr": "Yarın teslim edilecek."},
"include_external_ids": ["USER_ID"],
"url": "exampleapp://orders/1",
"data": {"order": "1"},
},
timeout=10,
)
response.raise_for_status()
result = response.json()
In Laravel, config('services.xulo.rest_key') stands for wherever your app keeps the key.
Response
Every call answers before Apple is called. The status code depends on the target.
A segment or a tag
A send to included_segments or tag with someone in its audience returns 202 Accepted. The send is recorded, and Xulo sends it in the background:
{
"id": 43,
"status": "queued",
"sent": 0,
"failed": 0,
"skipped": 0,
"estimate": 1200,
"message": "Queued for 1200 devices. It sends in the background."
}
Xulo splits the audience into chunks of 500 devices and sends them one after another. Read the progress with Notification stats: status moves from queued to sending, then to approved, or to draft if Apple accepted none of the devices.
One send reaches at most 1,000,000 devices. A larger audience returns 422 with "This send reaches 1,200,000 devices. One send reaches at most 1,000,000." and nothing is sent.
A list of ids
A send to include_subscription_ids or include_external_ids returns 200:
{
"id": 42,
"status": "sending",
"sent": 0,
"failed": 0,
"skipped": 0,
"estimate": 1,
"message": "Sending to 1 device."
}
Fields
| Field | Meaning |
|---|---|
id |
The id of this send. Use it with Notification stats. |
status |
queued, sending, approved, scheduled or draft. See below. |
sent |
Devices Apple accepted when the call answered. 0 while the send is queued or sending. |
failed |
Devices Apple refused. |
skipped |
Devices not sent to, such as a Subscription ID that matched no subscribed install. |
estimate |
Devices in the audience. |
message |
A sentence that says what happened. |
status |
Meaning |
|---|---|
queued |
A segment or tag send, accepted with 202. Xulo sends it in the background in chunks of 500. |
sending |
A list of ids, accepted. Xulo sends it in the background, 500 devices at a time, from a job that runs every minute. Read the counts with Notification stats. |
approved |
Handled now. With an empty audience, sent is 0 and the message says the send reached no one. |
scheduled |
Waiting for send_after. sent is 0. A send with delayed_option: "timezone" stays scheduled while each time zone goes out in the background in chunks. It does not use the list job above. See Scheduling. |
draft |
Not sent. The app has no Apple key or certificate, or Apple accepted none of the devices. The message says which. |
Check status and sent, not only the HTTP status. A call that reached nobody still returns 200. A queued or sending call has not reached anyone yet.
Throttling a send
throttle_rate_per_minute spreads a segment or tag send over time. With "throttle_rate_per_minute": 1000, the chunks are spaced so that about 1,000 devices get it each minute. Without it, the chunks go out one after another with no pause.
It applies to a segment or tag send made now or at a set time. A list of ids and a send with delayed_option: "timezone" do not use it. A value that is not a whole number from 1 to 100,000 returns 422.
Retrying safely
Send an Idempotency-Key header, or idempotency_key in the body, and a retry of the same call does not send twice. A repeat within 24 hours returns the first response with the header Idempotent-Replay: true. A repeat while the first call is still being handled returns 409. The same key with a different body returns 422. See Idempotency.
Delivery paused
If the app has an invoice past its due date, the call returns 402 with "Delivery is paused until the invoice for this app is paid." Nothing is recorded or sent.
If that happens while a segment or tag send is going out, the devices not reached yet are never sent, not even after the invoice is paid. The send ends as approved when some devices got it, or as draft when none did. See Notification stats.
Errors are listed on Errors.