---
title: "SDK API"
description: "The SDK API at /api/v1/sdk and the workspace server API at /api/v1/public."
canonical: https://docs.notifiable.sh/api/public-api
source: https://docs.notifiable.sh/api/public-api.md
---

# SDK API

The generated reference at `/docs/api` lists every declared scenario. This file records the routes that exist.

## GET /api/v1

- **Auth:** none.
- **Name:** `api.v1.show`
- **Request:** none.
- **Response:** `200` `API_READY`. `data.version` is `v1`. `data.sdk` is `/api/v1/sdk`. `data.public` is `/api/v1/public`. Message: "Notifiable API v1 is available."
- **Errors:**
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## GET /api/v1/sdk

- **Auth:** none.
- **Name:** `api.v1.sdk.show`
- **Request:** none.
- **Response:** `200` `API_READY`. `data.version` is `v1`. Message: "Notifiable API v1 is available."
- **Errors:**
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## GET /api/v1/sdk/client-config

- **Auth:** `X-Notifiable-Key` with the app's development or production public key. The matching key chooses the environment.
- **Name:** `api.v1.sdk.client-config.show`
- **Request:** none.
- **Response:** `200` `CLIENT_CONFIG`. Message: "The client configuration was found." `data` includes only a configured side. Android is `{ status, package_name }` and, when `status` is `ready`, `firebase` with `apiKey`, `appId`, `projectId`, and `messagingSenderId`. `status` is `pending`, `ready`, or `failed`. Android is omitted when the app has no package name. iOS is `{ bundle_id }` when the workspace Apple connection exists and the app has a bundle ID. Web is `{ public_key }`. Expo, and a cross-platform app with neither side saved, returns `data` as an empty object.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## POST /api/v1/sdk/deep-links

- **Auth:** `X-Notifiable-Key` with the app's development or production public key. Either key saves the routes for that app.
- **Name:** `api.v1.sdk.deep-links.store`
- **Request:** JSON `{ "links": [{ "path": string, "label": string }] }`. `links` is required and holds at most 50 routes. `path` is the path or URL opened on tap, at most 200 characters, and unique in the list. `label` is the name shown beside it, at most 80 characters. An empty list clears the stored routes. The list replaces whatever was stored before.
- **Response:** `200` `DEEP_LINKS_SAVED`. Message: "The deep links were saved." `data.links` is the stored list.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `422` `VALIDATION_FAILED`, type `validation_error`, when `links` is missing or a path or label is invalid.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## POST /api/v1/sdk/subscribers

- **Auth:** `X-Notifiable-Key` with the app's development or production public key. The matching key chooses the environment.
- **Name:** `api.v1.sdk.subscribers.store`
- **Request:** JSON body. Required: `device_id` (install id, max 80), `platform` (`ios`, `android`, `web`), `sdk` (`expo`, `react-native`, `flutter`, `swift`, `kotlin`, `web`), `permission` (`granted`, `denied`, `not_determined`). Required when `permission` is `granted`: `provider` (`apns`, `fcm`, `expo`, `web_push`) and `token` (max 4096). Optional: `external_id`, `anonymous_id`, `email`, `first_name`, `last_name` (max 80, email max 255), `attributes` (object, up to 20 keys of text, number, boolean, or one nested object; keys max 40), `tags` (up to 20 strings, each max 80), `device_model`, `device_manufacturer` (max 80), `device_ip`, `os_version`, `app_version`, `sdk_version`, `locale`, `language` (max 40), `timezone` (max 80), `country_code` (ISO alpha-2, stored uppercase), `prompted` (boolean; `true` means the SDK finished asking and clears a pending prompt). The same `device_id` updates the existing subscriber.
- **Response:** `201` `SUBSCRIBER_SAVED` for a new device. `200` `SUBSCRIBER_SAVED` when that `device_id` already exists in the app and environment. Message: "The subscriber was saved." `data` has `id`, `environment`, `platform`, `permission`, `sdk`, `provider`, `device_id`, `external_id`, `anonymous_id`, and `prompt_permission`. `prompt_permission` is true when this device should be asked for permission again. The push token is not returned. Sending the same permission on a later save does not clear that request.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `403` `SUBSCRIBER_LIMIT_REACHED`, type `authorization_error`, when a new production subscriber would pass the workspace cap. Development is not capped. An update of an existing device is not capped.
  - `422` `VALIDATION_FAILED`, type `validation_error`, when a field is missing or invalid. `granted` without a token or provider fails here.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## GET /api/v1/sdk/subscribers/{subscriber}

- **Auth:** `X-Notifiable-Key`, same as save.
- **Name:** `api.v1.sdk.subscribers.show`
- **Request:** subscriber id in the path.
- **Response:** `200` `SUBSCRIBER_FETCHED`. Message: "The subscriber was found." `data` has the same fields as save, including `prompt_permission`. Nothing is written.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `404` `SUBSCRIBER_NOT_FOUND`, type `resource_not_found`, when the id is missing or belongs to another app or the other environment.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## PATCH /api/v1/sdk/subscribers/{subscriber}

- **Auth:** `X-Notifiable-Key`, same as save.
- **Name:** `api.v1.sdk.subscribers.update`
- **Request:** any subset of the save fields. At least one field is required. An omitted field stays as stored. `null` clears a nullable field such as `external_id` or `email`. Setting `permission` to `granted` requires `token` and `provider` on this call, or already stored on the subscriber. `prompted: true` clears a pending permission prompt. A permission value that differs from the stored one also clears it.
- **Response:** `200` `SUBSCRIBER_UPDATED`. Message: "The subscriber was updated." `data` has the same fields as save.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `404` `SUBSCRIBER_NOT_FOUND`, type `resource_not_found`, when the id is missing or belongs to another app or the other environment.
  - `422` `VALIDATION_FAILED`, type `validation_error`, when the body is empty or a field is invalid.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## GET /api/v1/sdk/subscribers/{subscriber}/notifications

