Appio documentation

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Request bodies are JSON and strict: an unknown field returns 400. Text limits count UTF-8 bytes.
  7. 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.
  8. List endpoints are paginated. Follow pagination.next until it is null.
  9. 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

  1. You create an account, a service and an API key in the dashboard.
  2. You add the Appio button to your website and give it the service ID and the ID of the signed-in user.
  3. 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.
  4. You send notifications to that user_id, to a single device, or to everyone.
  5. 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.
  6. 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

Mobile APP

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/services

Request

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/services

Request

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/devices

Request

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/notifications

Request

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=completed
curl -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/notifications

Request

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/widgets

Request

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/widgets

Request

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/templates

Request

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/templates

Request

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"}}
            ]
        }
    ]
}
PropertyTypeDescription
variantsArrayrequired
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[].propertiesObjectrequired
Properties of the whole variant: its sizes, background, padding.
variants[].elementsArrayrequired
The elements the variant shows, from top to bottom.
elements[].typeStringrequired
Type of the element, e.g. text.
elements[].propertiesObjectrequired
Properties of the element. Each type has its own. Use {} when there are none.
elements[].elementsArrayChild 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 layoutShown in the widget
{{$.orders}}12
{{$.revenue}}1,240 €
{{$.updated.by}}Anna
Revenue {{$.revenue}}Revenue 1,240 €
{{$.missing}}empty

iOS and watchOS format

Used by template_ios (iPhone and iPad) and template_watchos (Apple Watch). Sizes are in points.

Variant properties

PropertyTypeDescription
versionStringrequired
Version of the format. Use 1.0.
supportedFamiliesArray of Stringrequired
Sizes this variant is used for, see Sizes. A variant without it is never used.
backgroundStringBackground Color.
paddingObjectPadding.
urlStringURL to open when the widget is tapped.
labelObjectLabel of some watchOS complications: {"text": "My label", "color": "cyan"}. text is required. Ignored on iOS.
privateBooleanHide 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".

SizeWhere it appears
systemSmall, systemMedium, systemLargeHome screen widgets on iPhone and iPad.
systemExtraLarge, systemExtraLargePortraitHome screen widgets on iPad.
accessoryCircular, accessoryInline, accessoryRectangularLock screen widgets on iPhone and iPad, and widgets and complications on Apple Watch.
accessoryCornerComplication on Apple Watch.
controlWidgetControl 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.

PropertyTypeDescription
alignmentStringhstack: top, center, bottom. vstack: leading, center, trailing. zstack: topLeading, top, topTrailing, leading, center, trailing, bottomLeading, bottom, bottomTrailing. Default: center.
backgroundStringBackground Color.
paddingObjectPadding.
spacingNumberSpace between the children. Ignored for zstack. Default: 8.

text
Shows a text.

PropertyTypeDescription
textStringrequired
The text.
colorStringText Color. Default: primary.
backgroundStringBackground Color.
fontSizeNumber or StringFont size. Default: 16.
fontWeightStringultralight, thin, light, regular, medium, semibold, bold, heavy, black. Default: regular.
italicBooleanItalic text.
underlineBooleanUnderlined text.
strikethroughBooleanStruck through text.
paddingObjectPadding.
widgetCurvesContentBooleanCurve the text along the corner of a watchOS accessoryCorner complication. Default: false.
privateBooleanHide 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.

PropertyTypeDescription
srcStringURL of the image.
symbolStringName of an SF Symbol.
widthNumber or StringWidth. Default: all available width.
heightNumber or StringHeight. Default: all available height.
contentModeStringfit or fill. Default: fit, applied only when width or height is set.
cornerRadiusNumberCorner radius.
borderColorStringBorder Color.
borderWidthNumberBorder width.
opacityNumberOpacity from 0 to 1. Default: 1.
paddingObjectPadding.
accentedRenderingModeStringHow the image is drawn on a tinted home screen (iOS 18): accented, accentedDesaturated, desaturated, fullColor. Default: fullColor.

gauge
Shows a value on a scale.

PropertyTypeDescription
valueNumber or Stringrequired
Current value.
minNumberMinimum value. Default: 0.
maxNumberMaximum value. Default: 100.
styleStringlinearCapacity, accessoryCircular, accessoryLinear, accessoryCircularCapacity, accessoryLinearCapacity. Default: linearCapacity.
labelStringLabel of the gauge.
currentValueLabelStringLabel of the current value.
minimumValueLabelStringLabel of the minimum value.
maximumValueLabelStringLabel of the maximum value.
colorStringColor of the labels. Default: primary.
tintStringTint Color. Default: accentColor.
opacityNumberOpacity from 0 to 1. Default: 1.
scaleEffectNumberScales the gauge. Default: 1.
paddingObjectPadding.

rectangle, ellipse
Draws a filled shape.

PropertyTypeDescription
colorStringFill Color. Default: primary.
widthNumber or StringWidth. Default: all available width.
heightNumber or StringHeight. Default: all available height.
cornerRadiusNumberCorner radius. rectangle only.
opacityNumberOpacity from 0 to 1. Default: 1.
paddingObjectPadding.

spacer
Adds flexible empty space between elements.

PropertyTypeDescription
lengthNumberMinimum length.
paddingObjectPadding.

button
Shows a button that calls a URL of yours, see Buttons.

