In-app messages
Drive the whole lifecycle of a message from code: draft it, send a test, launch it, pause or end it, and read its numbers. What a message can show and when is explained in the In-app messages guide.
These endpoints live on the SDK host: https://sdk.sqd.link on grovs Cloud, https://sdk.<SERVER_HOST> when you self-host. Send your project key in the PROJECT-KEY header. Send production or test in the ENVIRONMENT header. Each environment has its own messages.
Availability: an active paid plan on grovs Cloud. Always on for self-hosted Enterprise. Without it, every call answers 403 with error_code: "enterprise_required". Reports need ClickHouse.
Rate limit: 120 requests a minute per project key, shared with segments. Over it, the answer is 429 with error_code: "rate_limited" and a Retry-After header.
Images: put https:// image URLs in content.image. The server API takes no file upload.
Dashboard only: image and media uploads, survey answers and comments, experiment results, duplicate, audience update and project settings.
IDs: message IDs are hashids, such as m7Kp2. Journey steps are not listed and cannot be changed here.
The in-app message object
idstringHashid of the message.
namestringInternal label, up to 255 characters.
statusenumdraft, preparing (a segment audience is being resolved), scheduled, live, paused or ended.
templateenumThe format. See The definition. Fixed after launch.
contentobjectWhat the message shows.
triggersarrayWhat shows it.
conditionsarrayChecks on the device at trigger time.
capsobjectHow often one person sees it.
scheduleobjectDays and hours it may show, in device time.
priorityinteger1 (lowest) to 10. Default 1.
delay_secondsintegerWait after the trigger, 0 to 3600. Default 0.
start_atstring | nullISO 8601. null starts at launch.
end_atstring | nullISO 8601. null runs until you end it.
audienceobjecttype (all or segment), segment_id, segment_definition_hash and segment_changed, true when the segment was edited after launch.
revisionintegerGoes up when a launched message changes what devices show.
lock_versionintegerSend it back on update to refuse a save over someone else's change.
launched_atstring | nullISO 8601.
ended_atstring | nullISO 8601.
failure_reasonstring | nullWhy a segment message went back from preparing to draft: audience_busy, segment_invalid or audience_error.
control_groupinteger | nullPercent of the audience held back, 1 to 50. Fixed after launch.
conversionobject | nullThe goal, such as { "event": "checkout", "window_hours": 72 }. See Experiments.
variantsarray | nullUp to two extra contents, { "id": "b" | "c", "content": {…} }. The message's own content is variant a.
created_atstringISO 8601.
updated_atstringISO 8601.
{
"id": "m7Kp2",
"name": "Plans nudge",
"status": "live",
"template": "interstitial",
"content": {
"title": "Your plan is one tap away",
"message": "Unlimited video visits from 9.99 a month.",
"image": { "portrait": "https://cdn.medly.health/inapp/plans.png", "landscape": null },
"buttons": [
{ "id": "see_plans", "text": "See plans", "action": { "type": "open_url", "url": "medly://plans" } }
],
"close_button": true,
"backdrop": "dim"
},
"triggers": [{ "type": "screen_view", "name": "Plans" }],
"conditions": [
{ "type": "event_count", "name": "plans_viewed", "op": "gt", "value": 2 },
{ "type": "user_attribute", "key": "subscribed", "op": "neq", "value": true }
],
"caps": { "session": 1, "day": 1, "week": null, "lifetime": 3, "ignore_global": false },
"schedule": {},
"priority": 5,
"delay_seconds": 2,
"start_at": null,
"end_at": null,
"audience": { "type": "all", "segment_id": null, "segment_definition_hash": null, "segment_changed": false },
"revision": 1,
"lock_version": 3,
"launched_at": "2026-10-22T09:30:00.000Z",
"ended_at": null,
"failure_reason": null,
"control_group": null,
"conversion": null,
"variants": null,
"created_at": "2026-10-22T09:00:00.000Z",
"updated_at": "2026-10-22T09:30:00.000Z"
}The definition
A draft may be incomplete. Launch, and every save after it, checks the whole definition. The format is the one the composer writes.
templateenumcover, interstitial (default), half_interstitial, header, footer, alert, cover_image, interstitial_image, half_interstitial_image or html. See Formats.
titlestring | nullUp to 256 characters; 80 for the alert template.
messagestring | nullUp to 1024 characters; 300 for alert. Title and message may use fields such as {{ user.first_name | default: "there" }}.
imageobjectportrait and landscape, each an https:// URL or null.
buttonsarrayUp to two { "id", "text", "action", "colors" }. id is 1 to 16 of a-z, 0-9, _. text is up to 64 characters; 30 for alert.
tap_actionobject | nullAn action for a tap on the message itself.
colorsobjectbackground, title, message as #RRGGBB or #RRGGBBAA.
close_buttonbooleanDefault true.
dismissiblebooleanClose by tapping outside or swiping. Default true.
backdropenumdim (default) or none.
orientationenumany, portrait or landscape.
questionobject | nullStars or a choice. See Surveys and ratings.
htmlstringThe page for the html template, up to 64 KB, with layout: full, modal, banner_top or banner_bottom. See Custom HTML.
An action is { "type": "close" }, { "type": "open_url", "url": "…" }, { "type": "key_value", "payload": {…} } (up to 20 keys), or request_push_permission, request_app_rating, open_store (apps only).
triggersarray1 to 5. Any one shows the message. { "type": "app_open" }, { "type": "screen_view", "name": "Plans" } (omit name for any screen) or { "type": "event", "name": "cart_viewed", "filters": [...] } with up to 5 filters.
conditionsarrayUp to 10, all must hold. type is platform, language, app_version, os_version, event_count, user_attribute or first_seen, with op and value.
audienceobject{ "type": "all" } or { "type": "segment", "segment_id": "gk3x" } with an active segment. Only a draft's audience can change.
capsobjectsession, day, week, lifetime (views per person, null for no cap; day and week up to 20) and ignore_global (not held back by the project limit).
scheduleobjectdays (1 is Monday, 7 is Sunday), from and to as HH:MM, in each device's local time. {} for any time.
| Limit | Value |
|---|---|
Definition size, without html | 16 KB |
html page | 64 KB |
| Triggers | 5 |
| Conditions | 10 |
| Buttons | 2 |
| Variants | 2 |
| Running messages per project | 50 |
| Segments targeted at once per project | 20 |
{
"name": "Welcome",
"template": "interstitial",
"content": {
"title": "Hello",
"message": "Welcome to Medly",
"image": { "portrait": "https://cdn.medly.health/inapp/welcome.png", "landscape": null }
},
"triggers": [{ "type": "app_open" }]
}List in-app messages
/api /v1 /sdk /server /in_app_campaignsReturns messages newest first, a page at a time. Journey steps are left out.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
statusenumOnly messages in this status.
limitinteger1 to 200. Default 200.
afterstringnext_after from the previous page.
Returns 200 with in_app_campaigns, truncated (true when there is another page) and next_after. List rows leave out content.html.
Errors
curl -X GET "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns?status=live&limit=50" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production"{
"in_app_campaigns": [
{
"id": "m7Kp2",
"name": "Plans nudge",
"status": "live",
"template": "interstitial",
"…": "…"
}
],
"truncated": true,
"next_after": "m7Kp2"
}Create an in-app message
/api /v1 /sdk /server /in_app_campaignsSaves a new message as a draft. Nothing reaches devices until you launch it.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
namestringRequiredUp to 255 characters.
templateenumDefault interstitial.
contentobjectSee content.
triggersarray1 to 5 triggers.
conditionsarrayUp to 10 device conditions.
audienceobjectDefault { "type": "all" }.
capsobjectFrequency limits.
scheduleobjectDays and hours.
priorityinteger1 to 10.
delay_secondsinteger0 to 3600.
start_atstringISO 8601, such as 2026-11-01T09:00:00Z.
end_atstringISO 8601, after start_at.
control_groupinteger | null1 to 50.
conversionobject | nullThe goal.
variantsarray | nullUp to two { "id": "b" | "c", "content": {…} }.
Returns 201 with in_app_campaign, an in-app message object.
Errors
curl -X POST "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production" \
-H "Content-Type: application/json" \
-d '{
"name": "Plans nudge",
"template": "interstitial",
"content": {
"title": "Your plan is one tap away",
"message": "Unlimited video visits from 9.99 a month.",
"image": {
"portrait": "https://cdn.medly.health/inapp/plans.png",
"landscape": null
},
"buttons": [
{
"id": "see_plans",
"text": "See plans",
"action": { "type": "open_url", "url": "medly://plans" }
}
]
},
"triggers": [
{ "type": "screen_view", "name": "Plans" }
],
"conditions": [
{
"type": "event_count",
"name": "plans_viewed",
"op": "gt",
"value": 2
}
],
"audience": { "type": "segment", "segment_id": "gk3x" },
"caps": {
"session": 1,
"day": 1,
"week": null,
"lifetime": 3,
"ignore_global": false
},
"priority": 5,
"delay_seconds": 2
}'{
"in_app_campaign": {
"id": "m7Kp2",
"name": "Plans nudge",
"status": "draft",
"revision": 1,
"lock_version": 0,
"audience": { "type": "segment", "segment_id": "gk3x", "segment_definition_hash": null, "segment_changed": false },
"launched_at": null,
"…": "…"
}
}{
"error": "content must be an object"
}Retrieve an in-app message
/api /v1 /sdk /server /in_app_campaigns /{campaign_id}Returns one message with its full content, html included.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredThe message's id.
Returns 200 with in_app_campaign, an in-app message object.
Errors
curl -X GET "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns/{campaign_id}" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production"{
"error": "In-app campaign not found"
}Update an in-app message
/api /v1 /sdk /server /in_app_campaigns /{campaign_id}Changes only the fields you send. Each field replaces the stored one whole, so send all of content to change one part of it. On a launched message, devices pick up the change at their next sync and revision goes up.
After launch, template, audience, control_group and the number of variants are fixed. An ended message cannot be edited.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredThe message's id.
lock_versionintegerThe lock_version you read. If the message changed since, the save is refused with 409.
Any body field of Create an in-app message is accepted.
Returns 200 with in_app_campaign, an in-app message object.
Errors
curl -X PATCH "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns/{campaign_id}" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production" \
-H "Content-Type: application/json" \
-d '{
"lock_version": 3,
"priority": 8,
"end_at": "2026-11-30T23:59:59Z"
}'{
"in_app_campaign": {
"id": "m7Kp2",
"priority": 8,
"end_at": "2026-11-30T23:59:59.000Z",
"revision": 2,
"lock_version": 4,
"…": "…"
}
}{
"error": "Campaign changed while you were editing it; reload and retry"
}Delete a draft
/api /v1 /sdk /server /in_app_campaigns /{campaign_id}Deletes a message that was never launched. A launched message cannot be deleted; end it instead to keep its report.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredThe message's id.
Returns 200 with a message.
Errors
curl -X DELETE "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns/{campaign_id}" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production"{
"message": "Deleted"
}{
"error": "Only a draft can be deleted"
}Launch an in-app message
/api /v1 /sdk /server /in_app_campaigns /{campaign_id} /launchChecks the whole definition and starts delivery. An all audience goes live, or scheduled when start_at is in the future. A segment audience goes preparing while its members are written, then live, or back to draft with a failure_reason. The audience is frozen at launch.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredA draft's id.
Returns 202 with in_app_campaign, an in-app message object.
Errors
curl -X POST "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns/{campaign_id}/launch" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production"{
"in_app_campaign": {
"id": "m7Kp2",
"status": "preparing",
"launched_at": "2026-10-22T09:30:00.000Z",
"audience": { "type": "segment", "segment_id": "gk3x", "segment_definition_hash": "a1b2c3d4…", "segment_changed": false },
"…": "…"
}
}{
"error": "The segment is archived"
}Pause an in-app message
/api /v1 /sdk /server /in_app_campaigns /{campaign_id} /pauseStops delivery of a live message. Devices in use stop showing it usually within a minute, and at most about 15 minutes on an idle device.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredThe message's id.
Returns 200 with the message, now paused.
Errors
curl -X POST "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns/{campaign_id}/pause" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production"{
"in_app_campaign": { "id": "m7Kp2", "status": "paused", "…": "…" }
}Resume an in-app message
/api /v1 /sdk /server /in_app_campaigns /{campaign_id} /resumePuts a paused message back to live.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredThe message's id.
Returns 200 with the message, now live.
Errors
curl -X POST "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns/{campaign_id}/resume" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production"{
"in_app_campaign": { "id": "m7Kp2", "status": "live", "…": "…" }
}End an in-app message
/api /v1 /sdk /server /in_app_campaigns /{campaign_id} /endStops a launched message for good. The report stays readable. An ended message cannot be edited or relaunched.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredThe message's id.
Returns 200 with the message, now ended, and ended_at set.
Errors
curl -X POST "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns/{campaign_id}/end" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production"{
"in_app_campaign": {
"id": "m7Kp2",
"status": "ended",
"ended_at": "2026-11-02T17:45:10.000Z",
"…": "…"
}
}Send a test
/api /v1 /sdk /server /in_app_campaigns /{campaign_id} /testShows the last saved version of a message to one person, in any status. It reaches each of their devices, up to 50, at the next sync within 30 minutes. Triggers, conditions, limits and schedule are ignored. It shows once and is not counted in the report.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredThe message's id.
identifierstringRequiredThe user ID your app sets through the SDK.
Returns 202 with devices, the number of devices that will get it.
Errors
curl -X POST "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns/{campaign_id}/test" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production" \
-H "Content-Type: application/json" \
-d '{ "identifier": "member-4471" }'{
"devices": 2
}{
"error": "Nobody in this project has that identifier"
}Get the report
/api /v1 /sdk /server /in_app_campaigns /{campaign_id} /reportReturns views, clicks and dismissals for a date range, with a trend and splits by platform and button. What counts as each is in Reporting. Answers are cached for 60 seconds.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredThe message's id.
fromstringYYYY-MM-DD, UTC. Default: the launch day, at most 91 days back.
tostringYYYY-MM-DD, UTC. Default: today. The range covers at most 92 days.
byenumday (default) or hour. The trend bucket.
Returns 200 with report:
totals:views,clicks,dismissals,unique_viewers,unique_clickers,click_rate.trendandplatforms: the same counts per bucket and per platform.buttons: clicks perbutton_id.nullis a tap on the message itself.eligible_devices: devices in a segment audience.nullforall.answers: survey results.nullfor a message that asks no question.
Errors
curl -X GET "https://sdk.sqd.link/api/v1/sdk/server/in_app_campaigns/{campaign_id}/report?from=2026-10-22&to=2026-10-28" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production"{
"report": {
"totals": {
"views": 1840,
"clicks": 412,
"dismissals": 960,
"unique_viewers": 1502,
"unique_clickers": 398,
"click_rate": 0.2239
},
"trend": [
{ "bucket": "2026-10-22", "views": 310, "clicks": 74, "dismissals": 158 }
],
"platforms": [
{ "platform": "ios", "views": 1210, "clicks": 290, "dismissals": 611 },
{ "platform": "web", "views": 630, "clicks": 122, "dismissals": 349 }
],
"buttons": [
{ "button_id": "see_plans", "clicks": 371 },
{ "button_id": null, "clicks": 41 }
],
"eligible_devices": 8420,
"answers": null
}
}