Scope
The In-App admin routes are under /v1/apps/{app_id}/in-app-messages and accept an admin session or a REST key (Authorization: Bearer rk_...), so a backend or a script can manage messages without the dashboard. Every route is scoped to the App in the path. The field-by-field request and response shapes are in the generated In App API Reference.
Devices do not use these routes. The SDK-facing routes (eligible and events) use a Client key and are described in the In-App Messages Contract.
Create and activate
POST /v1/apps/{app_id}/in-app-messages creates a draft by default, or an active message when the body has "status": "active":
curl -X POST https://your-instance/v1/apps/{app_id}/in-app-messages \
-H "Authorization: Bearer $REST_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example announcement",
"format": "top",
"title": "Example title",
"body": "Example body.",
"buttons": [
{ "label": "Open", "action": { "type": "deeplink", "value": "exampleapp://offers/42" } }
],
"platforms": ["ios", "android"],
"frequency": { "type": "once" }
}'- Unknown fields are rejected.
formatis the layout:top,center(default in the dashboard),bottomorfull.frequencyisonce,n_times(withmax) orinterval(withinterval_seconds). - The image is either
media_id(an upload from the Media API) or animage_urloverhttps, never both. - Button ids are assigned by the server and stay stable across edits.
- Links accept
httpsand custom app schemes;http,javascript,data,file,vbscript,blobandaboutare rejected. - A button action is
url,deeplinkorpush_permission.push_permissionasks the device for the notification permission, takes novalue(a value answers422 invalid_content, withbuttons[0].action.value: invalid_action_valueindetails.fieldsfor the first button) and is not accepted onimage_action. The server hides a message with this action from devices that already receive push and from devices on an SDK that does not support it yet; see the contract. - A rejected body answers
422 invalid_contentwithdetails.fieldsmapping each field to a code.
PATCH .../{id} updates content (an omitted key keeps its value, null clears media_id, image_url, starts_at, ends_at, style, image_action and dismiss_after_seconds; buttons, platforms, segment_ids, exclude_segment_ids, frequency, style, image_action and trigger are replaced whole, and null or [] empties the two segment lists). Status changes go through POST .../{id}/activate and POST .../{id}/pause. Edits to an active message reach devices on their next fetch. DELETE .../{id} soft-deletes the message: it leaves lists and devices, its metrics stay readable by id, and the segments it used can be deleted again.
Look, behavior and trigger
Four optional fields shape how the SDK shows the message. They are part of the same create and PATCH body:
{
"format": "center",
"style": {
"background_color": "#102030",
"text_color": "#FFFFFF",
"button_color": "#E94B8B",
"button_text_color": "#FFFFFF",
"text_align": "center",
"margin": true
},
"image_url": "https://cdn.example.com/offer.png",
"image_action": { "type": "url", "value": "https://example.com/offer" },
"dismiss_after_seconds": 8,
"trigger": { "type": "event", "name": "checkout_done" }
}styleholds up to five tokens (background_color,text_color,button_color,button_text_coloras#RRGGBB;text_alignasleft,centerorright) andmargin. The set is closed: an unknown key answers422 invalid_contentwithstyle.<key>: unknown. A color that is not#RRGGBBanswersstyle.<key>: invalid_color, an alignment outside the three valuesstyle.text_align: invalid_align. Colors are stored in upper case.styleisnullwhen unset, and aPATCHthat sends an object replaces the whole style, so a token you leave out is cleared.image_actionmakes the image tappable. It has the shape and the link rules of a button action and needs an image (image_action: requires_imageotherwise). Clear the action together with the image.dismiss_after_secondscloses the message after 1 to 300 seconds on screen (out_of_rangeotherwise).nullkeeps it until the user closes it.triggeris{"type": "session_start"}(the default, and the value the API returns when none is set) or{"type": "event", "name": "..."}with a name of 1 to 64 characters froma-z,0-9,_,.and-. Codes:trigger: invalid_type,trigger.name: required,invalid_name,too_long, andnot_allowedfor a name on asession_starttrigger."trigger": nullon aPATCHsets session start.
GET /v1/apps/{app_id}/in-app-messages/trigger-events returns { "events": ["checkout_done", "..."] }: the distinct event names used by the App's messages that are not deleted, sorted, at most 50. Event names are not registered anywhere else; the list only feeds suggestions.
How the SDK uses these fields is in the In-App Messages Contract.
Targeting with segments
A message targets the same segments you use for push. segment_ids lists the segments it is shown to and exclude_segment_ids lists the ones whose devices never see it:
- A device sees the message when it matches at least one segment in
segment_ids, or when that list is empty (every device), and it matches none of the segments inexclude_segment_ids. - Each list holds up to 10 segments. A segment cannot repeat or appear in both lists. The fields answer
422 invalid_contentwith the codesinvalid_id,duplicate_id,included_and_excludedortoo_many. - A segment of another App, or one that does not exist, answers
404 segment_not_found. - A segment whose stored rules are no longer valid never matches. A message that includes it is not shown, and a message that excludes it is not shown either, so an unreadable exclusion never widens the audience.
- A segment used by a message that is not deleted cannot be deleted (
409 segment_in_use).
Plan limit
The plan limits how many messages can be active at once per App. Activating past the limit, by activate or by creating an active message, answers:
{ "error": { "code": "in_app_limit_exceeded", "message": "...", "details": { "reason": "in_app_limit_exceeded" } } }with status 402. An inactive subscription answers 402 subscription_inactive. Drafts and paused messages do not use a slot, and a message past its ends_at stops counting.
Changing to a plan that allows fewer active messages than an App currently has is not rejected. When the change takes effect, Notti pauses the messages above the new limit, keeping the oldest ones active (by creation date). The pause is applied by a background check that runs about every 5 minutes, so it can lag the plan change slightly. Paused messages keep their content and metrics, and you can reactivate them while the App is under its limit.
List and filter
GET /v1/apps/{app_id}/in-app-messages returns messages newest first, with cursor pagination (limit 1 to 100, cursor from next_cursor).
statusfilters on the display status:all(default),draft,active,scheduled,pausedorcompleted.scheduledis an active message whosestarts_atis still in the future, andcompletedis an active message whoseends_athas passed. The storedstatusfield is onlydraft,activeorpaused; each item also carriesdisplay_status.searchmatches the message name (up to 200 characters).- Each item has all-time
impressions,reachandctr, and the response hasactive_countandactive_limit(nullwhen there is no limit).
Audience estimate
POST /v1/apps/{app_id}/in-app-messages/audience-estimate with { "segment_ids": ["..."], "exclude_segment_ids": ["..."], "platforms": ["ios"] } returns { "reach": N }: how many of the App's devices the audience matches now, with the same include and exclude rules as above. Leave segment_ids empty for all devices and platforms empty for every platform. Devices count whether or not they allow push.
Metrics
GET /v1/apps/{app_id}/in-app-messages/{id}/metrics?range=14d returns totals, a zero-filled daily series, per-button clicks (a row with button_id: "image" comes first when the message has an image action) and breakdowns. totals carries dismissers, the devices that closed the message themselves, and auto_dismissed, the devices that only saw it close on its timer. range is 7d, 14d (default), 30d, 90d, or custom with from and to (YYYY-MM-DD, to inclusive). Deleted, paused and ended messages stay readable. The metric definitions are in What the metrics mean.