Quickstart
Create a member in the sandbox, credit a paid order to them, and see the points land on their balance and in your webhook endpoint. Every step is one curl command.
Available at launch
Before you start#
- A sandbox test key. Email support@unplugdining.com and ask for a sandbox. You get a
uk_test_key for your own copy of the demo environment. See Sandbox for what to include. - An HTTPS URL that shows incoming requests. Any request inspector works for this walkthrough. Later you'll point webhooks at your own server.
- curl, and a way to make UUIDs (
uuidgenon macOS and most Linux).
Put the key and host in your shell so the commands below run as written:
export UNPLUG_API=https://sandbox.api.unplugdining.com/v1
export UNPLUG_KEY=uk_test_... # the key from supportWarning Keep the secret key on your machine or your server. Never paste it into an app, a web page or a public repo, even a test key.
1. Register a webhook endpoint#
Do this first so you see every event the next steps cause. Use "*" to receive all event types while you explore.
curl -X POST $UNPLUG_API/webhook_endpoints \
-H "Authorization: Bearer $UNPLUG_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://hooks.example.dev/loyalty-sandbox",
"event_types": ["*"],
"description": "Quickstart"
}'const response = await fetch(`${process.env.UNPLUG_API}/webhook_endpoints`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "$(uuidgen)",
},
body: JSON.stringify({
url: "https://hooks.example.dev/loyalty-sandbox",
event_types: ["*"],
description: "Quickstart",
}),
});
const data = await response.json();import os
import requests
response = requests.post(
f"{os.environ['UNPLUG_API']}/webhook_endpoints",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
"Idempotency-Key": "$(uuidgen)",
},
json={
"url": "https://hooks.example.dev/loyalty-sandbox",
"event_types": ["*"],
"description": "Quickstart",
},
)
data = response.json(){
"id": "52a7e1c9-4f3b-4d8a-9e6c-1b0d7f3a5e82",
"url": "https://hooks.example.dev/loyalty-sandbox",
"event_types": ["*"],
"description": "Quickstart",
"status": "enabled",
"secret": "whsec_MfKQ9r8GKYqrTJ4kL2vJ8xVp3h7c2NnA",
"created_at": "2026-11-03T18:12:05Z"
}The secret is shown once. Save it: you need it to verify signatures.
2. Create a member#
Enroll a guest with your own id for them in external_id. The Idempotency-Key header is required here. If the request times out, send it again with the same key and you get the same member back, not a second one.
curl -X POST $UNPLUG_API/members \
-H "Authorization: Bearer $UNPLUG_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9e2c4a71-3b8f-4d5e-a1c6-7f0b2d9e8a34" \
-d '{
"external_id": "usr_10482",
"first_name": "Maya",
"last_name": "Lopez",
"email": "maya@example.com",
"communication_preferences": {
"email_marketing": true,
"sms_marketing": false,
"push_marketing": true
}
}'const response = await fetch(`${process.env.UNPLUG_API}/members`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "9e2c4a71-3b8f-4d5e-a1c6-7f0b2d9e8a34",
},
body: JSON.stringify({
external_id: "usr_10482",
first_name: "Maya",
last_name: "Lopez",
email: "maya@example.com",
communication_preferences: { email_marketing: true, sms_marketing: false, push_marketing: true },
}),
});
const data = await response.json();import os
import requests
response = requests.post(
f"{os.environ['UNPLUG_API']}/members",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
"Idempotency-Key": "9e2c4a71-3b8f-4d5e-a1c6-7f0b2d9e8a34",
},
json={
"external_id": "usr_10482",
"first_name": "Maya",
"last_name": "Lopez",
"email": "maya@example.com",
"communication_preferences": {
"email_marketing": True,
"sms_marketing": False,
"push_marketing": True,
},
},
)
data = response.json(){
"id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"external_id": "usr_10482",
"status": "active",
"first_name": "Maya",
"last_name": "Lopez",
"email": "maya@example.com",
"phone": null,
"phone_verified": false,
"birthday": null,
"points_balance": 0,
"lifetime_points": 0,
"tier": { "id": "2b9e4f7a-1c3d-4e6b-8a5f-9d0c2e7b4a13", "name": "Green" },
"next_tier": { "id": "6f1a8c3d-9b2e-4d7a-b5c6-3e0f8a1d2b47", "name": "Gold", "points_needed": 2000 },
"communication_preferences": { "email_marketing": true, "sms_marketing": false, "push_marketing": true },
"metadata": {},
"enrolled_at": "2026-11-03T18:14:37Z",
"created_at": "2026-11-03T18:14:37Z",
"updated_at": "2026-11-03T18:14:37Z"
}Keep the id. In your real integration you store it next to your user record. A member.created event arrives at your endpoint.
3. Credit a paid order#
Your server sends the order once payment is captured. First look up a location id:
curl $UNPLUG_API/locations \
-H "Authorization: Bearer $UNPLUG_KEY"const response = await fetch(`${process.env.UNPLUG_API}/locations`, {
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
},
});
const data = await response.json();import os
import requests
response = requests.get(
f"{os.environ['UNPLUG_API']}/locations",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
},
)
data = response.json(){
"data": [
{
"id": "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53",
"name": "South Congress",
"address": { "line1": "1412 S Congress Ave", "line2": null, "city": "Austin", "region": "TX", "postal_code": "78704", "country": "US" },
"time_zone": "America/Chicago",
"pos": "square",
"pos_location_id": "LQ7H2XK9M3RTB",
"status": "live"
},
{
"id": "1c7f9b3e-5a2d-4e8c-b9f0-6d3a2e1c8b74",
"name": "Riverside",
"address": { "line1": "2201 E Riverside Dr", "line2": "Suite 110", "city": "Austin", "region": "TX", "postal_code": "78741", "country": "US" },
"time_zone": "America/Chicago",
"pos": "square",
"pos_location_id": "L5VDN8P2WZ4KC",
"status": "live"
}
],
"has_more": false,
"next_cursor": null
}Now post a $12.50 pickup order at South Congress: an oat latte, a drip coffee and a butter croissant. Each pos_item_id is the Square catalog variation id from the sandbox menu.
curl -X POST $UNPLUG_API/orders \
-H "Authorization: Bearer $UNPLUG_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: c41f8e2a-6d3b-4a9c-8e57-1b0d4f6a2c93" \
-d '{
"external_order_id": "web-88213",
"member_id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"location_id": "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53",
"channel": "web",
"fulfillment": "pickup",
"placed_at": "2026-11-03T18:20:02Z",
"currency": "USD",
"subtotal_cents": 1250,
"discount_cents": 0,
"tax_cents": 103,
"tip_cents": 200,
"total_cents": 1553,
"items": [
{ "line_id": "1", "pos_item_id": "HV4MZ2TQ7LW3KXRB5P8NJ6DC", "name": "Oat latte", "quantity": 1, "unit_price_cents": 575 },
{ "line_id": "2", "pos_item_id": "3NZQ7XKJ2Y5PLVKCW6RQ4G2T", "name": "Drip coffee", "quantity": 1, "unit_price_cents": 325 },
{ "line_id": "3", "pos_item_id": "7PKD3RXN2WQ6ZLHM4TB5JV8C", "name": "Butter croissant", "quantity": 1, "unit_price_cents": 350 }
]
}'const response = await fetch(`${process.env.UNPLUG_API}/orders`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "c41f8e2a-6d3b-4a9c-8e57-1b0d4f6a2c93",
},
body: JSON.stringify({
external_order_id: "web-88213",
member_id: "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
location_id: "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53",
channel: "web",
fulfillment: "pickup",
placed_at: "2026-11-03T18:20:02Z",
currency: "USD",
subtotal_cents: 1250,
discount_cents: 0,
tax_cents: 103,
tip_cents: 200,
total_cents: 1553,
items: [
{
line_id: "1",
pos_item_id: "HV4MZ2TQ7LW3KXRB5P8NJ6DC",
name: "Oat latte",
quantity: 1,
unit_price_cents: 575,
},
{
line_id: "2",
pos_item_id: "3NZQ7XKJ2Y5PLVKCW6RQ4G2T",
name: "Drip coffee",
quantity: 1,
unit_price_cents: 325,
},
{
line_id: "3",
pos_item_id: "7PKD3RXN2WQ6ZLHM4TB5JV8C",
name: "Butter croissant",
quantity: 1,
unit_price_cents: 350,
},
],
}),
});
const data = await response.json();import os
import requests
response = requests.post(
f"{os.environ['UNPLUG_API']}/orders",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
"Idempotency-Key": "c41f8e2a-6d3b-4a9c-8e57-1b0d4f6a2c93",
},
json={
"external_order_id": "web-88213",
"member_id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"location_id": "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53",
"channel": "web",
"fulfillment": "pickup",
"placed_at": "2026-11-03T18:20:02Z",
"currency": "USD",
"subtotal_cents": 1250,
"discount_cents": 0,
"tax_cents": 103,
"tip_cents": 200,
"total_cents": 1553,
"items": [
{
"line_id": "1",
"pos_item_id": "HV4MZ2TQ7LW3KXRB5P8NJ6DC",
"name": "Oat latte",
"quantity": 1,
"unit_price_cents": 575,
},
{
"line_id": "2",
"pos_item_id": "3NZQ7XKJ2Y5PLVKCW6RQ4G2T",
"name": "Drip coffee",
"quantity": 1,
"unit_price_cents": 325,
},
{
"line_id": "3",
"pos_item_id": "7PKD3RXN2WQ6ZLHM4TB5JV8C",
"name": "Butter croissant",
"quantity": 1,
"unit_price_cents": 350,
},
],
},
)
data = response.json(){
"id": "0f6d4b2e-8a1c-4d9f-b3e7-5c2a8f0d6e13",
"external_order_id": "web-88213",
"status": "credited",
"points_earned": 125,
"bonus_points": 0,
"redemptions": [],
"member": {
"points_balance": 125,
"tier": { "id": "2b9e4f7a-1c3d-4e6b-8a5f-9d0c2e7b4a13", "name": "Green" }
},
"created_at": "2026-11-03T18:20:04Z"
}The demo program earns 10 Beans per dollar on the subtotal after discounts, rounded down. $12.50 with no discount is 125 Beans. Tax and tip don't earn.
Note external_order_id is unique per brand forever. Sending web-88213 again with a new Idempotency-Key returns 409 order_already_exists, so an order can never earn twice.
4. See the points and the webhooks#
Read the member back:
curl $UNPLUG_API/members/3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40 \
-H "Authorization: Bearer $UNPLUG_KEY"const response = await fetch(`${process.env.UNPLUG_API}/members/3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40`, {
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
},
});
const data = await response.json();import os
import requests
response = requests.get(
f"{os.environ['UNPLUG_API']}/members/3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
},
)
data = response.json(){
"id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"external_id": "usr_10482",
"status": "active",
"first_name": "Maya",
"last_name": "Lopez",
"points_balance": 125,
"lifetime_points": 125,
"tier": { "id": "2b9e4f7a-1c3d-4e6b-8a5f-9d0c2e7b4a13", "name": "Green" },
"next_tier": { "id": "6f1a8c3d-9b2e-4d7a-b5c6-3e0f8a1d2b47", "name": "Gold", "points_needed": 1875 },
"updated_at": "2026-11-03T18:20:04Z"
}The response above is trimmed to the fields that changed. Your request inspector now shows three deliveries: member.created, points.changed and order.credited. They can arrive in any order. Here is points.changed:
webhook-id: 3c1f7a92-5e4b-4d6c-8f0a-2b9e1d7c4a63
webhook-timestamp: 1793730004
webhook-signature: v1,K7mQ2pX9vR4tL8nB3wY6cF1hJ5dS0gA2eZ7uN4iO9kE=
content-type: application/json{
"id": "3c1f7a92-5e4b-4d6c-8f0a-2b9e1d7c4a63",
"type": "points.changed",
"created_at": "2026-11-03T18:20:04Z",
"api_version": "v1",
"livemode": false,
"brand_id": "e5a1c7d9-2f4b-4c8e-a3d6-8b0f1e9c2a57",
"data": {
"object": {
"id": "a71e4c2b-9d3f-4b8a-8e6c-0f2d5b7a3c19",
"member_id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"type": "earn",
"points": 125,
"balance_after": 125,
"description": "Order web-88213",
"order_id": "0f6d4b2e-8a1c-4d9f-b3e7-5c2a8f0d6e13",
"reward_id": null,
"adjustment_reason": null,
"created_at": "2026-11-03T18:20:04Z"
},
"previous_attributes": {}
}
}
Your endpoint must answer with any 2xx within 10 seconds. If it doesn't, we retry with backoff for 3 days.
Next steps#
To start over, POST /v1/test/reset deletes every member and order in your sandbox brand. See Test helpers.