- **Auth:** `X-Notifiable-Key`, same as save. The matching key chooses the environment.
- **Name:** `api.v1.sdk.subscribers.notifications.index`
- **Request:** optional `page` (integer, minimum 1) and optional `opened` (`true` or `false`). Each page has 20 notifications. `opened=false` returns delivered rows that have not been opened. `opened=true` returns rows that have been opened.
- **Response:** `200` `NOTIFICATIONS_LISTED`. Message: "The notifications were listed." `data.notifications` is that subscriber's deliveries, newest first. Each item has `id`, `title`, `body`, `url`, `image_url`, `data`, `alert`, `status` (`pending`, `delivered`, `failed`, or `skipped`), `opened`, `opened_at`, and `created_at`. `data.page`, `data.per_page`, and `data.total` describe the page. Campaign totals are not included.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `404` `SUBSCRIBER_NOT_FOUND`, type `resource_not_found`, when the subscriber is missing or belongs to another app or the other environment.
  - `422` `VALIDATION_FAILED`, type `validation_error`, when `page` is not a positive integer.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## GET /api/v1/sdk/subscribers/{subscriber}/notifications/{notification}

- **Auth:** `X-Notifiable-Key`, same as save.
- **Name:** `api.v1.sdk.subscribers.notifications.show`
- **Request:** subscriber id and notification id in the path.
- **Response:** `200` `NOTIFICATION_FETCHED`. Message: "The notification was found." `data` has the same fields as one inbox item, including `opened` and `opened_at`.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `404` `SUBSCRIBER_NOT_FOUND`, type `resource_not_found`, when the subscriber is missing or belongs to another app or the other environment.
  - `404` `NOTIFICATION_NOT_FOUND`, type `resource_not_found`, when that subscriber has no delivery for the notification.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## POST /api/v1/sdk/subscribers/{subscriber}/notifications/{notification}/open

- **Auth:** `X-Notifiable-Key` with the app's development or production public key. The matching key chooses the environment.
- **Name:** `api.v1.sdk.subscribers.notifications.open`
- **Request:** none.
- **Response:** `200` `NOTIFICATION_OPENED`. Message: "The open was recorded." `data.id` is the notification id. The first open sets `opened_at` and stores one `notification_opened` event for that subscriber and delivery. A second open does not change the first time and does not store another event.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `404` `SUBSCRIBER_NOT_FOUND`, type `resource_not_found`, when the subscriber is missing or belongs to another app or the other environment.
  - `404` `NOTIFICATION_NOT_FOUND`, type `resource_not_found`, when that subscriber has no delivered notification.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## POST /api/v1/sdk/deliveries/{delivery}/click

- **Auth:** `X-Notifiable-Key` with the app's development or production public key. The matching key chooses the environment.
- **Name:** `api.v1.sdk.deliveries.click`
- **Request:** none. `{delivery}` is the id appended to the notification URL as `opt`.
- **Response:** `200` `DELIVERY_CLICKED`. Message: "The click was recorded." `data.id` is the delivery id. The first click sets `clicked_at` and stores one `notification_clicked` event. A second click does not change the first time and does not store another event.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `404` `NOTIFICATION_NOT_FOUND`, type `resource_not_found`, when the delivery is missing, not delivered, or belongs to another app or the other environment.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

## POST /api/v1/sdk/events

- **Auth:** `X-Notifiable-Key` with the app's development or production public key. The matching key chooses the environment.
- **Name:** `api.v1.sdk.events.store`
- **Request:** JSON body. Required: `name` (max 80) and `subscriber_id`. Optional: `properties`, an object of at most 20 values. A value is text (max 200), a number, a boolean, or null.
- **Response:** `201` `EVENT_RECORDED`. Message: "The event was recorded." `data` has `id`, `name`, and `subscriber_id`. A saved automation for that event name sends only when this subscriber is in its segment.
- **Errors:**
  - `401` `INVALID_PUBLIC_KEY`, type `authentication_error`, when the key is missing, unknown, reset, or belongs to a deleted app.
  - `404` `SUBSCRIBER_NOT_FOUND`, type `resource_not_found`, when the subscriber is missing or belongs to another app or the other environment.
  - `422` `VALIDATION_FAILED`, type `validation_error`, when a field is missing or invalid.
  - `429` `RATE_LIMIT_EXCEEDED`, type `rate_limit_exceeded`, when the caller exceeds 60 requests per minute from one IP.
  - `500` `SERVER_ERROR`, type `server_error`, when the request fails unexpectedly. The body does not include the exception text.

Other `/api/*` failures use the same envelope. A missing route is `404` `ROUTE_NOT_FOUND`, type `route_not_found`. Invalid fields are `422` `VALIDATION_FAILED`, type `validation_error`, with `error.details` listing the field, rule code, and message. A missing model is `404` `RESOURCE_NOT_FOUND`, type `resource_not_found`, unless the action throws a more specific code such as `SUBSCRIBER_NOT_FOUND`.

The previous `/api/v1/subscribers`, `/api/v1/events`, `/api/v1/client-config`, `/api/v1/deep-links`, `/api/v1/deliveries`, and `/api/v1/notifications` paths are not kept. Server routes are documented with each feature and rendered in the Server group of `/docs/api`.
