Push and in-app messages
Connect your app to unPLUG so its campaigns reach your guests as push notifications and full-screen in-app messages. Pushes go out from your own Firebase project and Apple key, so they show your app's name and icon.
Available at launch Needs a one-time setup with unPLUG support for each app.
What unPLUG sends#
- Visible pushes. Title, body, an optional image and up to two buttons. No sound, ever.
- Silent pushes. Nothing shows. They tell your app that a full-screen message is waiting.
- Full-screen in-app messages. An image with optional title, body, buttons and an offer code. Your app fetches them from the API and draws them.
Your app does five things: register its device, render the pushes, fetch and show in-app messages, report what the guest did, and route taps to the right screen.
How a message reaches the guest#
For a full-screen message, unPLUG tries the quiet route first and falls back to a visible push.
- Silent push. unPLUG sends a silent push with
silent: "1"andinapp: "1"to every device the member has registered. - Your app fetches the message. On the silent push, or the next time the guest opens the app, you call
GET /v1/members/me/messages/nextand show it. - Fallback push. If the message isn't shown within the brand's fallback window (5 minutes by default), unPLUG sends one visible push with the same
message_idandinapp: "1". - Tap. When the guest taps it, you fetch that exact message with
GET /v1/members/me/messages/{id}and show it.
The whole flow counts as one message toward the guest's frequency cap. Quiet hours and frequency caps apply to the visible push only, since the silent push shows nothing. iOS and Android both drop or delay some silent pushes; the fallback is there so the message still arrives.
Set up with support#
Push credentials are set up once per app by unPLUG support. Do it in the sandbox first.
- Collect your credentials.
- Android, and iOS if your iOS app uses the Firebase SDK: a Firebase service account JSON key with the Firebase Cloud Messaging API (V1) turned on. Create it in the Firebase console under Project settings, Service accounts.
- iOS with raw APNs tokens: an APNs auth key (
.p8file) from your Apple Developer account, its Key ID (for example7KQ2M9XB4D) and your Team ID (for example4HJ8W2N6PZ).
- Email support for an upload link. Write to support@unplugdining.com with your brand name and environment. Support sends a secure upload link. Upload the files there, never as an email attachment.
- Send your app details in the same email. iOS bundle id (
com.example.app), Android package name (com.example.app), and which APNs environment each build uses: development for builds run from Xcode, production for TestFlight and the App Store. - Send your links. Your deep link scheme (
yourapp://) and your universal link or app link domain (order.example.com). - Wait for the confirmation. Support tells you when your app is connected. Then register a test device and ask for a test send.
unPLUG stores the keys in a secrets vault. Nobody can read them back, including unPLUG staff. To rotate a key, upload the new one the same way and tell support when to switch. Uploading credentials yourself from the developer dashboard is Coming soon.
Warning A Firebase service account key or APNs key lets anyone who holds it push to all your guests. Don't email it, commit it or paste it into a ticket. If one leaks, revoke it in Firebase or Apple first, then send support the new one.
Register the device#
Tell unPLUG which devices belong to the signed-in member, with their push token and permission. This is the only way unPLUG learns where to send.
curl -X PUT https://api.unplugdining.com/v1/members/me/devices/9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935 \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS" \
-H "Content-Type: application/json" \
-d '{
"platform": "ios",
"push_token": "7f3a9c2e1b8d4f6a0c5e9b2d7f1a3c8e4b6d0f2a9c7e5b3d1f8a6c4e2b0d9f7a",
"token_type": "apns",
"app_id": "com.example.app",
"push_enabled": true,
"app_version": "4.2.0",
"os_version": "18.1",
"locale": "en-US",
"time_zone": "America/Chicago"
}'const response = await fetch("https://api.unplugdining.com/v1/members/me/devices/9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935", {
method: "PUT",
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
"Content-Type": "application/json",
},
body: JSON.stringify({
platform: "ios",
push_token: "7f3a9c2e1b8d4f6a0c5e9b2d7f1a3c8e4b6d0f2a9c7e5b3d1f8a6c4e2b0d9f7a",
token_type: "apns",
app_id: "com.example.app",
push_enabled: true,
app_version: "4.2.0",
os_version: "18.1",
locale: "en-US",
time_zone: "America/Chicago",
}),
});
const data = await response.json();import requests
response = requests.put(
"https://api.unplugdining.com/v1/members/me/devices/9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
json={
"platform": "ios",
"push_token": "7f3a9c2e1b8d4f6a0c5e9b2d7f1a3c8e4b6d0f2a9c7e5b3d1f8a6c4e2b0d9f7a",
"token_type": "apns",
"app_id": "com.example.app",
"push_enabled": True,
"app_version": "4.2.0",
"os_version": "18.1",
"locale": "en-US",
"time_zone": "America/Chicago",
},
)
data = response.json(){
"device_id": "9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935",
"member_id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"platform": "ios",
"push_token": "7f3a9c2e1b8d4f6a0c5e9b2d7f1a3c8e4b6d0f2a9c7e5b3d1f8a6c4e2b0d9f7a",
"token_type": "apns",
"app_id": "com.example.app",
"push_enabled": true,
"app_version": "4.2.0",
"os_version": "18.1",
"locale": "en-US",
"time_zone": "America/Chicago",
"created_at": "2026-10-12T15:04:51Z",
"updated_at": "2026-10-30T14:58:20Z"
}device_idis your own id for this install. Generate a UUID the first time the app runs and keep it in app storage. Don't use the push token: it changes.token_typeisfcmif the token comes from the Firebase SDK (always on Android, and on iOS if you use Firebase), orapnsfor a raw APNs device token on iOS.push_enabledis the OS notification permission right now.app_idis the bundle id or package name you gave support.
Call it on every app start once the member is signed in, and again whenever the push token changes, the guest changes the notification permission in Settings, or a different member signs in. It's a PUT: sending the same device again is safe. A member can have many devices.
On sign out, delete the device before you throw the member token away, so the next person on the phone doesn't get Maya's messages:
curl -X DELETE https://api.unplugdining.com/v1/members/me/devices/9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935 \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS"const response = await fetch("https://api.unplugdining.com/v1/members/me/devices/9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935", {
method: "DELETE",
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
});
console.log(response.status);import requests
response = requests.delete(
"https://api.unplugdining.com/v1/members/me/devices/9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
)
print(response.status_code)It returns 204. If the member token has expired, get a new one from your server first. Keep the same device_id after sign out; the next member who signs in registers it again with their own token.
Set up the app#
Register the button categories#
unPLUG pushes use two notification categories, UNPLUG_ONE_BUTTON and UNPLUG_TWO_BUTTONS, with action ids primary and secondary. Register them at launch. The titles here are placeholders; the Notification Service Extension replaces them with each push's labels.
import UserNotifications
func registerUnplugCategories() {
let primary = UNNotificationAction(identifier: "primary", title: "Open", options: [.foreground])
let secondary = UNNotificationAction(identifier: "secondary", title: "Not now", options: [])
let one = UNNotificationCategory(identifier: "UNPLUG_ONE_BUTTON",
actions: [primary], intentIdentifiers: [])
let two = UNNotificationCategory(identifier: "UNPLUG_TWO_BUTTONS",
actions: [primary, secondary], intentIdentifiers: [])
UNUserNotificationCenter.current().setNotificationCategories([one, two])
}Add a Notification Service Extension#
Pushes arrive with mutable-content: 1. Add a Notification Service Extension target to download image_url and set the button titles from buttons. Without it, pushes show text only and the buttons show your placeholder titles.
import UserNotifications
struct PushButton: Decodable {
let id: String // "primary" or "secondary"
let label: String
let action: String // "open_screen", "open_url" or "close"
let url: String?
}
final class NotificationService: UNNotificationServiceExtension {
private var contentHandler: ((UNNotificationContent) -> Void)?
private var content: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
guard let content = request.content.mutableCopy() as? UNMutableNotificationContent else {
return contentHandler(request.content)
}
self.contentHandler = contentHandler
self.content = content
let info = content.userInfo
let group = DispatchGroup()
// Button titles: re-register the category with this push's labels.
let buttons = Array(Self.buttons(from: info["buttons"]).prefix(2))
if !buttons.isEmpty {
let actions = buttons.map {
UNNotificationAction(identifier: $0.id, title: $0.label,
options: $0.action == "close" ? [] : [.foreground])
}
let id = actions.count == 2 ? "UNPLUG_TWO_BUTTONS" : "UNPLUG_ONE_BUTTON"
let category = UNNotificationCategory(identifier: id, actions: actions, intentIdentifiers: [])
content.categoryIdentifier = id
group.enter()
let center = UNUserNotificationCenter.current()
center.getNotificationCategories { existing in
center.setNotificationCategories(existing.filter { $0.identifier != id }.union([category]))
group.leave()
}
}
// Image: download it and attach it.
if let link = info["image_url"] as? String, let url = URL(string: link) {
group.enter()
URLSession.shared.downloadTask(with: url) { tmp, _, _ in
defer { group.leave() }
guard let tmp = tmp else { return }
let ext = url.pathExtension.isEmpty ? "jpg" : url.pathExtension
let file = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString).appendingPathExtension(ext)
try? FileManager.default.moveItem(at: tmp, to: file)
if let image = try? UNNotificationAttachment(identifier: "image", url: file) {
content.attachments = [image]
}
}.resume()
}
group.notify(queue: .main) { contentHandler(content) }
}
override func serviceExtensionTimeWillExpire() {
if let handler = contentHandler, let content = content { handler(content) }
}
// buttons arrives as a JSON array from APNs, or as a JSON string through FCM.
private static func buttons(from raw: Any?) -> [PushButton] {
var data: Data?
if let text = raw as? String { data = text.data(using: .utf8) }
else if let list = raw { data = try? JSONSerialization.data(withJSONObject: list) }
guard let data = data else { return [] }
return (try? JSONDecoder().decode([PushButton].self, from: data)) ?? []
}
}Give the extension its own bundle id under your app's (com.example.app.NotificationService). It doesn't need to be registered with support.
Handle silent pushes#
Turn on the Remote notifications background mode. In application(_:didReceiveRemoteNotification:fetchCompletionHandler:), when silent and inapp are both "1", call GET /v1/members/me/messages/next. Show the message right away if the app is in the foreground and the guest isn't in checkout; otherwise keep it and show it when they next open the app.
Create the channels#
Create two channels at app start, marketing and transactional. Each push names its channel in channel_id. Give both channels no sound to match the pushes, and let guests turn marketing off in system settings without losing order updates.
fun createUnplugChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = context.getSystemService(NotificationManager::class.java)
val marketing = NotificationChannel("marketing", "Offers and news",
NotificationManager.IMPORTANCE_DEFAULT).apply { setSound(null, null) }
val transactional = NotificationChannel("transactional", "Orders and rewards",
NotificationManager.IMPORTANCE_DEFAULT).apply { setSound(null, null) }
manager.createNotificationChannels(listOf(marketing, transactional))
}Build the notification from the data message#
unPLUG sends Android pushes as data messages, so Android never draws them for you. Your FirebaseMessagingService reads the data and builds the notification. Every value in the data map is a string; buttons is a JSON string you parse.
class AppMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
// Re-register: PUT /v1/members/me/devices/{device_id} with the new token.
DeviceRegistration.sync(applicationContext)
}
override fun onMessageReceived(message: RemoteMessage) {
val d = message.data
val messageId = d["message_id"] ?: return
if (d["silent"] == "1") {
if (d["inapp"] == "1") InAppMessages.fetchNext(applicationContext)
return
}
val builder = NotificationCompat.Builder(this, d["channel_id"] ?: "marketing")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(d["title"])
.setContentText(d["body"])
.setAutoCancel(true)
.setContentIntent(openIntent(d, buttonId = null, url = d["deep_link"]))
d["thumbnail_url"]?.let { loadBitmap(it) }?.let { builder.setLargeIcon(it) }
d["image_url"]?.let { loadBitmap(it) }?.let {
builder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(it))
}
d["buttons"]?.let { json ->
val buttons = JSONArray(json)
for (i in 0 until minOf(buttons.length(), 2)) {
val b = buttons.getJSONObject(i)
val id = b.getString("id")
val intent = if (b.getString("action") == "close") dismissIntent(d, id)
else openIntent(d, id, b.optString("url"))
builder.addAction(0, b.getString("label"), intent)
}
}
if (NotificationManagerCompat.from(this).areNotificationsEnabled()) {
NotificationManagerCompat.from(this).notify(messageId.hashCode(), builder.build())
}
}
private fun openIntent(d: Map<String, String>, buttonId: String?, url: String?): PendingIntent {
val intent = Intent(this, MainActivity::class.java).apply {
action = "com.example.app.PUSH_OPEN"
putExtra("message_id", d["message_id"])
putExtra("button", buttonId)
putExtra("url", url)
putExtra("inapp", d["inapp"])
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
return PendingIntent.getActivity(this, (d["message_id"] + buttonId).hashCode(), intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
// dismissIntent(d, id) points at a BroadcastReceiver that cancels the
// notification and reports the tap with POST /v1/members/me/push_opens.
private fun loadBitmap(url: String): Bitmap? = try {
URL(url).openStream().use { BitmapFactory.decodeStream(it) }
} catch (e: IOException) {
null
}
}On Android 13 and later, ask for POST_NOTIFICATIONS at a moment that makes sense to the guest (after their first order works well), then send the result as push_enabled.
Push payload#
These are the keys your app reads. On iOS, title, subtitle and body are in aps.alert and the rest are top-level keys. On Android they're all in the data map, as strings.
| Key | Platform | What it is |
|---|---|---|
message_id | Both | The message. Send it back on push opens, and use it to fetch the in-app message. |
campaign_id | Both | The unPLUG campaign it came from. Useful in your own analytics. |
title | Both | Notification title. |
subtitle | iOS | Line under the title. |
body | Both | Notification text. |
image_url | Both | Large image. HTTPS. |
thumbnail_url | Android | Small image for the collapsed notification. |
deep_link | Both | Where a tap on the notification goes: your scheme (yourapp://menu/fall) or your https domain. |
buttons | Both | Up to 2 buttons, each { id, label, action, url }. id is primary or secondary; action is open_screen, open_url or close. |
inapp | Both | "1": on a tap, open the in-app message for message_id instead of deep_link. On a silent push, fetch the next message. |
silent | Both | "1": show nothing. |
channel_id | Android | marketing or transactional. |
Pushes never play a sound. A payload is at most 4 KB. A push expires after at most 28 days, so a phone that's been off for a month doesn't wake up to old offers.
{
"aps": {
"alert": {
"title": "A pastry on us",
"subtitle": "South Congress",
"body": "Pick any pastry with your next order. It's in your wallet until November 12."
},
"mutable-content": 1,
"category": "UNPLUG_TWO_BUTTONS"
},
"message_id": "5b81d3e0-2c4f-4a96-8e17-d0f9a3b62c58",
"campaign_id": "e4a09b27-7d13-4c5e-9f82-31b6c0d8a4f1",
"image_url": "https://cdn.unplugdining.com/demo/campaigns/fall-pastry.jpg",
"deep_link": "yourapp://wallet/7e3a9c51-0b8d-4f2e-a6c4-5d1b9e7f3a28",
"buttons": [
{ "id": "primary", "label": "See my wallet", "action": "open_screen", "url": "yourapp://wallet/7e3a9c51-0b8d-4f2e-a6c4-5d1b9e7f3a28" },
{ "id": "secondary", "label": "Not now", "action": "close", "url": null }
]
}{
"message_id": "5b81d3e0-2c4f-4a96-8e17-d0f9a3b62c58",
"campaign_id": "e4a09b27-7d13-4c5e-9f82-31b6c0d8a4f1",
"title": "A pastry on us",
"body": "Pick any pastry with your next order. It's in your wallet until November 12.",
"image_url": "https://cdn.unplugdining.com/demo/campaigns/fall-pastry.jpg",
"thumbnail_url": "https://cdn.unplugdining.com/demo/campaigns/fall-pastry-thumb.jpg",
"deep_link": "yourapp://wallet/7e3a9c51-0b8d-4f2e-a6c4-5d1b9e7f3a28",
"buttons": "[{\"id\":\"primary\",\"label\":\"See my wallet\",\"action\":\"open_screen\",\"url\":\"yourapp://wallet/7e3a9c51-0b8d-4f2e-a6c4-5d1b9e7f3a28\"},{\"id\":\"secondary\",\"label\":\"Not now\",\"action\":\"close\",\"url\":null}]",
"channel_id": "marketing"
}Handle a push tap#
- Report the open. On any tap, on the notification or a button, call
POST /v1/members/me/push_opens. Includebuttonwhen a button was tapped. - If
inappis"1", fetchGET /v1/members/me/messages/{message_id}and show that message. Don't followdeep_link. - Otherwise route. A button goes to its
url(or closes, forclose). A tap on the notification goes todeep_link.
curl -X POST https://api.unplugdining.com/v1/members/me/push_opens \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS" \
-H "Content-Type: application/json" \
-d '{
"message_id": "5b81d3e0-2c4f-4a96-8e17-d0f9a3b62c58",
"opened_at": "2026-10-30T15:06:12Z",
"button": "primary"
}'const response = await fetch("https://api.unplugdining.com/v1/members/me/push_opens", {
method: "POST",
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
"Content-Type": "application/json",
},
body: JSON.stringify({
message_id: "5b81d3e0-2c4f-4a96-8e17-d0f9a3b62c58",
opened_at: "2026-10-30T15:06:12Z",
button: "primary",
}),
});
const data = await response.json();import requests
response = requests.post(
"https://api.unplugdining.com/v1/members/me/push_opens",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
json={
"message_id": "5b81d3e0-2c4f-4a96-8e17-d0f9a3b62c58",
"opened_at": "2026-10-30T15:06:12Z",
"button": "primary",
},
)
data = response.json()It returns 204. Send opened_at as the time of the tap, not the time of the request: a tap on a cold start may reach you after the app has loaded and fetched a member token. If the guest is signed out when they tap, route the tap and skip the report.
Full-screen in-app messages#
When to fetch#
Call GET /v1/members/me/messages/next each time the app comes to the foreground, and when a silent push with inapp: "1" arrives. It returns 200 with a message, or 204 when there's nothing to show. A member sees at most one full-screen message a day; the API enforces this, so you don't need to count.
Never show a message during checkout. Each message says so in display.never_during: ["checkout"]. If the app opens straight into checkout (for example from a cart reminder), wait until the guest leaves checkout, then fetch.
curl https://api.unplugdining.com/v1/members/me/messages/next \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS"const response = await fetch("https://api.unplugdining.com/v1/members/me/messages/next", {
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
});
const data = await response.json();import requests
response = requests.get(
"https://api.unplugdining.com/v1/members/me/messages/next",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
)
data = response.json(){
"id": "5b81d3e0-2c4f-4a96-8e17-d0f9a3b62c58",
"layout": "image_and_text",
"image_url": "https://cdn.unplugdining.com/demo/campaigns/fall-pastry.jpg",
"title": "A pastry on us",
"body": "Croissant or muffin, your pick. Add one to your next order and it's free. Good until November 12.",
"primary": { "label": "See my wallet", "action": "open_screen", "url": "yourapp://wallet/7e3a9c51-0b8d-4f2e-a6c4-5d1b9e7f3a28" },
"secondary": { "label": "Not now", "action": "close", "url": null },
"offer": { "code": null, "wallet_item_id": "7e3a9c51-0b8d-4f2e-a6c4-5d1b9e7f3a28", "text": "Fall pastry on us, until November 12." },
"display": { "never_during": ["checkout"] },
"created_at": "2026-10-30T15:00:00Z",
"expires_at": "2026-11-13T05:59:59Z"
}Layouts#
| Layout | How to draw it |
|---|---|
full_image | The image fills the screen and carries the words. Draw the buttons over the bottom of it. Use title as the image's accessibility label. |
image_and_text | The image on top, then title, body, the offer if there is one, and the buttons. |
Always draw your own close button in the top corner, whatever the message says. secondary and offer can be null. Don't show a message after its expires_at.
Buttons and offers#
open_screen: openurl, a link in your scheme or on your link domain, inside the app.open_url: openurl, always https, in the browser or an in-app browser.close: dismiss the message.
When offer.code is set, show the code with a copy button and prefill it on checkout if you can. When offer.wallet_item_id is set, the offer is already in the member's wallet; link to it. See Offers and promo codes.
Report what the guest did#
Post an impression when the message is on screen, a button_tap with the button when one is tapped, and a close when it's dismissed any other way. Each is recorded once per message, so retries are safe.
curl -X POST https://api.unplugdining.com/v1/members/me/messages/5b81d3e0-2c4f-4a96-8e17-d0f9a3b62c58/events \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS" \
-H "Content-Type: application/json" \
-d '{ "type": "button_tap", "button": "primary" }'const response = await fetch("https://api.unplugdining.com/v1/members/me/messages/5b81d3e0-2c4f-4a96-8e17-d0f9a3b62c58/events", {
method: "POST",
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
"Content-Type": "application/json",
},
body: JSON.stringify({ type: "button_tap", button: "primary" }),
});
const data = await response.json();import requests
response = requests.post(
"https://api.unplugdining.com/v1/members/me/messages/5b81d3e0-2c4f-4a96-8e17-d0f9a3b62c58/events",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
json={"type": "button_tap", "button": "primary"},
)
data = response.json()It returns 204. The impression is what stops the fallback push, so send it as soon as the message is visible, before any image finishes loading.
Consent#
Two separate things decide whether a guest gets marketing pushes:
- OS permission. Whether the phone lets your app show notifications. You send it as
push_enabledon the device. - Marketing preference. Whether the guest agreed to marketing pushes from your brand:
communication_preferences.push_marketingon the member.
unPLUG sends marketing pushes only when both are on. Put an "Offers and news" switch in your app's settings and save it like this:
curl -X PATCH https://api.unplugdining.com/v1/members/me/communication_preferences \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS" \
-H "Content-Type: application/json" \
-d '{ "push_marketing": false }'const response = await fetch("https://api.unplugdining.com/v1/members/me/communication_preferences", {
method: "PATCH",
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
"Content-Type": "application/json",
},
body: JSON.stringify({ push_marketing: false }),
});
const data = await response.json();import requests
response = requests.patch(
"https://api.unplugdining.com/v1/members/me/communication_preferences",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
json={"push_marketing": False},
)
data = response.json()The change is stamped with source: "app" and fires communication_preferences.updated. See Members for the full preferences object.
Web#
Web push isn't part of v1. unPLUG reaches guests who only use your website by email.
Test in the sandbox#
- Connect your sandbox app. Go through the setup above with support for the sandbox environment. Debug builds usually get development APNs tokens.
- Register your test device. Sign in with a sandbox member and call
PUT /v1/members/me/devices/{device_id}onhttps://sandbox.api.unplugdining.com/v1. Sandbox sends only reach devices registered in the sandbox, so real guests can't get a test message. - Ask support for a test send. Support or your unPLUG contact sends a push and a full-screen message to your sandbox brand.
- Try the hard cases. The app closed, in the background and in checkout. A cold start from a push tap. A message with two buttons and one with an offer. Sign out, then check the device no longer gets pushes.
Before you go live#
- Production credentials are uploaded and support has confirmed your production bundle id and package name.
- The device is registered on every app start and on token, permission and member changes, and deleted on sign out.
- iOS: both categories are registered and the Notification Service Extension ships in the release build.
- Android: both channels exist and data messages render with image and buttons.
- Taps report
push_opensand route correctly from a cold start, includinginapp: "1"pushes. - In-app messages never show during checkout, always have your close button, and report impression, button_tap and close.
- Your settings screen has a marketing push switch that saves
push_marketing.