Overview
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!
AI Instructions
This section is for AI agents and other automated clients. This documentation is also available as plain Markdown, including the widget template format, at /llms.txt.
Rules to follow when calling the API:
- The base URL is https://api.appio.so. Send Authorization: Bearer <api-key> on every request, and X-Service-Id: <service-id> on every /v1 endpoint except /v1/services and /v1/billing/status.
- You create your account, API key and subscription in the dashboard; they cannot be created through the API. The API key and service ID are shown there.
- Devices and users cannot be created through the API. A device appears once one of your users installs the Appio app from the Appio button on your website. user_id is the user value that button was given.
You choose that value for each of your users. Use a non-sequential, non-predictable string, and the same one every time for the same user: an HMAC-SHA-256 of your internal user ID with a secret kept on your server, or a random ID stored with the user. Do not use the internal ID itself, an email address alone or anything else that can be guessed. - POST /v1/notifications without device_id or user_id sends to every device of the service. Query parameters the endpoint does not know are ignored, so a misspelled ?userId= is a broadcast. Check the query string before sending.
- Sending a notification is not idempotent and cannot be undone: there is no idempotency key, and a notification cannot be edited, cancelled or deleted. Do not automatically retry a POST /v1/notifications that may have succeeded.
- Request bodies are JSON and strict: an unknown field returns 400. Text limits count UTF-8 bytes.
- template_ios, template_android and template_watchos are strings that contain JSON. The format differs per platform, see Widget Template Format.
The preferred way to create a widget template is the visual (WYSIWYG) editor in the dashboard, which writes all three layouts. Write the JSON by hand only when the template has to be created through the API. - List endpoints are paginated. Follow pagination.next until it is null.
- On 429 wait the number of seconds in the Retry-After header when it is present, otherwise wait a while, then try again. Using Appio needs a subscription, which you buy in the dashboard; without one requests return 402.
How Appio Works
Your users install one app, the Appio app (iOS, Android and Apple Watch), and link it to your service. From then on you can send them push notifications and show them home screen widgets through this API, without building a mobile app of your own.
Terms
- Service
- Your product as it appears in the Appio app: title, description, logo, banner and colors. Notifications, widgets and devices all belong to a service. An account can have several services.
- User
- One of your users. Appio stores no user records, only the identifier you choose and pass as user to the Appio button. The API calls it user_id.
- Device
- One installation of the Appio app that is linked to your service. A device belongs to one user of the service; a user can have several devices. An iPhone and its Apple Watch are two devices: the watch is linked automatically together with the iPhone, with the same user_id.
- Notification
- A push message sent to one device, to all devices of one user, or to all devices of the service.
- Widget template
- The layout of a widget, one JSON document per platform, with {{…}} placeholders for data. The preferred way to create one is the visual (WYSIWYG) editor in the dashboard.
- Data source
- A URL of yours that returns JSON. Appio requests it for each user and fills the template placeholders with the response.
- Widget
- A widget template combined with an optional data source. Once published, users can add it to their home screen.
- Data source snapshot
- A record of the last response a data source returned for one user. For inspection only.
Typical flow
- You create an account, a service and an API key in the dashboard.
- You add the Appio button to your website and give it the service ID and the ID of the signed-in user.
- Your user taps the button and installs the Appio app. Their device is now linked to your service and is listed by GET /v1/devices with that user_id.
- You send notifications to that user_id, to a single device, or to everyone.
- For widgets you create a widget template, a data source and a published widget that joins the two. The preferred way to create the widget template is the visual (WYSIWYG) editor in the dashboard.
- Your user adds the Appio widget to their home screen and picks your widget. Each time the widget refreshes, Appio requests your data source for that user, fills the template and sends the result to the device.
A device keeps the user it was first linked with. Linking the same device again with another user changes nothing until the device has been deactivated for the service.
Getting Started
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.
Option 1: JavaScript
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: "docs-appio-so:hi@appio.so"
})
}
</script>
<a href="#" onclick="mobileApp()">Mobile APP</a>Appio
- service
- Use your service ID, which was assigned to you during registration.
Replace svc_**YOUR*SERVICE*ID** with your service id. - eventCallback
- optional
Callback function to receive events from Appio. function (type, data)
Appio.open
Opens Appio for the given user. Call it from a click handler; a browser blocks the new tab otherwise.
- user
- A unique identifier for your user. This is the user_id of the API.
For security, use a non-sequential, non-predictable string, and use the same one every time for the same user.
Suggestion: an HMAC-SHA-256 of your internal user ID with a secret kept on your server, or a random ID stored with the user. - service
- optional
Use your service ID, which was assigned to you during registration.
Appio.close
Closes the Appio dialog. Takes no arguments.
Events
eventCallback is called with the event type and its data:
- appio.opened
- Appio was opened. data.mode is dialog (on a computer) or tab (on a phone).
- appio.closed
- The dialog was closed. No data.
- appio.error
- Appio could not be opened. data.reason is popup_blocked when the browser blocked the new tab.
- appio.loaded
- The Appio page finished loading. No data.
- appio.view
- The Appio page shows a screen. data is one of qr, ios, android or error.
- appio.device_connected
- A device was linked to the service while the QR code was shown. No data.
Option 2: HTML attributes
Preview
Install code
<script src="https://cdn.appio.so/v1/appio.js"></script>
<a
data-appio="on"
data-service="svc_00dddddd000000ccccccssssss"
data-user="docs-appio-so:hi@appio.so"
href="#"
>
Mobile APP
</a>- data-appio
- Activates Appio. The field is required, but its value is optional and can be omitted.
- data-service
- Use your service ID, which was assigned to you during registration.
Replace svc_**YOUR*SERVICE*ID** with your service id. - data-user
- A unique identifier for your user. This is the user_id of the API.
The same rules apply as for user in Option 1.
The element can be any HTML element, and elements added to the page later work as well.
API Overview
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.
Authentication
All API requests go to https://api.appio.so and must include an API key in the Authorization: Bearer <api-key> header.
API keys are created in the dashboard, not through the API.
Using Appio needs a subscription that is active or trialing; without one requests return 402.
Every /v1 endpoint, unless stated otherwise, 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 key in these examples
The examples in this documentation use a public API key that belongs to the "Appio Docs" service (svc_00dddddd000000ccccccssssss). Use your own API key for full access.
- Works
- Every GET, and POST on /v1/notifications, /v1/widgets and /v1/images.
POST /v1/images stores nothing and always returns the same placeholder URL. - Returns 403
- Every PATCH and DELETE, and every POST on services, data sources (including preview and snapshots) and widget templates.
Requests
Request bodies are JSON and are decoded strictly: a field the endpoint does not know returns 400.
Query parameters the endpoint does not know are ignored.
Text limits count UTF-8 bytes, not characters, so emoji and accented text hit them early.
Pagination
Every list endpoint returns its items in data, newest first, and accepts the same parameters:
- limit
- optional
Integer. Default: 50. Min: 1. Values above 100 are treated as 100.
Maximum number of items to return. A value below 1 or not an integer returns 400. - after
- optional
String. ID of the last item of the previous page.
Items are listed newest first, so this returns older items. A malformed ID returns 400.
pagination.next is the path of the next page, or null on the last page.
API Services
View service details
Returns the details of a single service.
The {id} path parameter names the service.
No X-Service-Id header is required.
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": ""
}List services
Returns a paginated list of the active services.
Accepts the limit and after pagination parameters.
No X-Service-Id header is required.
Path
GET /v1/servicesRequest
curl -X GET https://api.appio.so/v1/services \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A"Response
HTTP Code: 200{
"data": [
{
"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": ""
}
],
"pagination": {
"next": null
}
}Service fields
Body of create and update requests. A response contains every field; one that was never set is an empty string.
- title
- String. 1 to 255 bytes. Required for create.
- description
- optional
String. Max 1000 bytes. - logo_url
- optional
String. Max 512 bytes. Absolute http(s) URL, or "" to clear it. - banner_url
- optional
String. Max 512 bytes. Absolute http(s) URL, or "" to clear it. - url
- optional
String. Max 512 bytes. Absolute http(s) URL, or "" to clear it. - text_color
- optional
String. Color in format #RRGGBB, or "" to clear it. A color set by an update comes back lowercase. - background_color
- optional
String. Color in format #RRGGBB, or "" to clear it. - accent_color
- optional
String. Color in format #RRGGBB, or "" to clear it.
Create service
No X-Service-Id header is required.
Use your own API key.
Path
POST /v1/servicesRequest
curl -X POST https://api.appio.so/v1/services \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-d '{"title":"My Service","description":"A service for my application"}'Request body
{
"title": "My Service",
"description": "A service for my application"
}Response
HTTP Code: 201{
"id": "svc_01jv7938vhvsccakmeq7c1hcy8"
}Update service
Every field is optional, but at least one must be present: a body with none returns 400.
No X-Service-Id header is required.
Use your own API key.
Path
PATCH /v1/services/{id}Request
curl -X PATCH https://api.appio.so/v1/services/svc_00dddddd000000ccccccssssss \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-d '{"title":"Updated Service Name","accent_color":"#FF5733"}'Request body
{
"title": "Updated Service Name",
"accent_color": "#FF5733"
}Response
HTTP Code: 200{
"id": "svc_00dddddd000000ccccccssssss"
}Deactivate service
No X-Service-Id header is required.
Use your own API key.
Path
DELETE /v1/services/{id}Request
curl -X DELETE https://api.appio.so/v1/services/svc_00dddddd000000ccccccssssss \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A"Response
HTTP Code: 200{
"id": "svc_00dddddd000000ccccccssssss"
}API Devices
A device is one installation of the Appio app linked to the service. Devices cannot be created through the API: a device appears when a user installs the Appio app from the Appio button.
user_id is the user value the button was given. notifications_enabled tells whether the user allows notifications on that device; a device with false receives none.
List devices subscribed to service
Returns a paginated list of devices subscribed to the service.
platform is one of ios, android or watchos.
Accepts the limit and after pagination parameters.
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": "docs-appio-so:hi@appio.so",
"name": "Custom user name",
"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"
}
}View device details
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": "docs-appio-so:hi@appio.so",
"name": "Custom user name",
"platform": "ios",
"os_version": "18.3",
"model": "iPhone",
"device_token": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"notifications_enabled": true,
"device_identifier": "iPhone14,5",
"marketing_name": "iPhone 13"
}Deactivate device
Deactivate a device when it should no longer receive notifications or when its associated user has been deactivated.
The device is unlinked from this service only. The user can link it again with the Appio button.
Everything for that device stops: it receives no notifications, also ones that were already scheduled, and its widgets are no longer updated.
Use your own API key.
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 user devices
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.
user_id is required: without it the request returns 404 and changes nothing.
Use your own API key.
Path
DELETE /v1/devices?user_id={user_id}Request
curl -X DELETE https://api.appio.so/v1/devices?user_id=docs-appio-so:hi@appio.so \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200[
{
"id": "dvc_01jmpmh9fvxgyym44sqanjr9hs"
},
...
]API Notifications
A notification is a push message. It is sent to one device, to all devices of one user, or to all devices of the service, see Target options.
Sending is asynchronous: POST /v1/notifications returns the ID at once, and GET /v1/notifications/{id} reports the progress.
Sending a notification needs a subscription and returns 402 without one.
A notification cannot be edited, cancelled or deleted once created, also when it is scheduled for later.
There is no idempotency key: sending the same request twice sends two notifications.
Notification request
- payload
- Notification payload
- scheduled_at
- optional
String. RFC 3339 format.
Specifies the date and time to schedule the notification for future delivery, up to 30 days in advance. Must be in the future.
Omit it for immediate delivery; the notification then reports its creation time as scheduled_at.
Notification payload
- title
- String. Max 50 bytes.
- message
- String. Max 200 bytes.
- link
- optional
String. Valid URL.
Returned as null when not set. - image_url
- optional
String. Image url.
Image dimension ratio 16/9, up to 3MB.
Allowed formats: .gif, .jpg, .png, .webp
Returned as null when not set.
Text limits count bytes, not characters, so emoji and accented text hit them early.
Notification status
Appio creates one delivery per device the notification is sent to. The status of the notification follows its deliveries.
- created
- The notification exists but its deliveries do not yet. Only a notification for all devices has this status, while it waits for scheduled_at or while its deliveries are being created.
- queued
- All deliveries exist and at least one is still waiting to be sent. A scheduled notification for a device or a user has this status until scheduled_at.
- completed
- Every delivery was sent and none failed. Final.
- failed
- Every delivery was processed and at least one failed, e.g. the push provider rejected the device. The other deliveries were sent. Final.
- skipped
- Nothing was sent because the service was deactivated before the notification was due. Final.
delivery_stats, returned by GET /v1/notifications/{id}, counts the deliveries in each of these statuses; total is the number of devices the notification was sent to.
Filter options
- status
- optional
String. Filter notifications by status: created, queued, completed, failed, skipped.
Any other value returns 400. - device_id
- optional
String. Filter notifications for a specific device. - user_id
- optional
String. Filter notifications for a specific user.
If both device_id and user_id are provided, only device_id is used.
List notifications
Returns a paginated list of the notifications of the service, newest first.
Accepts the limit and after pagination parameters.
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": "2027-01-01T12:34:56.789000Z"
},
...
],
"pagination": {
"next": "/v1/notifications?after=ntf_01jmpmgb6my0s57c960q1s862v&limit=50"
}
}List filtered notifications
The filter options are query parameters of GET /v1/notifications and can be combined with each other and with the pagination parameters.
The response has the same shape as List notifications, and pagination.next keeps the filters.
Notifications with a status, e.g. delivered ones
GET /v1/notifications?status=completedcurl -X GET "https://api.appio.so/v1/notifications?status=completed" \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Notifications for a device
GET /v1/notifications?device_id={device_id}curl -X GET "https://api.appio.so/v1/notifications?device_id=dvc_01jmpmh9fvxgyym44sqanjr9hs" \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Notifications for a user
GET /v1/notifications?user_id={user_id}curl -X GET "https://api.appio.so/v1/notifications?user_id=docs-appio-so:hi@appio.so" \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"data": [
...
],
"pagination": {
"next": "/v1/notifications?after=ntf_01jmpmgb6my0s57c960q1s862v&limit=50&user_id=docs-appio-so%3Ahi%40appio.so"
}
}View notification details
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": "2027-01-01T12:34:56.789000Z",
"delivery_stats": {
"total": 1,
"created": 0,
"queued": 0,
"completed": 1,
"failed": 0,
"skipped": 0
}
}Target options
The audience of POST /v1/notifications is chosen by two optional query parameters. They go in the URL, not in the JSON body:
- device_id
- optional
String. Deliver to that single device. - user_id
- optional
String. Deliver to all devices of that user.
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.
A request without device_id and user_id is sent to every device of the service.
Query parameters the endpoint does not know are ignored, so a misspelled parameter such as ?userId= or ?user= also sends to every device. Check the spelling before sending to one user.
Send a notification to all subscribed devices
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"
}Send a scheduled notification to a user
Sends a notification to all eligible devices belonging to this user.
scheduled_at works the same way for every audience. The value in this example is only an example: use a time in the future, at most 30 days ahead, in RFC 3339 format such as 2026-12-05T09:00:00Z. A time in the past or further ahead returns 400.
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=docs-appio-so:hi@appio.so \
-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":"2026-12-05T09:00:00.000Z"}'Request body
{
"payload": {
"title": "Notification",
"message": "Hello from Appio Docs"
},
"scheduled_at": "2026-12-05T09:00:00.000Z"
}Response
HTTP Code: 201{
"id": "ntf_01jv7akwwbj47rf3je11jgpvvf"
}Send a notification to a single device
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"
}API Widgets
A widget shows the data of a data source, laid out by a widget template, on the user's device.
A widget without a data source shows its template as it is.
Only a published widget is offered to users. In the Appio app the user adds the Appio widget to their home screen, lock screen or watch face and picks one of the published widgets of the service. A widget that is unpublished or deleted is no longer offered.
A widget is rendered for one user at a time: when a device refreshes the widget, Appio requests the data source for the user of that device, fills the placeholders of the template and returns the result to the device.
Devices refresh a widget on their own schedule (Android about every 15 minutes, iOS and watchOS when the operating system allows it), when the user taps a refreshButton or button element, and after the widget template was updated. The refresh time cannot be set through the API.
To set up a widget: create a widget template, create a data source (optional), then create the widget with both IDs and published: true.
The preferred way to create the widget template is the visual (WYSIWYG) editor in the dashboard; a template created there can be used through the API by its ID.
Widget fields
- name
- String. Max 255 bytes. Required for create.
- template_id
- String. ID of a widget template. Required for create.
- data_source_id
- optional
String. ID of a data source.
Send an explicit null to clear it; an empty string is rejected as a malformed ID. - published
- optional
Boolean. Whether the widget is published. Default: false.
List widgets
Returns a paginated list of the widgets of the service.
Accepts the limit and after pagination parameters.
Path
GET /v1/widgetsRequest
curl -X GET https://api.appio.so/v1/widgets \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"data": [
{
"id": "wgt_01jmpr3vt1008txv1jehgdeny7",
"service_id": "svc_00dddddd000000ccccccssssss",
"published": true,
"name": "Doc Widget",
"data_source_id": "ds_01k784vqqpassea0bfq2v2qps3",
"template_id": "wt_01k77dkvgnen72bk0xvvpvbq3a"
}
],
"pagination": {
"next": null
}
}View widget details
Path
GET /v1/widgets/{id}Request
curl -X GET https://api.appio.so/v1/widgets/wgt_01jmpr3vt1008txv1jehgdeny7 \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "wgt_01jmpr3vt1008txv1jehgdeny7",
"service_id": "svc_00dddddd000000ccccccssssss",
"published": true,
"name": "Doc Widget",
"data_source_id": "ds_01k784vqqpassea0bfq2v2qps3",
"template_id": "wt_01k77dkvgnen72bk0xvvpvbq3a"
}Create widget
Creates a new widget for the service. This request works with the docs key.
Path
POST /v1/widgetsRequest
curl -X POST https://api.appio.so/v1/widgets \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"name":"User Stats Widget","data_source_id":"ds_01k784vqqpassea0bfq2v2qps3","template_id":"wt_01k77dkvgnen72bk0xvvpvbq3a","published":true}'Request body
{
"name": "User Stats Widget",
"data_source_id": "ds_01k784vqqpassea0bfq2v2qps3",
"template_id": "wt_01k77dkvgnen72bk0xvvpvbq3a",
"published": true
}Response
HTTP Code: 201{
"id": "wgt_01jv7938vhvsccakmeq7c1hcy8"
}Update widget
Every field is optional, but at least one of name, data_source_id, template_id or published must be present: a body with none of them returns 400.
An empty name counts as absent.
Use your own API key.
Path
PATCH /v1/widgets/{id}Request
curl -X PATCH https://api.appio.so/v1/widgets/wgt_01jmpr3vt1008txv1jehgdeny7 \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"published":false}'Request body
{
"published": false
}Response
HTTP Code: 200{
"id": "wgt_01jmpr3vt1008txv1jehgdeny7"
}Delete widget
Use your own API key.
Path
DELETE /v1/widgets/{id}Request
curl -X DELETE https://api.appio.so/v1/widgets/wgt_01jmpr3vt1008txv1jehgdeny7 \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "wgt_01jmpr3vt1008txv1jehgdeny7"
}API Widget Templates
A widget template defines how a widget looks on each platform. It holds three layouts, one each for iOS, Android and watchOS.
Each layout is a JSON document, described in Widget Template Format, and may contain placeholders that are filled from the data source of the widget.
The preferred way to create and edit a widget template is the visual (WYSIWYG) editor in the dashboard. It shows the widget as you build it and writes the layouts of all three platforms for you.
Use the endpoints below when you need to create or change templates from your own code. GET /v1/templates lists the templates made in the editor as well.
template_ios, template_android and template_watchos are strings that contain JSON, not JSON objects: serialize the layout and send it as a string, as in the examples below.
The API only checks that they are not empty. A layout that is not valid for its platform is saved, and the widget then shows nothing on that platform.
Widget template fields
- name
- String. Max 255 bytes. Required for create. Unique within the service: a duplicate returns 409.
- template_ios
- String. Layout for iPhone and iPad as a JSON string, in the iOS and watchOS format. Required for create.
- template_android
- String. Layout for Android as a JSON string, in the Android format. Required for create.
- template_watchos
- String. Layout for Apple Watch as a JSON string, in the iOS and watchOS format. Required for create.
All three layouts are required when creating a template, also when you only care about one platform. A minimal valid layout for a platform you do not use is the one in the examples below.
Responses also contain id, service_id, created_at and updated_at.
List widget templates
Returns a paginated list of the widget templates of the service.
Accepts the limit and after pagination parameters.
Path
GET /v1/templatesRequest
curl -X GET https://api.appio.so/v1/templates \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"data": [
{
"id": "wt_01k77dkvgnen72bk0xvvpvbq3a",
"service_id": "svc_00dddddd000000ccccccssssss",
"name": "Docs Widget Template",
"template_ios": "{\"variants\": [{\"properties\": {\"version\": \"1.0\", \"supportedFamilies\": [\"systemMedium\"]}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}",
"template_android": "{\"variants\": [{\"properties\": {\"version\": \"1.0\"}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}",
"template_watchos": "{\"variants\": [{\"properties\": {\"version\": \"1.0\", \"supportedFamilies\": [\"accessoryRectangular\"]}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}",
"created_at": "2026-11-02T16:50:31.160387Z",
"updated_at": "2026-11-02T16:50:31.160387Z"
}
],
"pagination": {
"next": null
}
}View widget template details
Path
GET /v1/templates/{id}Request
curl -X GET https://api.appio.so/v1/templates/wt_01k77dkvgnen72bk0xvvpvbq3a \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "wt_01k77dkvgnen72bk0xvvpvbq3a",
"service_id": "svc_00dddddd000000ccccccssssss",
"name": "Docs Widget Template",
"template_ios": "{\"variants\": [{\"properties\": {\"version\": \"1.0\", \"supportedFamilies\": [\"systemMedium\"]}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}",
"template_android": "{\"variants\": [{\"properties\": {\"version\": \"1.0\"}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}",
"template_watchos": "{\"variants\": [{\"properties\": {\"version\": \"1.0\", \"supportedFamilies\": [\"accessoryRectangular\"]}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}",
"created_at": "2026-11-02T16:50:31.160387Z",
"updated_at": "2026-11-02T16:50:31.160387Z"
}Create widget template
Use your own API key.
Path
POST /v1/templatesRequest
curl -X POST https://api.appio.so/v1/templates \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"name":"My Widget Template","template_ios":"{\"variants\": [{\"properties\": {\"version\": \"1.0\", \"supportedFamilies\": [\"systemMedium\"]}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}","template_android":"{\"variants\": [{\"properties\": {\"version\": \"1.0\"}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}","template_watchos":"{\"variants\": [{\"properties\": {\"version\": \"1.0\", \"supportedFamilies\": [\"accessoryRectangular\"]}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}"}'Request body
{
"name": "My Widget Template",
"template_ios": "{\"variants\": [{\"properties\": {\"version\": \"1.0\", \"supportedFamilies\": [\"systemMedium\"]}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}",
"template_android": "{\"variants\": [{\"properties\": {\"version\": \"1.0\"}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}",
"template_watchos": "{\"variants\": [{\"properties\": {\"version\": \"1.0\", \"supportedFamilies\": [\"accessoryRectangular\"]}, \"elements\": [{\"type\": \"text\", \"properties\": {\"text\": \"Demo\"}}]}]}"
}Response
HTTP Code: 201{
"id": "wt_01jv7938vhvsccakmeq7c1hcy8"
}Update widget template
Every field is optional, but at least one of name, template_ios, template_android or template_watchos must be present and non-empty: a body with none of them returns 400.
A successful update also sends a silent push that makes the service's devices refresh their widgets.
It does not appear in GET /v1/notifications.
Use your own API key.
Path
PATCH /v1/templates/{id}Request
curl -X PATCH https://api.appio.so/v1/templates/wt_01k77dkvgnen72bk0xvvpvbq3a \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"name":"Renamed Widget Template"}'Request body
{
"name": "Renamed Widget Template"
}Response
HTTP Code: 200{
"id": "wt_01k77dkvgnen72bk0xvvpvbq3a"
}Delete widget template
Use your own API key.
Path
DELETE /v1/templates/{id}Request
curl -X DELETE https://api.appio.so/v1/templates/wt_01k77dkvgnen72bk0xvvpvbq3a \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "wt_01k77dkvgnen72bk0xvvpvbq3a"
}Widget Template Format
A layout is a JSON document that describes what a widget shows. A widget template holds three of them: template_ios and template_watchos use the iOS and watchOS format, template_android uses the Android format.
The two formats share the same structure and differ in the names of their elements and properties.
You do not need to write a layout by hand. The preferred way to create a widget template is the visual (WYSIWYG) editor in the dashboard, which writes these layouts for you. This section is the reference for templates created or changed through the API.
Structure
{
"variants": [
{
"properties": {"version": "1.0"},
"elements": [
{"type": "text", "properties": {"text": "Hello"}}
]
}
]
}| Property | Type | Description |
|---|---|---|
| variants | Array | required One or more variants. A variant is the layout for one or more widget sizes; the device picks the variant that matches the size the user chose. |
| variants[].properties | Object | required Properties of the whole variant: its sizes, background, padding. |
| variants[].elements | Array | required The elements the variant shows, from top to bottom. |
| elements[].type | String | required Type of the element, e.g. text. |
| elements[].properties | Object | required Properties of the element. Each type has its own. Use {} when there are none. |
| elements[].elements | Array | Child elements. Only for elements that contain others (stacks, rows, columns, boxes). |
Write each layout with the element types of its own format. A layout that is not valid JSON, misses a required property, or uses an element type its platform does not know (e.g. vstack on Android) may not be shown at all.
Placeholders
A placeholder is replaced by a value from the JSON that the data source of the widget returned for the user. It is written {{<JSONPath>}}, where the path is an RFC 9535 JSONPath starting at $, the root of the response.
Data source response example:
{
"orders": 12,
"revenue": "1,240 €",
"goal_percent": 62,
"updated": {
"by": "Anna"
}
}| Written in the layout | Shown in the widget |
|---|---|
| {{$.orders}} | 12 |
| {{$.revenue}} | 1,240 € |
| {{$.updated.by}} | Anna |
| Revenue {{$.revenue}} | Revenue 1,240 € |
| {{$.missing}} | empty |
- A placeholder must be inside a JSON string: "text": "{{$.orders}}". It can be mixed with other text and used several times.
- It works in every string property, also colors, URLs and image sources. Properties documented as Number or String, such as value of a gauge or fontSize, accept it as well.
- Write it without spaces inside the braces.
- When a path matches several values the first one is used. Numbers and booleans are inserted as text, objects and arrays as JSON text.
- A path that matches nothing, or a response that is not JSON, gives an empty string. The widget is still shown.
- A widget without a data source shows its placeholders as they are written.
iOS and watchOS format
Used by template_ios (iPhone and iPad) and template_watchos (Apple Watch). Sizes are in points.
Variant properties
| Property | Type | Description |
|---|---|---|
| version | String | required Version of the format. Use 1.0. |
| supportedFamilies | Array of String | required Sizes this variant is used for, see Sizes. A variant without it is never used. |
| background | String | Background Color. |
| padding | Object | Padding. |
| url | String | URL to open when the widget is tapped. |
| label | Object | Label of some watchOS complications: {"text": "My label", "color": "cyan"}. text is required. Ignored on iOS. |
| private | Boolean | Hide the content while the device is locked. Lock screen and watchOS only. Default: false. |
Sizes
supportedFamilies lists the widget sizes a variant is used for. The device uses the first variant that lists the size the user chose; a size no variant lists shows "Size not supported".
| Size | Where it appears |
|---|---|
| systemSmall, systemMedium, systemLarge | Home screen widgets on iPhone and iPad. |
| systemExtraLarge, systemExtraLargePortrait | Home screen widgets on iPad. |
| accessoryCircular, accessoryInline, accessoryRectangular | Lock screen widgets on iPhone and iPad, and widgets and complications on Apple Watch. |
| accessoryCorner | Complication on Apple Watch. |
| controlWidget | Control Center control on iPhone and iPad. Its variant holds one controlWidget element. |
Elements
hstack, vstack, zstack
Arranges its child elements horizontally (hstack), vertically (vstack) or on top of each other (zstack). The children go in elements.
| Property | Type | Description |
|---|---|---|
| alignment | String | hstack: top, center, bottom. vstack: leading, center, trailing. zstack: topLeading, top, topTrailing, leading, center, trailing, bottomLeading, bottom, bottomTrailing. Default: center. |
| background | String | Background Color. |
| padding | Object | Padding. |
| spacing | Number | Space between the children. Ignored for zstack. Default: 8. |
text
Shows a text.
| Property | Type | Description |
|---|---|---|
| text | String | required The text. |
| color | String | Text Color. Default: primary. |
| background | String | Background Color. |
| fontSize | Number or String | Font size. Default: 16. |
| fontWeight | String | ultralight, thin, light, regular, medium, semibold, bold, heavy, black. Default: regular. |
| italic | Boolean | Italic text. |
| underline | Boolean | Underlined text. |
| strikethrough | Boolean | Struck through text. |
| padding | Object | Padding. |
| widgetCurvesContent | Boolean | Curve the text along the corner of a watchOS accessoryCorner complication. Default: false. |
| private | Boolean | Hide the text while the device is locked. Lock screen and watchOS only. Default: false. |
image
Shows an image from a URL or an SF Symbol. Either src or symbol is required.
| Property | Type | Description |
|---|---|---|
| src | String | URL of the image. |
| symbol | String | Name of an SF Symbol. |
| width | Number or String | Width. Default: all available width. |
| height | Number or String | Height. Default: all available height. |
| contentMode | String | fit or fill. Default: fit, applied only when width or height is set. |
| cornerRadius | Number | Corner radius. |
| borderColor | String | Border Color. |
| borderWidth | Number | Border width. |
| opacity | Number | Opacity from 0 to 1. Default: 1. |
| padding | Object | Padding. |
| accentedRenderingMode | String | How the image is drawn on a tinted home screen (iOS 18): accented, accentedDesaturated, desaturated, fullColor. Default: fullColor. |
gauge
Shows a value on a scale.
| Property | Type | Description |
|---|---|---|
| value | Number or String | required Current value. |
| min | Number | Minimum value. Default: 0. |
| max | Number | Maximum value. Default: 100. |
| style | String | linearCapacity, accessoryCircular, accessoryLinear, accessoryCircularCapacity, accessoryLinearCapacity. Default: linearCapacity. |
| label | String | Label of the gauge. |
| currentValueLabel | String | Label of the current value. |
| minimumValueLabel | String | Label of the minimum value. |
| maximumValueLabel | String | Label of the maximum value. |
| color | String | Color of the labels. Default: primary. |
| tint | String | Tint Color. Default: accentColor. |
| opacity | Number | Opacity from 0 to 1. Default: 1. |
| scaleEffect | Number | Scales the gauge. Default: 1. |
| padding | Object | Padding. |
rectangle, ellipse
Draws a filled shape.
| Property | Type | Description |
|---|---|---|
| color | String | Fill Color. Default: primary. |
| width | Number or String | Width. Default: all available width. |
| height | Number or String | Height. Default: all available height. |
| cornerRadius | Number | Corner radius. rectangle only. |
| opacity | Number | Opacity from 0 to 1. Default: 1. |
| padding | Object | Padding. |
spacer
Adds flexible empty space between elements.
| Property | Type | Description |
|---|---|---|
| length | Number | Minimum length. |
| padding | Object | Padding. |
button
Shows a button that calls a URL of yours, see Buttons.
| Property | Type | Description |
|---|---|---|
| text | String | required Label of the button. |
| url | String | required URL to call when the button is tapped. |
| style | String | bordered, borderedProminent, plain, borderless. Default: bordered. |
| color | String | Text Color. Default: accentColor. |
| tint | String | Tint Color. Default: accentColor. |
| padding | Object | Padding. |
refreshButton
Shows a button that refreshes the widget.
| Property | Type | Description |
|---|---|---|
| text | String | Label of the button. Default: Refresh. |
| style | String | bordered, borderedProminent, plain, borderless. Default: bordered. |
| color | String | Text Color. Default: accentColor. |
| tint | String | Tint Color. Default: accentColor. |
| padding | Object | Padding. |
lastUpdated
Shows the time the widget was last refreshed.
| Property | Type | Description |
|---|---|---|
| format | String | Date format. Tokens: HH hours (24h), hh hours (12h), mm minutes, ss seconds, a AM/PM. Default: HH:mm:ss. |
| color | String | Text Color. Default: primary. |
| background | String | Background Color. |
| fontSize | Number or String | Font size. Default: 16. |
| fontWeight | String | Same values as for text. Default: regular. |
| padding | Object | Padding. |
| widgetCurvesContent | Boolean | Curve the text along the corner of a watchOS accessoryCorner complication. Default: false. |
controlWidget
Defines a Control Center control. Only used in a variant with supportedFamilies: ["controlWidget"], and ignored everywhere else.
| Property | Type | Description |
|---|---|---|
| label | String | required Label of the control. |
| url | String | required URL to call when the control is tapped, see Buttons. |
| symbol | String | Name of an SF Symbol. Default: gearshape. |
button and refreshButton are not shown on accessoryInline and accessoryCircular, nor on accessoryCorner on Apple Watch. On an Apple Watch face they are shown in accessoryRectangular but cannot be tapped.
Android format
Used by template_android. Sizes are in dp, font sizes in sp.
Variant properties
| Property | Type | Description |
|---|---|---|
| version | String | required Version of the format. Use 1.0. |
| width | Number | Minimum width in dp this variant is made for, see Sizes. |
| height | Number | Minimum height in dp this variant is made for. |
| background | String | Background Color. |
| padding | Object | Padding. |
| url | String | URL to open when the widget is tapped. |
Sizes
The device picks the variant whose width and height fit best into the size of the widget on the home screen: the largest variant that still fits. A variant without width and height fits every size. One variant without a size is enough for a widget that looks the same at every size.
As a guide, on a Pixel 9 Pro a home screen cell is about 157 dp wide and 61 dp high, and a 2×2 widget is 325 × 132 dp.
Elements
row, column, box
Arranges its child elements horizontally (row), vertically (column) or on top of each other (box). The children go in elements.
| Property | Type | Description |
|---|---|---|
| horizontalAlignment | String | start, center, end. Default: center. |
| verticalAlignment | String | top, center, bottom. Default: center. |
| background | String | Background Color. |
| cornerRadius | Number | Corner radius in dp. |
| width | Number or String | Width in dp. Default: all available width. |
| height | Number or String | Height in dp. Default: all available height. |
| padding | Object | Padding. |
| spacing | Number | Space between the children in dp. row and column only. Default: 0. |
text
Shows a text.
| Property | Type | Description |
|---|---|---|
| text | String | required The text. |
| color | String | Text Color. |
| background | String | Background Color. |
| fontSize | Number or String | Font size in sp. Default: 14. |
| fontWeight | String | normal, medium, bold. Default: normal. |
| italic | Boolean | Italic text. |
| padding | Object | Padding. |
image
Shows an image from a URL.
| Property | Type | Description |
|---|---|---|
| src | String | required URL of the image. |
| width | Number or String | Width in dp. Default: all available width. |
| height | Number or String | Height in dp. Default: all available height. |
| contentMode | String | fit, fill or crop. Default: fit. |
| cornerRadius | Number | Corner radius in dp. |
| borderColor | String | Border Color. |
| borderWidth | Number | Border width in dp. |
| opacity | Number | Opacity from 0 to 1. Default: 1. |
| padding | Object | Padding. |
gauge
Shows a value on a scale.
| Property | Type | Description |
|---|---|---|
| value | Number or String | required Current value, between min and max. |
| min | Number or String | Minimum value. Default: 0. |
| max | Number or String | Maximum value. Default: 100. |
| label | String | Text shown over the gauge. |
| color | String | Color of the label. |
| fontSize | Number or String | Font size of the label in sp. Default: 14. |
| fontWeight | String | normal, medium, bold. Default: normal. |
| tint | String | Color of the gauge. |
| size | Number or String | Size in dp. Default: all available space. |
| opacity | Number | Opacity from 0 to 1. Default: 1. |
| padding | Object | Padding. |
rectangle, ellipse
Draws a filled shape.
| Property | Type | Description |
|---|---|---|
| color | String | Fill Color. Default: primary. |
| width | Number or String | Width in dp. Default: all available width. |
| height | Number or String | Height in dp. Default: all available height. |
| cornerRadius | Number | Corner radius in dp. rectangle only. |
| opacity | Number | Opacity from 0 to 1. Default: 1. |
| padding | Object | Padding around the shape. |
spacer
Adds empty space between elements.
| Property | Type | Description |
|---|---|---|
| length | Number or String | Size in dp. Default: fills the empty space. |
| padding | Object | Padding. |
button
Shows a button that calls a URL of yours, see Buttons.
| Property | Type | Description |
|---|---|---|
| text | String | required Label of the button. |
| url | String | required URL to call when the button is tapped. |
| style | String | filled, outline, icon-primary, icon-secondary. |
| color | String | Text Color. |
| tint | String | Background Color. |
| padding | Object | Padding. |
refreshButton
Shows a button that refreshes the widget.
| Property | Type | Description |
|---|---|---|
| text | String | Label of the button. Default: Refresh. |
| style | String | filled, outline, icon-primary, icon-secondary. |
| color | String | Text Color. |
| tint | String | Background Color. |
| padding | Object | Padding. |
lastUpdated
Shows the time the widget was last refreshed, as HH:mm:ss.
| Property | Type | Description |
|---|---|---|
| color | String | Text Color. |
| background | String | Background Color. |
| fontSize | Number or String | Font size in sp. Default: 14. |
| fontWeight | String | normal, medium, bold. Default: normal. |
| italic | Boolean | Italic text. |
| padding | Object | Padding. |
Android has no hstack, vstack, zstack or controlWidget, and text has no underline or strikethrough. Use row, column and box instead of the stacks.
Values
Padding
"padding": {"top": 4, "bottom": 4, "left": 8, "right": 8}Each side is optional.
Color
| Values | |
|---|---|
| Hex | #00FF00, #aa00FF. |
| Named, both platforms | black, blue, brown, cyan, gray, green, indigo, mint, orange, pink, purple, red, teal, white, yellow, magenta, aqua, darkgray, fuchsia, lightgray, lime, maroon, navy, olive, silver, clear. |
| Named, iOS and watchOS only | primary (black in light mode, white in dark mode), secondary, accentColor (blue). |
| Named, Android only | Colors of the system theme: primary, onprimary, primarycontainer, onprimarycontainer, secondary, onsecondary, secondarycontainer, onsecondarycontainer, tertiary, ontertiary, tertiarycontainer, ontertiarycontainer, error, onerror, errorcontainer, onerrorcontainer, background, onbackground, surface, onsurface, surfacevariant, onsurfacevariant, outline, inversesurface, inverseonsurface, inverseprimary, widgetbackground. |
| Light and dark mode | Two colors separated by a comma, light mode first: #000000,#FFFFFF. |
| Gradient, iOS and watchOS only | Colors joined by +: red+green+blue. Can be combined with light and dark mode: red+green,#f00+yellow. |
Buttons
Tapping a button element, or a controlWidget control, does not open anything. The device sends a GET request to its url in the background and then refreshes the widget, so the URL can change something on your side and the widget shows the result.
The request comes from the user's device and carries these headers:
| Header | Value |
|---|---|
| X-Service-Id | ID of the service. |
| X-Widget-Id | ID of the widget. |
| X-Device-Id | ID of the device. |
| X-Customer-User-Id | The user_id of the user. |
| User-Agent | Appio-IOS-Widget-<version>, Appio-Watch-Widget-<version> or Appio-Android-Widget-<version>. |
Complete example
A widget that shows the orders of the day. The data source returns the JSON shown under Placeholders.
Layout for template_ios: one variant for the small and medium home screen widget, one for the lock screen
{
"variants": [
{
"properties": {
"version": "1.0",
"supportedFamilies": [
"systemSmall",
"systemMedium"
],
"background": "#FFFFFF,#1C1C1E",
"url": "https://example.com/orders"
},
"elements": [
{
"type": "vstack",
"properties": {
"alignment": "leading",
"spacing": 4
},
"elements": [
{
"type": "text",
"properties": {
"text": "Orders today",
"fontSize": 13,
"color": "secondary"
}
},
{
"type": "text",
"properties": {
"text": "{{$.orders}}",
"fontSize": 40,
"fontWeight": "bold"
}
},
{
"type": "text",
"properties": {
"text": "Revenue {{$.revenue}}",
"fontSize": 13
}
},
{
"type": "gauge",
"properties": {
"value": "{{$.goal_percent}}",
"tint": "green"
}
}
]
}
]
},
{
"properties": {
"version": "1.0",
"supportedFamilies": [
"accessoryRectangular"
]
},
"elements": [
{
"type": "text",
"properties": {
"text": "{{$.orders}} orders",
"fontWeight": "semibold"
}
}
]
}
]
}Layout for template_watchos
{
"variants": [
{
"properties": {
"version": "1.0",
"supportedFamilies": [
"accessoryRectangular",
"accessoryInline"
]
},
"elements": [
{
"type": "text",
"properties": {
"text": "{{$.orders}} orders"
}
}
]
},
{
"properties": {
"version": "1.0",
"supportedFamilies": [
"accessoryCircular"
]
},
"elements": [
{
"type": "gauge",
"properties": {
"value": "{{$.goal_percent}}",
"style": "accessoryCircularCapacity",
"currentValueLabel": "{{$.orders}}"
}
}
]
}
]
}Layout for template_android: one variant for every size
{
"variants": [
{
"properties": {
"version": "1.0",
"background": "widgetbackground",
"url": "https://example.com/orders"
},
"elements": [
{
"type": "column",
"properties": {
"horizontalAlignment": "start",
"spacing": 4,
"padding": {
"top": 12,
"bottom": 12,
"left": 12,
"right": 12
}
},
"elements": [
{
"type": "text",
"properties": {
"text": "Orders today",
"fontSize": 13,
"color": "onsurfacevariant"
}
},
{
"type": "text",
"properties": {
"text": "{{$.orders}}",
"fontSize": 36,
"fontWeight": "bold"
}
},
{
"type": "text",
"properties": {
"text": "Revenue {{$.revenue}}",
"fontSize": 13
}
}
]
}
]
}
]
}Then create the three resources. Each layout is sent as a string:
1. POST /v1/templates
{"name": "Orders", "template_ios": "<layout as string>", "template_android": "<layout as string>", "template_watchos": "<layout as string>"}
-> {"id": "wt_…"}
2. POST /v1/datasources
{"name": "Orders", "type": "url", "definition": {"url": "https://example.com/api/orders", "query_param": true}}
-> {"id": "ds_…"}
3. POST /v1/widgets
{"name": "Orders", "template_id": "wt_…", "data_source_id": "ds_…", "published": true}
-> {"id": "wgt_…"}When a device of the user abc refreshes the widget, Appio requests https://example.com/api/orders?user_id=abc and fills the placeholders with the response. Preview the data source to check what your URL returns before saving it.
API Data Sources
A data source tells Appio where to fetch the data a widget shows. Appio requests it for each user when their widget is rendered.
The request comes from Appio's servers, never from the user's device:
- It is a GET request with the header User-Agent: Appio and the headers of the definition.
- The ID of the user is added as the user_id query parameter and/or the X-User-Id header, when the definition asks for it. Without either, every user gets the same data.
- Your URL must answer with status 200 and a JSON body within 10 seconds. The body can be up to 10 MB.
- Nothing is cached: every refresh of a widget is one request to your URL.
- The request is not signed. It is up to your URL to return the right data for the user_id it is given, so you need to be able to find your user by that ID. To accept requests from Appio only, put a secret in headers and check it.
The JSON you return is what the placeholders of the widget template read from. If the URL cannot be reached, is too slow, or does not answer with 200, the widget is not updated. If the body is not JSON, or a placeholder finds nothing, that placeholder is shown empty.
Data source fields
- name
- String. Max 255 bytes. Required for create. Unique within the service: a duplicate returns 409.
- type
- String. url. Required for create.
Any other non-empty value is saved, but a widget using it fails to render. - definition
- Object. Required for create. See below.
Responses also contain id, service_id, created_at and updated_at.
Definition for type url
- url
- String. URL to fetch the data from. It must return JSON.
- headers
- optional
Object of strings. Custom headers sent with the request. - query_param
- optional
Boolean. Default: false. Send the user's ID as the user_id query parameter. - header
- optional
Boolean. Default: false. Send the user's ID in the X-User-Id header.
The definition is returned as it was saved, including headers. Other keys are stored as they are.
List data sources
Returns a paginated list of the data sources of the service.
Accepts the limit and after pagination parameters.
Path
GET /v1/datasourcesRequest
curl -X GET https://api.appio.so/v1/datasources \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"data": [
{
"id": "ds_01k784vqqpassea0bfq2v2qps3",
"service_id": "svc_00dddddd000000ccccccssssss",
"name": "Docs Data Source",
"type": "url",
"definition": {
"url": "https://demo.appio.so/data/random/1/99"
},
"created_at": "2026-11-02T16:50:31.160387Z",
"updated_at": "2026-11-02T16:50:31.160387Z"
}
],
"pagination": {
"next": null
}
}View data source details
Path
GET /v1/datasources/{id}Request
curl -X GET https://api.appio.so/v1/datasources/ds_01k784vqqpassea0bfq2v2qps3 \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "ds_01k784vqqpassea0bfq2v2qps3",
"service_id": "svc_00dddddd000000ccccccssssss",
"name": "Docs Data Source",
"type": "url",
"definition": {
"url": "https://demo.appio.so/data/random/1/99"
},
"created_at": "2026-11-02T16:50:31.160387Z",
"updated_at": "2026-11-02T16:50:31.160387Z"
}Create data source
Use your own API key.
Path
POST /v1/datasourcesRequest
curl -X POST https://api.appio.so/v1/datasources \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"name":"My Data Source","type":"url","definition":{"url":"https://demo.appio.so/data/random/1/99","headers":{"Authorization":"Bearer 1234567890"},"query_param":true}}'Request body
{
"name": "My Data Source",
"type": "url",
"definition": {
"url": "https://demo.appio.so/data/random/1/99",
"headers": {
"Authorization": "Bearer 1234567890"
},
"query_param": true
}
}Response
HTTP Code: 201{
"id": "ds_01jv7938vhvsccakmeq7c1hcy8"
}Update data source
Every field is optional, but at least one of name, type or definition must be present: a body with none of them returns 400.
An empty name or type counts as absent.
Use your own API key.
Path
PATCH /v1/datasources/{id}Request
curl -X PATCH https://api.appio.so/v1/datasources/ds_01k784vqqpassea0bfq2v2qps3 \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"name":"Renamed Data Source"}'Request body
{
"name": "Renamed Data Source"
}Response
HTTP Code: 200{
"id": "ds_01k784vqqpassea0bfq2v2qps3"
}Delete data source
Use your own API key.
Path
DELETE /v1/datasources/{id}Request
curl -X DELETE https://api.appio.so/v1/datasources/ds_01k784vqqpassea0bfq2v2qps3 \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "ds_01k784vqqpassea0bfq2v2qps3"
}Preview data source URL
Tests a data source URL before saving it. Appio requests the URL with the given user ID and headers and returns what it received.
- name
- String. Max 255 bytes.
- url
- String. URL to fetch the data from.
- user_id
- String. User ID to test with.
- query_param
- optional
Boolean. Default: false. Append user_id as a query parameter to the URL. - header
- optional
Boolean. Default: false. Send user_id in the X-User-Id header. - headers
- optional
Object of strings. Custom headers sent with the request.
At least one of query_param or header must be true.
The response is 200 whatever the URL answered:
- code
- Integer. HTTP status code the URL answered with.
- body
- String. Response body of the URL.
- type
- String. Always JSON, the only format a widget can render.
- valid
- Boolean. Whether the body parses as JSON. An XML, HTML or plain text body is reported as false.
A URL that cannot be reached, does not answer within 10 seconds, or returns a body over 10 MB returns 500.
Use your own API key.
Path
POST /v1/datasources/previewRequest
curl -X POST https://api.appio.so/v1/datasources/preview \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"name":"Test Preview","url":"https://demo.appio.so/data/random/1/99","user_id":"docs-appio-so:hi@appio.so","query_param":true}'Request body
{
"name": "Test Preview",
"url": "https://demo.appio.so/data/random/1/99",
"user_id": "docs-appio-so:hi@appio.so",
"query_param": true
}Response
HTTP Code: 200{
"code": 200,
"body": "{\"value\": 37}",
"type": "JSON",
"valid": true
}API Data Source Snapshots
A snapshot is the last response of a data source stored for one user. There is one snapshot per data source and user.
Snapshots exist for inspection: they answer "what did my URL last return for this user?" when a widget shows something unexpected. Appio writes one each time it requests the data source to render a widget for that user.
A snapshot is a record, not a cache. Widgets are always rendered from a fresh request to the data source, never from the snapshot, so storing a snapshot does not change what a widget shows.
View data source snapshot
Returns the snapshot of a data source for the user in the required user_id query parameter.
A missing user_id returns 400; a user without a snapshot returns 404. A user has no snapshot until a widget using the data source was rendered for them.
Path
GET /v1/datasources/{id}/snapshots?user_id={user_id}Request
curl -X GET https://api.appio.so/v1/datasources/ds_01k784vqqpassea0bfq2v2qps3/snapshots?user_id=docs-appio-so:hi@appio.so \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss"Response
HTTP Code: 200{
"id": "dss_01kjaf76ntb57y7as24kgzg5qk",
"data_source_id": "ds_01k784vqqpassea0bfq2v2qps3",
"user_id": "docs-appio-so:hi@appio.so",
"snapshot": "{\"user_id\":\"\",\"value\":37}",
"created_at": "2026-11-02T16:50:31.160387Z",
"updated_at": "2026-11-02T16:50:31.160387Z"
}Store data source snapshot
Stores the snapshot of a data source for a user, replacing the one already stored for that user (which keeps its id).
A later widget render for that user replaces it again.
- user_id
- String. ID of the user the snapshot belongs to.
- snapshot
- String. The data source response to store, as a string: a JSON body is sent as a JSON string, as in the example.
Use your own API key.
Path
POST /v1/datasources/{id}/snapshotsRequest
curl -X POST https://api.appio.so/v1/datasources/ds_01k784vqqpassea0bfq2v2qps3/snapshots \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "Content-Type: application/json" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-d '{"user_id":"docs-appio-so:hi@appio.so","snapshot":"{\"value\": 37}"}'Request body
{
"user_id": "docs-appio-so:hi@appio.so",
"snapshot": "{\"value\": 37}"
}Response
HTTP Code: 201{
"id": "dss_01kjaf76ntb57y7as24kgzg5qk",
"data_source_id": "ds_01k784vqqpassea0bfq2v2qps3",
"user_id": "docs-appio-so:hi@appio.so",
"snapshot": "{\"value\": 37}",
"created_at": "2026-11-02T16:50:31.160387Z",
"updated_at": "2026-11-02T16:50:31.160387Z"
}API Images
Upload image
Uploads an image for the service and returns its public URL, e.g. to use as a notification image_url.
Send it as multipart/form-data in the image field.
Allowed formats: JPEG, PNG, GIF, WebP. Max 3MB. SVG is not accepted.
The Content-Type of the part is verified against the file's content; if they contradict each other the upload returns 400.
The file name in the returned URL is generated by Appio, and its extension (.jpg, .png, .gif, .webp) comes from the verified format.
With the docs key nothing is stored and the same placeholder URL is always returned.
Use your own API key for full access.
Path
POST /v1/imagesRequest
curl -X POST https://api.appio.so/v1/images \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A" \
-H "X-Service-Id: svc_00dddddd000000ccccccssssss" \
-F "image=@banner.png"Response
HTTP Code: 201{
"url": "https://cdn-r2.appio.so/svc_00dddddd000000ccccccssssss/img_00dddddd000000ccccccssssss.png"
}API Billing
View billing status
Returns whether the organization has active subscription.
entitled is true when at least one subscription is active or trialing, and false otherwise.
No X-Service-Id header is required.
Path
GET /v1/billing/statusRequest
curl -X GET https://api.appio.so/v1/billing/status \
-H "Authorization: Bearer docs_g3psUMsuKZ7NGGJvuk1csf47pvJfukz97cS5ZrOuHnY98yhY5A"Response
HTTP Code: 200{
"entitled": true
}API Errors
- 400
- Bad Request
The request contains invalid or missing data.
Please double-check the API endpoint URL and ensure the request body is correctly formatted and complete.
This includes a missing or malformed X-Service-Id header, and any request body field the endpoint does not know. - 401
- Unauthorized
Authentication is required.
The Authorization header is missing, or is not Bearer <api-key>. - 402
- Payment Required
Your organization has no subscription that is active or trialing.
Subscribe at the URL in data.url of the error response. - 403
- Forbidden
The API key does not exist, has been deactivated, is bound to a service that has been deactivated, or has no access to this service or endpoint.
On the "Appio Docs" service, every 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. - 404
- Not Found
The requested entity doesn't exist.
Verify that the resource identifier is correct, exists, and is currently active.
Also returned by POST /v1/notifications for a device or user that has no device able to receive the notification. - 409
- Conflict
A data source or widget template with the same name already exists in this service. - 429
- Too Many Requests
The per-IP rate limit was exceeded, or too many failed attempts were made with the same API key.
After exceeding the rate limit, wait the number of seconds in the Retry-After header before retrying. - 500
- Internal Error
Something went wrong while processing your request.
Try again later or contact support if the issue persists.
Also returned by the data source preview when the URL cannot be reached, is too slow or returns too much data.
Error response
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"
}
]
}
}
}- message
- String. Human-readable description of the error.
- data
- optional
Object. Extra context when available, e.g. doc_url, entity, validation_errors, url.