PropertyTypeDescription
textStringrequired
Label of the button.
urlStringrequired
URL to call when the button is tapped.
styleStringbordered, borderedProminent, plain, borderless. Default: bordered.
colorStringText Color. Default: accentColor.
tintStringTint Color. Default: accentColor.
paddingObjectPadding.

refreshButton
Shows a button that refreshes the widget.

PropertyTypeDescription
textStringLabel of the button. Default: Refresh.
styleStringbordered, borderedProminent, plain, borderless. Default: bordered.
colorStringText Color. Default: accentColor.
tintStringTint Color. Default: accentColor.
paddingObjectPadding.

lastUpdated
Shows the time the widget was last refreshed.

PropertyTypeDescription
formatStringDate format. Tokens: HH hours (24h), hh hours (12h), mm minutes, ss seconds, a AM/PM. Default: HH:mm:ss.
colorStringText Color. Default: primary.
backgroundStringBackground Color.
fontSizeNumber or StringFont size. Default: 16.
fontWeightStringSame values as for text. Default: regular.
paddingObjectPadding.
widgetCurvesContentBooleanCurve 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.

PropertyTypeDescription
labelStringrequired
Label of the control.
urlStringrequired
URL to call when the control is tapped, see Buttons.
symbolStringName 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

PropertyTypeDescription
versionStringrequired
Version of the format. Use 1.0.
widthNumberMinimum width in dp this variant is made for, see Sizes.
heightNumberMinimum height in dp this variant is made for.
backgroundStringBackground Color.
paddingObjectPadding.
urlStringURL 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.

PropertyTypeDescription
horizontalAlignmentStringstart, center, end. Default: center.
verticalAlignmentStringtop, center, bottom. Default: center.
backgroundStringBackground Color.
cornerRadiusNumberCorner radius in dp.
widthNumber or StringWidth in dp. Default: all available width.
heightNumber or StringHeight in dp. Default: all available height.
paddingObjectPadding.
spacingNumberSpace between the children in dp. row and column only. Default: 0.

text
Shows a text.

PropertyTypeDescription
textStringrequired
The text.
colorStringText Color.
backgroundStringBackground Color.
fontSizeNumber or StringFont size in sp. Default: 14.
fontWeightStringnormal, medium, bold. Default: normal.
italicBooleanItalic text.
paddingObjectPadding.

image
Shows an image from a URL.

PropertyTypeDescription
srcStringrequired
URL of the image.
widthNumber or StringWidth in dp. Default: all available width.
heightNumber or StringHeight in dp. Default: all available height.
contentModeStringfit, fill or crop. Default: fit.
cornerRadiusNumberCorner radius in dp.
borderColorStringBorder Color.
borderWidthNumberBorder width in dp.
opacityNumberOpacity from 0 to 1. Default: 1.
paddingObjectPadding.

gauge
Shows a value on a scale.

PropertyTypeDescription
valueNumber or Stringrequired
Current value, between min and max.
minNumber or StringMinimum value. Default: 0.
maxNumber or StringMaximum value. Default: 100.
labelStringText shown over the gauge.
colorStringColor of the label.
fontSizeNumber or StringFont size of the label in sp. Default: 14.
fontWeightStringnormal, medium, bold. Default: normal.
tintStringColor of the gauge.
sizeNumber or StringSize in dp. Default: all available space.
opacityNumberOpacity from 0 to 1. Default: 1.
paddingObjectPadding.

rectangle, ellipse
Draws a filled shape.

PropertyTypeDescription
colorStringFill Color. Default: primary.
widthNumber or StringWidth in dp. Default: all available width.
heightNumber or StringHeight in dp. Default: all available height.
cornerRadiusNumberCorner radius in dp. rectangle only.
opacityNumberOpacity from 0 to 1. Default: 1.
paddingObjectPadding around the shape.

spacer
Adds empty space between elements.

PropertyTypeDescription
lengthNumber or StringSize in dp. Default: fills the empty space.
paddingObjectPadding.

button
Shows a button that calls a URL of yours, see Buttons.

PropertyTypeDescription
textStringrequired
Label of the button.
urlStringrequired
URL to call when the button is tapped.
styleStringfilled, outline, icon-primary, icon-secondary.
colorStringText Color.
tintStringBackground Color.
paddingObjectPadding.

refreshButton
Shows a button that refreshes the widget.

PropertyTypeDescription
textStringLabel of the button. Default: Refresh.
styleStringfilled, outline, icon-primary, icon-secondary.
colorStringText Color.
tintStringBackground Color.
paddingObjectPadding.

lastUpdated
Shows the time the widget was last refreshed, as HH:mm:ss.

PropertyTypeDescription
colorStringText Color.
backgroundStringBackground Color.
fontSizeNumber or StringFont size in sp. Default: 14.
fontWeightStringnormal, medium, bold. Default: normal.
italicBooleanItalic text.
paddingObjectPadding.

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 platformsblack, 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 onlyprimary (black in light mode, white in dark mode), secondary, accentColor (blue).
Named, Android onlyColors 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 modeTwo colors separated by a comma, light mode first: #000000,#FFFFFF.
Gradient, iOS and watchOS onlyColors 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:

HeaderValue
X-Service-IdID of the service.
X-Widget-IdID of the widget.
X-Device-IdID of the device.
X-Customer-User-IdThe user_id of the user.
User-AgentAppio-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:

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/datasources

Request

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/datasources

Request

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/preview

Request

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}/snapshots

Request

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/images

Request

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/status

Request

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.