Notify
POST https://api.pushwoosh.com/messaging/v2/notify
Creates and schedules a single message.
Request structure
Anchor link toThe request body is a NotifyRequest with exactly one of two kinds:
segment: target an audience segment by segment code, or — without creating a segment first — a seglang expression or a structured filter expression.transactional: send to an explicit list of hwids, user IDs, push tokens, or test devices.
{ "segment": { ... }, // OR "transactional": { ... }, "transaction_id": "unique-uuid"}| Field | Type | Description |
|---|---|---|
transaction_id | string | Optional. Idempotency key for the request — works with both segment and transactional. A repeat call with the same transaction_id within 5 minutes returns the original message_code instead of sending a duplicate message. Use a UUID or another value unique per logical send. |
NotifySegment
Anchor link toTargets users who match an audience segment or filter expression. Set exactly one of code, expression, or filter_expression. For expression and filter_expression, no segment needs to be created beforehand — the expression is evaluated inline for this send only.
| Field | Type | Description |
|---|---|---|
schedule | Schedule | When and how to send. Required. |
application | string | Application code. |
platforms | array of Platform | Platforms the message targets. |
code | string | Segment code of a segment saved in advance. Mutually exclusive with expression and filter_expression. |
expression | string | Seglang expression, evaluated for this send only — no saved segment required. Mutually exclusive with code and filter_expression. |
filter_expression | FilterExpression | The same logic as expression, as a structured object instead of a seglang string. Mutually exclusive with code and expression. The FilterExpression schema isn’t publicly documented — request it from Pushwoosh support if you need the structured form. |
payload | Payload | Push / SMS / Telegram / Kakao / LINE / WhatsApp / Viber / Facebook Messenger payload. Mutually exclusive with email_payload. |
email_payload | EmailPayload | Email payload. |
campaign | string | Campaign code to attribute this message to. |
campaign_name | string | Internal name for this message, shown as the row title in Message History and in its export. Unrelated to campaign above. It doesn’t group messages or affect delivery. Max 255 characters, trimmed. Omit it or leave it empty to keep the row titled from the payload’s own content instead. |
frequency_capping | FrequencyCapping | Per-user frequency limits. |
send_rate | SendRate | Throttling for the send. |
message_type | MessageType | MESSAGE_TYPE_MARKETING (default) or MESSAGE_TYPE_TRANSACTIONAL. Controls control-group filtering. |
dynamic_content_placeholders | map<string, string> | Replaces placeholders in content. |
meta_data | object | Free-form metadata forwarded to downstream analytics. |
use_latest_user_device | bool | When true, delivers the message to each user’s most recently active device (the one with the latest Last Application Open) instead of every device matched by the segment. Scoped to platforms: only devices on those platforms are considered, and if none has Last Application Open data, the first matching device is used instead of dropping the send. Defaults to false (send to every device). |
geo_zone | GeoZone | Optional. Narrows the audience to devices whose last recorded location is inside a circle you define in this request. It is combined with code, expression, or filter_expression by AND: a device must match the segment and be inside the circle. Devices with no recorded location are skipped. Not available in NotifyTransactional. |
control_group | string | Code or name of a send’s own hold-out group to apply to this message. It doesn’t narrow which control groups apply: every enabled control group of the application holds users out of marketing sends whether this field is set or not. Mutually exclusive with holdout_percentage. |
holdout_percentage | uint32 | Draws this share of the segment, 1–20, into a control group scoped to this send, on top of the application’s enabled control groups, to measure a single campaign’s impact without spending down the shared ones. 0 (default) leaves it off. Mutually exclusive with control_group. |
Devices drawn into the holdout are excluded from delivery the same way as by the application’s control groups. They aren’t sent to, and the tracking log records the drop as Device is in control group. A message_type of MESSAGE_TYPE_TRANSACTIONAL skips this exclusion entirely, same as it does for the application’s control groups.
Setting holdout_percentage together with control_group, or a holdout_percentage above 20, returns 400:
{ "code": 3, "message": "invalid argument", "details": [ { "@type": "type.googleapis.com/google.rpc.BadRequest", "fieldViolations": [ { "field": "holdout_percentage", "description": "holdout_percentage and control_group are mutually exclusive" } ] }, { "@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "InvalidHoldoutPercentage", "domain": "api.pushwoosh.com", "metadata": {} } ]}Example: Send to a segment
Anchor link tocurl -X POST https://api.pushwoosh.com/messaging/v2/notify \ -H "Authorization: Token YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "segment": { "application": "XXXXX-XXXXX", "platforms": ["IOS", "ANDROID"], "code": "active_users", "payload": { "content": { "localized_content": { "en": { "ios": { "body": "Hello!" }, "android": { "body": "Hello!" } } } } }, "schedule": { "at": "2026-05-01T12:00:00Z" }, "message_type": "MESSAGE_TYPE_MARKETING" } }'NotifyTransactional
Anchor link toSends to an explicit list of recipients.
| Field | Type | Description |
|---|---|---|
schedule | Schedule | Required. |
application | string | Application code. |
platforms | array of Platform | Platforms the message targets. |
test_devices | bool | If true, send to the app’s test devices only. |
hwids | { "list": [string, ...] } | Send to these hwids only. |
users | { "list": [string, ...] } | Send to these user IDs only. |
push_tokens | { "list": [string, ...] } | Send to these push tokens only. |
payload | Payload | Push / SMS / Telegram / Kakao / LINE / WhatsApp / Viber / Facebook Messenger payload. |
email_payload | EmailPayload | Email payload. |
return_unknown_identifiers | bool | When true, the response’s unknown_identifiers lists identifiers that were not found. |
registration_type | RegistrationType | What to do with hwids that aren’t registered in Pushwoosh yet. Defaults to REGISTRATION_TYPE_DEFAULT, which skips them. Set REGISTRATION_TYPE_FORCE to register and send to new email addresses or phone numbers in the same request. |
use_latest_user_device | bool | Only applies when you target users. Same behavior as in NotifySegment above — delivers one message per user instead of one per device, scoped to platforms. Defaults to false. |
campaign, campaign_name, frequency_capping, send_rate, message_type, dynamic_content_placeholders, meta_data | See NotifySegment above. |
test_devices, hwids, users, and push_tokens are mutually exclusive. Exactly one must be set.
Send to new email addresses and phone numbers
Anchor link toFor email, SMS, WhatsApp, LINE, Viber, and Kakao, the hwid is the destination itself, such as the email address or the phone number. To send to an hwid that isn’t registered yet, list it in hwids and set registration_type to REGISTRATION_TYPE_FORCE. Pushwoosh registers it and delivers the message in the same request, so you don’t need a separate /registerDevice or /registerEmail call first.
registration_type works only with hwids. With users, an unknown user ID isn’t created, whatever registration_type is set to.
Write each hwid in the same format /registerDevice stores for that channel:
| Channel | hwid format | Example |
|---|---|---|
| Email address | new.customer@example.com | |
| SMS | Phone number in international format | +14155552671 |
Phone number with the whatsapp: prefix | whatsapp:+14155552671 | |
| Kakao | Phone number with the kakao: prefix | kakao:+821012345678 |
| Viber | Phone number with the viber: prefix. Pushwoosh adds the prefix if it’s missing | viber:+14155552671 |
| LINE | LINE user ID | U4af4980629a1b2c3d4e5f6a7b8c9d0e1 |
Keep the whatsapp: and kakao: prefixes. Without them, the number is read as an SMS hwid. If that number is already registered for SMS, the send finds the SMS hwid, which doesn’t match the targeted platform, and the recipient is skipped.
Pushwoosh checks phone numbers only with return_unknown_identifiers: true. Then an SMS, WhatsApp, or Kakao hwid that isn’t a valid phone number isn’t registered and comes back in unknown_identifiers. Without that flag, the value is registered as is.
REGISTRATION_TYPE_FORCE doesn’t register hwids on push platforms, Telegram, or Facebook Messenger, as these need a token from the SDK or the messenger. An unknown hwid on these platforms is dropped silently. It doesn’t come back in unknown_identifiers, and nothing is sent to it.
For WhatsApp, registering a number doesn’t open the 24-hour customer service window. A number that hasn’t messaged you can receive only a pre-approved template set in whatsapp.content_id, not free-form whatsapp.content.
Example: Send to a new email address
Anchor link tocurl -X POST https://api.pushwoosh.com/messaging/v2/notify \ -H "Authorization: Token YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "transactional": { "application": "XXXXX-XXXXX", "platforms": ["EMAIL"], "hwids": { "list": ["new.customer@example.com"] }, "registration_type": "REGISTRATION_TYPE_FORCE", "email_payload": { "subject": { "en": "Welcome aboard" }, "body": "<p>Thanks for signing up!</p>" }, "schedule": { "at": "2026-05-01T12:00:00Z" }, "message_type": "MESSAGE_TYPE_TRANSACTIONAL", "return_unknown_identifiers": true } }'Example: Transactional by user IDs
Anchor link tocurl -X POST https://api.pushwoosh.com/messaging/v2/notify \ -H "Authorization: Token YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "transactional": { "application": "XXXXX-XXXXX", "platforms": ["IOS", "ANDROID"], "users": { "list": ["user-123", "user-456"] }, "payload": { "content": { "localized_content": { "en": { "ios": { "body": "Your order has shipped." } } } } }, "schedule": { "at": "2026-05-01T12:00:00Z" }, "message_type": "MESSAGE_TYPE_TRANSACTIONAL", "return_unknown_identifiers": true, "use_latest_user_device": true } }'Response
Anchor link to{ "result": { "message_code": "XXXXX-XXXXX-XXXXX", "unknown_identifiers": [] }}| Field | Type | Description |
|---|---|---|
message_code | string | Unique message code. Use it with /getMessageDetails and message statistics endpoints. |
unknown_identifiers | array of string | Identifiers not found on the account. Populated only for a transactional send with return_unknown_identifiers: true or with push_tokens targeting. Identifiers registered through REGISTRATION_TYPE_FORCE aren’t listed. |
No recipients found
Anchor link toWith return_unknown_identifiers: true or push_tokens targeting, Pushwoosh looks each recipient up before creating the message. If none of them is registered, Notify returns HTTP 400 with this error, and no message is created:
{ "code": 9, "message": "No recipients found", "details": [ ... ]}If at least one recipient is found, the request succeeds and the rest are listed in unknown_identifiers. To send to new email addresses or phone numbers instead, set registration_type to REGISTRATION_TYPE_FORCE.
For email, invalid addresses and addresses that bounced earlier are removed before this check, without being listed in unknown_identifiers. If no address is left after that, Notify returns 200 with a message_code, but no message is created and nothing is sent.
Shared types
Anchor link toSchedule
Anchor link to{ "at": "2026-05-01T12:00:00Z", "follow_user_timezone": true, "past_timezones_behaviour": "PAST_TIMEZONES_BEHAVIOUR_SEND_IMMEDIATELY"}| Field | Type | Description |
|---|---|---|
at | timestamp | Absolute send time (RFC 3339). Maximum 30 days in the future. With follow_user_timezone unset or false, a past at is replaced with the time the request was received, so the message goes out immediately to everyone. With follow_user_timezone: true, a past at is kept as is. See the caution below. |
after | duration | Alternative to at. Send after this offset from “now” (e.g. "3600s"). Maximum 30 days. |
follow_user_timezone | bool | When true, each device receives the message at at in its local timezone. |
past_timezones_behaviour | enum | PAST_TIMEZONES_BEHAVIOUR_SEND_IMMEDIATELY (default), PAST_TIMEZONES_BEHAVIOUR_DO_NOT_SEND, or PAST_TIMEZONES_BEHAVIOUR_NEXT_DAY. Only meaningful when follow_user_timezone is true or best_time_to_send is set. |
best_time_to_send | timestamp | Optional. Segment sends only. Each device gets the message at its own best hour within 24 hours of this time. Devices without a best hour get it at the date and time of at / after in their local timezone. See Best time to send. |
Best time to send
Anchor link tobest_time_to_send is the API counterpart of the Best time option in the Control Panel. at / after sets the fallback for devices without enough data for a best hour. Pushwoosh takes the UTC date and time of at / after and delivers the fallback at that date and time in each device’s local timezone, as with follow_user_timezone: true. This applies implicitly: leave follow_user_timezone unset, since true is rejected with BestTimeToSendWithUserTimezone. In the example below, devices without a best hour receive the message at 18:00 local time on May 1. Where that local time has already passed when the send starts, past_timezones_behaviour applies.
{ "at": "2026-05-01T18:00:00Z", "best_time_to_send": "2026-05-01T00:00:00Z"}Pushwoosh rejects the request with 400 unless all of these hold:
- The request uses the
segmentkind.transactionalsends don’t supportbest_time_to_send. follow_user_timezoneis unset orfalse.- The send time from
at/afteris in the future. A pastat,after: "0s", or an omittedaftercount as “send now” and are rejected. best_time_to_sendis no earlier than the next full hour and no more than 30 days ahead. It is rounded down to the full hour.- The send time from
at/afteris no later thanbest_time_to_sendplus 24 hours.
The error’s reason (in the ErrorInfo detail) names the failed check:
reason | Cause |
|---|---|
BestTimeToSendWithUserTimezone | follow_user_timezone is true. |
BestTimeToSendTooEarly | best_time_to_send is earlier than the next full hour. |
BestTimeToSendTooFarInFuture | best_time_to_send is more than 30 days ahead. |
SendDateAfterBestTimeToSend | at / after is later than best_time_to_send plus 24 hours. |
BestTimeToSendNotAllowed | best_time_to_send is set on a transactional send. |
BestTimeToSendWithSendNow | at / after resolves to “now” (past at or zero after). |
GeoZone
Anchor link toA one-off circle for geo_zone, defined in the request itself. It isn’t a saved geozone: geo campaigns and the Geozones API aren’t involved, and nothing is stored in the application.
{ "lat": 40.7128, "lng": -74.006, "range": 500 }| Field | Type | Description |
|---|---|---|
lat | double | Latitude of the circle center, in degrees. Required. |
lng | double | Longitude of the circle center, in degrees. Required. |
range | double | Circle radius in meters. Required. |
FrequencyCapping
Anchor link toPer-user frequency limits for marketing sends. To disable capping, omit frequency_capping entirely, or send days: 0 together with count: 0.
{ "days": 7, "count": 3, "exclude": false, "avoid": true }days(int, 1–30, or0to disable capping): look-back window. Must be sent together withcount— if one is0, the other must also be0; sending one as0while the other is nonzero returns400.count(int, 1 or higher, or0to disable capping): maximum messages allowed withindays. Same pairing rule asdaysabove.exclude(bool): hard-exclude users who have already hit the cap.avoid(bool): soft-avoid users who have already hit the cap (they still count toward analytics).
SendRate
Anchor link to{ "value": 500, "bucket": "1s", "avoid": false }Throttles the send. value is messages per bucket; typical bucket is "1s".
Platform enum
Anchor link toIOS, ANDROID, OSX, WINDOWS, AMAZON, SAFARI, CHROME, FIREFOX, IE, EMAIL, HUAWEI_ANDROID, SMS, WEB, KAKAO, TELEGRAM, LINE, WHATS_APP, VIBER, FB_MESSENGER.
RegistrationType enum
Anchor link toREGISTRATION_TYPE_DEFAULT: skips identifiers that aren’t registered in Pushwoosh. Default.REGISTRATION_TYPE_NONE: sends to every listed hwid without saving a device for it.REGISTRATION_TYPE_FORCE: registers each unknown hwid as a device on the targeted channel, then sends to it. Applies to email, SMS, WhatsApp, LINE, Viber, and Kakao.
MessageType enum
Anchor link toMESSAGE_TYPE_UNSPECIFIED: equivalent toMESSAGE_TYPE_MARKETING.MESSAGE_TYPE_MARKETING: subject to control-group filtering and frequency capping.MESSAGE_TYPE_TRANSACTIONAL: skips control-group filtering and frequency capping. Use for order confirmations, OTPs, and similar critical flows.