Xulo Docs

REST API

Send notification

POST /api/v1/notifications sends a push to one audience of one app, now or at a set time.

HTTP
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:

JSON
{
  "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:

JSON
{
  "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.