Appio helps businesses add widgets and notifications to their products, without building or maintaining mobile apps, hiring developers, or dealing with app stores.
Learn more about Appio, try our interactive demo, or explore our case studies.
Found an issue or have feedback? Let us know!
Before getting started, you'll need to create an account and obtain your service_id.
There are two integration options for Appio, both functionally identical.
Preview
Install code
<script src="https://cdn.appio.so/v1/appio.js"></script>
<script>
const appio = Appio({
service: "svc_00dddddd000000ccccccssssss"
})
function mobileApp() {
appio.open({
user: "23d0e9848fe0ad06272dea39a03679ff"
})
}
</script>
<a href="#" onclick="mobileApp()">Mobile APP</a>Preview
Install code
<script src="https://cdn.appio.so/v1/appio.js"></script>
<a
data-appio="on"
data-service="svc_00dddddd000000ccccccssssss"
data-user="23d0e9848fe0ad06272dea39a03679ff"
href="#"
>
Mobile APP
</a>All commands in this documentation are live and ready for testing.
NOTE:
Every 15 minutes the seeded data is restored and anything created more than 30 minutes earlier is removed.
This also removes the "Appio Docs" service from your device after 30 minutes.
All API requests must include an API key in the Authorization: Bearer <api-key> header.
Keys created in the dashboard start with prod_, the key in these examples with docs_.
Every /v1 endpoint documented here, except viewing a service, also requires the X-Service-Id header, set to a service the key has access to.
You can validate your token by calling the testing endpoint:
Request
curl https://api.appio.so/hi \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A"Response
HTTP Code: 200"👋"The default token in these examples belongs to the "Appio Docs" service.
Reads work, but every PATCH and DELETE, and any POST on data sources or widget templates, returns 403.
Use your own token for those.
Returns the details of a single service.
The {id} path parameter names the service, so no X-Service-Id header is needed.
Path
GET /v1/services/{id}Request
curl -X GET https://api.appio.so/v1/services/svc_00dddddd000000ccccccssssss \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A"Response
HTTP Code: 200{
"id": "svc_00dddddd000000ccccccssssss",
"title": "Appio Docs",
"description": "Try out the functionality of Appio by downloading the app.",
"logo_url": "https://cdn.appio.so/app/docs.appio.so/logo.png",
"banner_url": "https://cdn.appio.so/app/docs.appio.so/banner.jpg",
"url": "https://docs.appio.so/",
"text_color": "",
"background_color": "",
"accent_color": ""
}Returns a paginated list of devices subscribed to the service.
platform is one of ios, android or watchos.
pagination.next is the path of the next page, or null on the last page.
Path
GET /v1/devicesRequest
curl -X GET https://api.appio.so/v1/devices \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"data": [
{
"id": "dvc_01jmpmh9fvxgyym44sqanjr9hs",
"user_id": "23d0e9848fe0ad06272dea39a03679ff",
"name": "iPhone 13",
"platform": "ios",
"os_version": "18.3",
"model": "iPhone",
"device_token": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"notifications_enabled": true,
"device_identifier": "iPhone14,5",
"marketing_name": "iPhone 13"
},
...
],
"pagination": {
"next": "/v1/devices?after=dvc_01jmpmh9fvxgyym44sqanjr9hs&limit=50"
}
}Path
GET /v1/devices/{id}Request
curl -X GET https://api.appio.so/v1/devices/dvc_01jmpmh9fvxgyym44sqanjr9hs \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "dvc_01jmpmh9fvxgyym44sqanjr9hs",
"user_id": "23d0e9848fe0ad06272dea39a03679ff",
"name": "iPhone 13",
"platform": "ios",
"os_version": "18.3",
"model": "iPhone",
"device_token": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"notifications_enabled": true,
"device_identifier": "iPhone14,5",
"marketing_name": "iPhone 13"
}Deactivate a device when it should no longer receive notifications or when its associated user has been deactivated.
Path
DELETE /v1/devices/{id}Request
curl -X DELETE https://api.appio.so/v1/devices/dvc_01jmpmh9fvxgyym44sqanjr9hs \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "dvc_01jmpmh9fvxgyym44sqanjr9hs"
}Deactivate all of a user’s devices when they should no longer receive notifications or when the user has been deactivated.
The devices are unlinked from this service only; their links to other services are left alone.
Returns an empty array when the user has no devices.
Path
DELETE /v1/devices?user_id={user_id}Request
curl -X DELETE https://api.appio.so/v1/devices?user_id=23d0e9848fe0ad06272dea39a03679ff \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200[
{
"id": "dvc_01jmpmh9fvxgyym44sqanjr9hs"
},
...
]Text limits count bytes, not characters, so emoji and accented text hit them early.
If both device_id and user_id are provided, only device_id is used.
pagination.next is the path of the next page, or null on the last page.
Path
GET /v1/notificationsRequest
curl -X GET https://api.appio.so/v1/notifications \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"data": [
{
"id": "ntf_01jmpmgb6my0s57c960q1s862v",
"service_id": "svc_00dddddd000000ccccccssssss",
"status": "completed",
"payload": {
"link": "https://docs.appio.so/",
"title": "Notification",
"message": "Hello from Appio Docs",
"image_url": "https://cdn.appio.so/app/docs.appio.so/banner.jpg"
},
"scheduled_at": "2025-03-24T14:23:37.016526Z"
},
...
],
"pagination": {
"next": "/v1/notifications?after=ntf_01jmpmgb6my0s57c960q1s862v&limit=50"
}
}Path
GET /v1/notifications?status=completedRequest
curl -X GET https://api.appio.so/v1/notifications?status=completed \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"data": [
{
"id": "ntf_01jmpmgb6my0s57c960q1s862v",
"service_id": "svc_00dddddd000000ccccccssssss",
"status": "completed",
"payload": {
"link": "https://docs.appio.so/",
"title": "Notification",
"message": "Hello from Appio Docs",
"image_url": "https://cdn.appio.so/app/docs.appio.so/banner.jpg"
},
"scheduled_at": "2025-03-24T14:23:37.016526Z"
},
...
],
"pagination": {
"next": "/v1/notifications?after=ntf_01jmpmgb6my0s57c960q1s862v&limit=50&status=completed"
}
}Path
GET /v1/notifications?device_id={device_id}Request
curl -X GET https://api.appio.so/v1/notifications?device_id=dvc_01jmpmh9fvxgyym44sqanjr9hs \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"data": [
{
"id": "ntf_01jmpmgb6my0s57c960q1s862v",
"service_id": "svc_00dddddd000000ccccccssssss",
"status": "completed",
"payload": {
"link": "https://docs.appio.so/",
"title": "Notification",
"message": "Hello from Appio Docs",
"image_url": "https://cdn.appio.so/app/docs.appio.so/banner.jpg"
},
"scheduled_at": "2025-03-24T14:23:37.016526Z"
},
...
],
"pagination": {
"next": "/v1/notifications?after=ntf_01jmpmgb6my0s57c960q1s862v&device_id=dvc_01jmpmh9fvxgyym44sqanjr9hs&limit=50"
}
}Path
GET /v1/notifications?user_id={user_id}Request
curl -X GET https://api.appio.so/v1/notifications?user_id=23d0e9848fe0ad06272dea39a03679ff \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"data": [
{
"id": "ntf_01jmpmgb6my0s57c960q1s862v",
"service_id": "svc_00dddddd000000ccccccssssss",
"status": "completed",
"payload": {
"link": "https://docs.appio.so/",
"title": "Notification",
"message": "Hello from Appio Docs",
"image_url": "https://cdn.appio.so/app/docs.appio.so/banner.jpg"
},
"scheduled_at": "2025-03-24T14:23:37.016526Z"
},
...
],
"pagination": {
"next": "/v1/notifications?after=ntf_01jmpmgb6my0s57c960q1s862v&limit=50&user_id=23d0e9848fe0ad06272dea39a03679ff"
}
}View a notification's details, including its sending status.
Each notification may be delivered to multiple devices.
Path
GET /v1/notifications/{id}Request
curl -X GET https://api.appio.so/v1/notifications/ntf_01jmpmgb6my0s57c960q1s862v \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "ntf_01jmpmgb6my0s57c960q1s862v",
"service_id": "svc_00dddddd000000ccccccssssss",
"status": "completed",
"payload": {
"link": "https://docs.appio.so/",
"title": "Notification",
"message": "Hello from Appio Docs",
"image_url": "https://cdn.appio.so/app/docs.appio.so/banner.jpg"
},
"scheduled_at": "2025-03-24T14:23:37.016526Z",
"delivery_stats": {
"total": 1,
"created": 0,
"queued": 0,
"completed": 1,
"failed": 0,
"skipped": 0
}
}The audience of POST /v1/notifications is chosen by two optional query parameters:
Omitting both broadcasts the notification to every device of the service. This is the default behaviour.
If both are given, device_id is used and user_id is ignored.
Notification is delivered only to eligible devices: devices that are linked to the service, have notifications enabled, and have a push token.
Targeted notification (device_id / user_id) sent to an empty audience returns 404 and creates nothing.
A broadcast to a service with no eligible devices is still created and completes with zero deliveries.
Sent to every eligible device linked to the service.
A broadcast is fanned out by the queueing cron, which does not pick the notification up before scheduled_at has passed. Until then it reports status: created and has no deliveries; it becomes queued once every per-device delivery exists.
Path
POST /v1/notificationsRequest
curl -X POST https://api.appio.so/v1/notifications \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"payload":{"title":"Notification","message":"Hello from Appio Docs"}}'Request body
{
"payload": {
"title": "Notification",
"message": "Hello from Appio Docs"
}
}Response
HTTP Code: 201{
"id": "ntf_01jv7938vhvsccakmeq7c1hcy8"
}Sends a notification to all eligible devices belonging to this user.
Returns 404, and creates nothing, if the user is unknown to the service or none of their devices can receive a push.
The per-device deliveries are created immediately, so the notification reports status: queued from creation onwards; the deliveries are simply held back until scheduled_at.
Path
POST /v1/notifications?user_id={user_id}Request
curl -X POST https://api.appio.so/v1/notifications?user_id=23d0e9848fe0ad06272dea39a03679ff \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"payload":{"title":"Notification","message":"Hello from Appio Docs"},"scheduled_at":""}'Request body
{
"payload": {
"title": "Notification",
"message": "Hello from Appio Docs"
},
"scheduled_at": ""
}Response
HTTP Code: 201{
"id": "ntf_01jv7akwwbj47rf3je11jgpvvf"
}Sends a notification to one device.
Returns 404, and creates nothing, if the device is unknown to the service or cannot receive a push (notifications disabled, no push token).
The delivery is created immediately, so the notification reports status: queued from creation onwards.
Path
POST /v1/notifications?device_id={device_id}Request
curl -X POST https://api.appio.so/v1/notifications?device_id=dvc_01jmpmh9fvxgyym44sqanjr9hs \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"payload":{"title":"Notification","message":"Hello from Appio Docs"}}'Request body
{
"payload": {
"title": "Notification",
"message": "Hello from Appio Docs"
}
}Response
HTTP Code: 201{
"id": "ntf_01jmpmgb6my0s57c960q1s862v"
}POST /v1/notifications), and saving or previewing a data source whose URL host is not localhost, 127.0.0.1 or api.appio.so, require one. Subscribe at the URL in data.url of the error response, my.appio.so/pricing.PATCH and DELETE, and any POST on data sources or widget templates, returns Not allowed on the Appio Docs service, use your production API key.Every error uses the same body shape.
{
"error": {
"message": "Invalid input data",
"data": {
"doc_url": "https://docs.appio.so/#api-services",
"entity": "service",
"validation_errors": [
{
"field": "title",
"reason": "title is required"
}
]
}
}
}LLM-friendly content for AI agents is available at /llms.txt.
OpenAPI definition is available at /openapi-v1.yaml.
It covers the whole API: services, devices, notifications, widgets, widget templates, data sources and their snapshots, image upload and billing status.
The sections above document the subset most integrations need.