Server push
Send a push to your users, a list of devices or a segment, then read its delivery report. Campaigns built in the dashboard are covered in Push campaigns.
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.
Availability: an active paid plan on grovs Cloud. Always on for self-hosted Enterprise. Segment audiences need ClickHouse.
Rate limit: each project has a budget per minute of 600 campaigns, 10 segment campaigns and 100,000 list recipients. A refused request creates nothing and uses no budget.
IDs: campaign and segment IDs are hashids, such as k9Qx2. link_id is the numeric link ID from Get link details.
The push campaign object
idstringHashid of the campaign.
namestringInternal label, up to 255 characters. Defaults to the title.
titlestringNotification title, up to 255 characters.
bodystringNotification text, up to 1000 characters. The whole APNs payload must fit in 4096 bytes, so a long body with a long image_url or data is refused with 422.
image_urlstring | nullAn https:// image URL, up to 2048 characters. Use PNG, JPEG or GIF. iOS needs a Notification Service Extension in your app to show it.
imagestring | nullRead-only. The image the push carries.
soundstring | nulldefault or the name of a sound file bundled in your app, up to 64 characters.
link_idinteger | nullA grovs link of this project. Its URL travels in the payload as grovs_link.
dataobjectCustom keys delivered in the payload, at most 2048 bytes as JSON. grovs_campaign_id, grovs_link and grovs_image are overwritten. aps, from, notification, message_type and keys starting with google, gcm or __rpush_ are refused.
sourceenumapi for campaigns sent through this API.
statusenumscheduled, evaluating, sending, sent, failed or cancelled.
audienceobjecttype, recipients_count, segment_id, segment_definition_hash and recipients.
scheduled_atstring | nullWhen the campaign goes out, ISO 8601.
local_timebooleanDeliver at the wall-clock time of scheduled_at in each device's own timezone.
timezonestringIANA zone the wall-clock is read in, and the fallback for devices with no timezone. Defaults to UTC.
reportobjectDelivery counters. See below.
evaluated_atstring | nullWhen the audience was frozen.
started_atstring | nullWhen sending started.
finished_atstring | nullWhen every device had been handed off, or the campaign was cancelled.
failure_reasonstring | nullFor a failed campaign: segment_busy, segment_invalid, query_too_heavy or evaluation_error.
created_atstringISO 8601.
updated_atstringISO 8601.
targetedintegerEvery resolved device, held people excluded.
no_channelintegerDevices with no push token, or on web or desktop. Skipped.
not_foundintegerRecipients that matched no visitor, malformed UUIDs included.
queuedintegerHanded to the push process.
deliveredintegerAccepted by APNs or FCM. Not the same as shown on screen.
failedintegerRejected or not sent. error_counts gives the reasons.
openedintegerDevices whose SDK reported a tap, once per device.
cancelledintegerDevices dropped by a cancel before they were handed off.
heldintegerPeople kept back as a control. Always 0 for server pushes.
waitingintegerPeople waiting for an A/B winner. Always 0 for server pushes.
error_countsobjectFailures by reason. APNs reasons keep their name (Unregistered, BadDeviceToken). FCM failures are keyed by HTTP status (404). capped counts pushes over the project's daily cap; no_app and no_token count devices that could not be sent to.
audienceobjectpersons, devices and incomplete for a segment audience. incomplete is true when a segment window reached past your data retention.
Per-device rows are kept for 30 days. The counters stay.
The object also has A/B test and goal fields. They are null for server pushes.
{
"id": "k9Qx2",
"name": "Flu shot reminder",
"title": "Flu season is here",
"body": "Book your flu shot in two taps.",
"image_url": "https://cdn.medly.health/push/flu.png",
"image": "https://cdn.medly.health/push/flu.png",
"sound": "default",
"link_id": 4821,
"data": { "screen": "book", "service": "flu_shot" },
"source": "api",
"status": "sent",
"scheduled_at": null,
"local_time": false,
"timezone": "UTC",
"evaluated_at": "2026-10-22T09:12:05.000Z",
"started_at": "2026-10-22T09:12:04.000Z",
"finished_at": "2026-10-22T09:20:41.000Z",
"failure_reason": null,
"created_at": "2026-10-21T15:02:11.000Z",
"updated_at": "2026-10-22T09:20:41.000Z",
"control_group": null,
"conversion": null,
"variants": null,
"test_share": null,
"winner_after_minutes": null,
"winner_by": null,
"winner_due_at": null,
"winner": null,
"audience": {
"type": "segment",
"recipients_count": 0,
"segment_id": "s9Kq1",
"segment_definition_hash": "9c41e0b7…",
"recipients": []
},
"report": {
"targeted": 41230,
"no_channel": 1204,
"not_found": 0,
"queued": 36020,
"delivered": 35511,
"failed": 509,
"opened": 2210,
"cancelled": 0,
"held": 0,
"waiting": 0,
"error_counts": { "Unregistered": 474, "404": 35 },
"audience": { "persons": 38120, "devices": 41230, "incomplete": false }
}
}Send a push from your server
/api /v1 /sdk /server /pushCreates and sends a campaign in one call. Use it for transactional pushes such as "your test results are ready".
The campaign has source: "api". It takes no variants, goal or control group. The project-wide control group does not hold it back.
The response is 202 with status evaluating, or scheduled when you pass scheduled_at. A background job resolves the audience within a couple of seconds. Poll Retrieve a server push for progress.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
titlestringRequiredUp to 255 characters.
bodystringRequiredUp to 1000 characters.
audienceobjectRequiredWho gets the push. One of:
{ "type": "identifiers", "recipients": [...] }: the user IDs your app set through the SDK.{ "type": "visitors", "recipients": [...] }: visitor UUIDs.{ "type": "segment", "segment_id": "…" }: an active segment.
Lists hold 1 to 10,000 non-empty strings of at most 256 bytes. An identifier reaches at most 50 of its devices.
namestringDefaults to the title.
image_urlstringhttps:// only. The server API takes no file upload.
soundstringdefault or a bundled sound file name.
link_idintegerA link of this project.
dataobjectCustom payload keys, at most 2048 bytes as JSON.
scheduled_atstringISO 8601, in the future and at most 30 days away.
local_timebooleanDeliver at the wall-clock time of scheduled_at in each device's timezone. Needs scheduled_at.
timezonestringIANA zone name. Defaults to UTC.
Returns 202 with a campaign object.
Errors
curl -X POST "https://sdk.sqd.link/api/v1/sdk/server/push" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production" \
-H "Content-Type: application/json" \
-d '{
"title": "Your test results are ready",
"body": "Tap to see them in Medly.",
"link_id": 5120,
"data": { "screen": "results", "result_id": "r_88213" },
"audience": {
"type": "identifiers",
"recipients": ["member-4471"]
}
}'{
"campaign": {
"id": "pR4tz",
"name": "Your test results are ready",
"title": "Your test results are ready",
"body": "Tap to see them in Medly.",
"source": "api",
"status": "evaluating",
"link_id": 5120,
"data": { "screen": "results", "result_id": "r_88213" },
"audience": {
"type": "identifiers",
"recipients_count": 1,
"segment_id": null,
"segment_definition_hash": null,
"recipients": ["member-4471"]
},
"report": { "targeted": 0, "queued": 0, "delivered": 0, "…": "…" },
"…": "…"
}
}{ "error": "recipients: 100000 per minute", "error_code": "rate_limited" }Retrieve a server push
/api /v1 /sdk /server /push /{campaign_id}Returns a campaign of the project with its live report.
PROJECT-KEYstringRequiredYour project key.
ENVIRONMENTstringRequiredproduction or test.
campaign_idstringRequiredThe id from the send response.
Returns 200 with a campaign object and its live report.
Errors
curl -X GET "https://sdk.sqd.link/api/v1/sdk/server/push/{campaign_id}" \
-H "PROJECT-KEY: $GROVS_PROJECT_KEY" \
-H "ENVIRONMENT: production"{
"campaign": {
"id": "pR4tz",
"status": "sent",
"report": {
"targeted": 2,
"no_channel": 0,
"not_found": 0,
"queued": 2,
"delivered": 2,
"failed": 0,
"opened": 1,
"cancelled": 0,
"held": 0,
"waiting": 0,
"error_counts": {},
"audience": { "persons": null, "devices": null, "incomplete": false }
},
"…": "…"
}
}