Run offers from your server
The unPLUG Dashboard and unPLUG campaigns create offers for you. This guide is for brands whose own tools should do it instead: drafting, publishing, granting to members, unique code batches and budgets. To accept codes and wallet offers in your app, see Offers and promo codes.
Coming soon
Every call takes a secret key with offers:write, from your server only. The full reference is Offers in the Management API.
Lifecycle and versions#
An offer moves through five statuses.
| Status | Meaning | How it gets there |
|---|---|---|
draft | Being written. Guests can't see or use it. | POST /v1/offers |
scheduled | Published with a starts_at in the future. | POST /v1/offers/{id}/publish |
live | Usable at checkout, and grants are allowed. | Publish, or starts_at passes |
paused | Temporarily off. Codes and wallet items don't apply. | /pause, back with /resume |
ended | Off for good. Codes stop working and no new grants go out. Wallet items already granted stay usable until their own expires_at. | /end, or ends_at passes |
You edit a draft in place. Editing an offer that is already scheduled or live creates a new version. Members keep the version they got: a wallet item granted under version 1 still gives $3 off after you change the offer to $2 off.
Preview before you publish#
Run a dry run on the draft. It checks the offer's rules against your members' order history and returns how many members are eligible, how many redemptions to expect, and what that costs.
curl -X POST https://api.unplugdining.com/v1/offers/71c3e8a5-4b2d-4f60-a9e1-58d7b0c2f346/preview \
-H "Authorization: Bearer $UNPLUG_KEY"const response = await fetch("https://api.unplugdining.com/v1/offers/71c3e8a5-4b2d-4f60-a9e1-58d7b0c2f346/preview", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
},
});
const data = await response.json();import os
import requests
response = requests.post(
"https://api.unplugdining.com/v1/offers/71c3e8a5-4b2d-4f60-a9e1-58d7b0c2f346/preview",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
},
)
data = response.json(){
"eligible_members": 1840,
"expected_redemptions": 410,
"expected_cost_cents": 123000,
"warnings": [
"The budget covers 300 redemptions. About 410 are expected, so the offer will stop early."
]
}Read the warnings before you publish. They are the cheapest place to catch an offer that would outrun its budget.
Grant an offer to members#
For a granted offer, send member ids in batches of up to 500. Each grant becomes a wallet item and fires reward.issued.
curl -X POST https://api.unplugdining.com/v1/offers/71c3e8a5-4b2d-4f60-a9e1-58d7b0c2f346/grants \
-H "Authorization: Bearer $UNPLUG_KEY" \
-H "Idempotency-Key: a1f5c9e3-2d7b-4086-9e4a-6b3c8d0f1e25" \
-H "Content-Type: application/json" \
-d '{
"grant_ref": "nov-winback-1",
"member_ids": [
"3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"9a4d2f6b-1c8e-4b73-a0d5-e7f3c19b6284"
]
}'const response = await fetch("https://api.unplugdining.com/v1/offers/71c3e8a5-4b2d-4f60-a9e1-58d7b0c2f346/grants", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
"Idempotency-Key": "a1f5c9e3-2d7b-4086-9e4a-6b3c8d0f1e25",
"Content-Type": "application/json",
},
body: JSON.stringify({
grant_ref: "nov-winback-1",
member_ids: [
"3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"9a4d2f6b-1c8e-4b73-a0d5-e7f3c19b6284",
],
}),
});
const data = await response.json();import os
import requests
response = requests.post(
"https://api.unplugdining.com/v1/offers/71c3e8a5-4b2d-4f60-a9e1-58d7b0c2f346/grants",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
"Idempotency-Key": "a1f5c9e3-2d7b-4086-9e4a-6b3c8d0f1e25",
},
json={
"grant_ref": "nov-winback-1",
"member_ids": [
"3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"9a4d2f6b-1c8e-4b73-a0d5-e7f3c19b6284",
],
},
)
data = response.json(){
"granted": 1,
"skipped": [
{ "member_id": "9a4d2f6b-1c8e-4b73-a0d5-e7f3c19b6284", "reason": "already_granted" }
]
}grant_ref is your name for this send. The same grant_ref and member never grant twice, even after the 24-hour Idempotency-Key window. Give each batch job one grant_ref and reuse it when you resume a job that stopped halfway.
Unique code batches#
For a unique_codes offer, create a batch of codes with a prefix, then download them as CSV for your printer or email tool.
curl -X POST https://api.unplugdining.com/v1/offers/e7d25a90-4c18-4b6f-8a3e-91f0c6b2d475/code_batches \
-H "Authorization: Bearer $UNPLUG_KEY" \
-H "Content-Type: application/json" \
-d '{ "prefix": "HOLIDAY", "quantity": 5000 }'const response = await fetch("https://api.unplugdining.com/v1/offers/e7d25a90-4c18-4b6f-8a3e-91f0c6b2d475/code_batches", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ prefix: "HOLIDAY", quantity: 5000 }),
});
const data = await response.json();import os
import requests
response = requests.post(
"https://api.unplugdining.com/v1/offers/e7d25a90-4c18-4b6f-8a3e-91f0c6b2d475/code_batches",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
},
json={"prefix": "HOLIDAY", "quantity": 5000},
)
data = response.json()A batch of 5,000 is ready in a few seconds; check its status before you download. If a batch leaks, void it: every unused code in it stops working, and used codes keep their history.
Budgets and per-member limits#
- Budget. Set
budget.max_redemptions,budget.max_discount_centsor both. unPLUG enforces it at checkout: once the budget is used up, preview returnsbudget_exhaustedand new redemptions are refused. - Per-member limit.
per_member_limitcaps how many times one member can use the offer. Past it, preview returnsper_member_limit_reached.