Members and sign-in
Every guest in your loyalty program is one member in unPLUG. Create that member from your own sign-up, link guests who already earned at the register, and keep the profile in sync until the guest deletes their account.
Available at launch
Enroll on sign-up#
When a guest creates an account in your app, your server creates the member. Send your own user id as external_id. It's how you and unPLUG agree on who the guest is.
- Create your own account first. Your user id becomes the
external_id. - Call
POST /v1/membersfrom your server with anIdempotency-Key. - Store the returned
idon your user record. You need it for tokens, orders and lookups.
curl -X POST https://api.unplugdining.com/v1/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",
"phone": "+15125550142",
"phone_verified": true,
"communication_preferences": {
"email_marketing": true,
"sms_marketing": false,
"push_marketing": true
}
}'const response = await fetch("https://api.unplugdining.com/v1/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",
phone: "+15125550142",
phone_verified: true,
communication_preferences: { email_marketing: true, sms_marketing: false, push_marketing: true },
}),
});
const data = await response.json();import os
import requests
response = requests.post(
"https://api.unplugdining.com/v1/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",
"phone": "+15125550142",
"phone_verified": True,
"communication_preferences": {
"email_marketing": True,
"sms_marketing": False,
"push_marketing": True,
},
},
)
data = response.json()The call is an upsert on external_id:
- 201: a new member was created.
- 200: a member with this
external_idalready exists, and you get it back unchanged. Retrying a sign-up is always safe. - 200 with
"linked_existing": true: the verified phone matched a guest who already earned at the register. See the next section.
Email is stored lowercase. Phone numbers must be E.164, like +15125550142.
Verified phones and guests who already earn in store#
Many of your guests already have a member: they gave their phone number at a Square register and have been earning Beans for months. That member has a phone but no external_id. When you send the same phone with phone_verified: true, unPLUG links your account to that member instead of creating a new one, and the guest keeps their points.
Maya earned 1,880 Beans at the South Congress register and has 420 left. She signs up in the brand's app and confirms her phone with an SMS code. The request above returns her existing member:
{
"id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"external_id": "usr_10482",
"status": "active",
"linked_existing": true,
"first_name": "Maya",
"last_name": "Lopez",
"email": "maya@example.com",
"phone": "+15125550142",
"phone_verified": true,
"birthday": null,
"points_balance": 420,
"lifetime_points": 1880,
"tier": { "id": "2b9e4f7a-1c3d-4e6b-8a5f-9d0c2e7b4a13", "name": "Green" },
"next_tier": { "id": "6f1a8c3d-9b2e-4d7a-b5c6-3e0f8a1d2b47", "name": "Gold", "points_needed": 120 },
"communication_preferences": { "email_marketing": true, "sms_marketing": false, "push_marketing": true },
"metadata": {},
"enrolled_at": "2025-06-14T15:02:44Z",
"created_at": "2025-06-14T15:02:44Z",
"updated_at": "2026-11-03T18:14:37Z"
}Show her balance on the first screen after sign-up. Guests notice when their points come with them.
The rules:
- Only verified phones link. Send
phone_verified: trueonly when the guest proved they own the number with a code you sent. A verified phone is the only thing standing between a stranger and someone else's points. - Unverified phones never link. unPLUG creates a new member and stores the phone as unverified. If you verify it later, update it from your server with
PATCH /v1/members/{id}. - A phone can belong to one account. If another member with an
external_idalready has the number, you get409 phone_in_use. Ask the guest to sign in to their other account, or to use a different number.
Warning Sending phone_verified: true for a number you didn't verify lets anyone claim another guest's points by typing their phone number.
Sign-in for existing accounts#
Guests who signed up before you added loyalty have no stored member id. On their next sign-in, call POST /v1/members with their external_id and store the id you get back. Because it's an upsert, you can run the same code on every sign-in until each user has an id, then skip it. After that, sign-in only needs a member token.
Find a member#
Look a member up by external_id, email or phone. Matches are exact; there's no partial search.
curl "https://api.unplugdining.com/v1/members?external_id=usr_10482" \
-H "Authorization: Bearer $UNPLUG_KEY"const response = await fetch("https://api.unplugdining.com/v1/members?external_id=usr_10482", {
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
},
});
const data = await response.json();import os
import requests
response = requests.get(
"https://api.unplugdining.com/v1/members?external_id=usr_10482",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
},
)
data = response.json(){
"data": [
{
"id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"external_id": "usr_10482",
"status": "active",
"first_name": "Maya",
"last_name": "Lopez",
"email": "maya@example.com",
"phone": "+15125550142",
"points_balance": 420
}
],
"has_more": false,
"next_cursor": null
}The list is trimmed here. Your secret key needs members:read. In the app, use GET /v1/members/me with the member token instead.
Update a profile#
When a guest edits their profile in your app, send the change to unPLUG from either side:
| Field | Member token (PATCH /v1/members/me) | Secret key (PATCH /v1/members/{id}) |
|---|---|---|
first_name, last_name, email | Yes | Yes |
phone, phone_verified | No | Yes |
birthday | Once, when empty | Yes, to correct it |
metadata | Yes | Yes |
curl -X PATCH https://api.unplugdining.com/v1/members/3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40 \
-H "Authorization: Bearer $UNPLUG_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone": "+15125550187", "phone_verified": true }'const response = await fetch("https://api.unplugdining.com/v1/members/3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.UNPLUG_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ phone: "+15125550187", phone_verified: true }),
});
const data = await response.json();import os
import requests
response = requests.patch(
"https://api.unplugdining.com/v1/members/3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
headers={
"Authorization": f"Bearer {os.environ['UNPLUG_KEY']}",
},
json={"phone": "+15125550187", "phone_verified": True},
)
data = response.json()Phone changes go through your server so you can verify the new number first. A member.updated webhook fires with the old values in previous_attributes.
Birthdays#
Birthday rewards land in the wallet automatically, so guests have a reason to give you a real date. To stop guests from changing it every month for another reward, the app can set it once. After that, only your server can correct it, for example after a support request.
curl -X PATCH https://api.unplugdining.com/v1/members/me \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS" \
-H "Content-Type: application/json" \
-d '{ "birthday": "1994-03-18" }'const response = await fetch("https://api.unplugdining.com/v1/members/me", {
method: "PATCH",
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
"Content-Type": "application/json",
},
body: JSON.stringify({ birthday: "1994-03-18" }),
});
const data = await response.json();import requests
response = requests.patch(
"https://api.unplugdining.com/v1/members/me",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
json={"birthday": "1994-03-18"},
)
data = response.json()Use YYYY-MM-DD. Tell the guest in the form that they can't change it later.
Communication preferences#
Marketing consent lives on the member, separate from OS push permission. Show the three toggles in your settings screen and save changes with the member token:
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(){
"email_marketing": true,
"sms_marketing": false,
"push_marketing": false,
"updated_at": "2026-11-20T02:41:19Z",
"source": "app"
}unPLUG sets source for you: app or web for member tokens, api for your server, unsubscribe_link when the guest unsubscribes from an email. Each change fires communication_preferences.updated, so if you also keep consent in your own system, update it from that webhook. Your server uses /v1/members/{id}/communication_preferences.
Metadata#
Use metadata for small facts about the guest that your app wants back, like a favorite store. Up to 20 keys, each value up to 500 characters.
curl -X PATCH https://api.unplugdining.com/v1/members/me \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS" \
-H "Content-Type: application/json" \
-d '{ "metadata": { "favorite_store": "south-congress" } }'const response = await fetch("https://api.unplugdining.com/v1/members/me", {
method: "PATCH",
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
"Content-Type": "application/json",
},
body: JSON.stringify({ metadata: { favorite_store: "south-congress" } }),
});
const data = await response.json();import requests
response = requests.patch(
"https://api.unplugdining.com/v1/members/me",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
json={"metadata": {"favorite_store": "south-congress"}},
)
data = response.json()Don't put personal data or secrets in metadata. Guests can set it from the app, so treat what you read back as untrusted input.
Delete an account#
Apple requires apps that let people create an account to let them delete it from inside the app. Google Play has the same rule and also asks for a web link. One call removes the loyalty side:
curl -X DELETE https://api.unplugdining.com/v1/members/me \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS"const response = await fetch("https://api.unplugdining.com/v1/members/me", {
method: "DELETE",
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
});
console.log(response.status);import requests
response = requests.delete(
"https://api.unplugdining.com/v1/members/me",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
)
print(response.status_code)- 202 Accepted means the deletion has started. It finishes within 30 days and usually within minutes.
- Personal data is erased. Points and rewards are forfeited and can't be restored. Say so on your confirmation screen, with the current balance.
- A
member.deletedwebhook fires. Use it to clean up the member id on your side. - After deletion, calls for this member return
410 member_deleted. If the same person signs up again with the sameexternal_id, they start as a new member with no points.
Your server can do the same with DELETE /v1/members/{id}, for example when a guest asks support to delete them. Delete your own account in the same flow.
{
"id": "8b4e1d7a-2c9f-4a3e-b6d0-5f7c1a9e3b82",
"type": "member.deleted",
"created_at": "2026-12-14T02:44:51Z",
"api_version": "v1",
"livemode": true,
"brand_id": "e5a1c7d9-2f4b-4c8e-a3d6-8b0f1e9c2a57",
"data": {
"object": {
"id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40",
"external_id": "usr_10482",
"status": "deleted",
"deleted_at": "2026-12-14T02:44:51Z"
},
"previous_attributes": {}
}
}Merge two members#
Coming soon
For guests who ended up with two members, for example one from an unverified sign-up and one from the register. Your support team merges them in the unPLUG Dashboard, or your own tools call POST /v1/members/{id}/merge in the Management API. Until it ships, verify phones before sign-up so the duplicate doesn't happen.
What to store on your side#
- The member
id, on your user record. Everything else can be read from unPLUG. - Not the balance. Read it live with
GET /v1/members/meorGET /v1/members/{id}. If you cache it, update it frompoints.changedwebhooks, which carrybalance_after. Points also change from register visits, adjustments and refunds you didn't make. - Not the member token. It expires in 15 minutes; your app keeps it in memory.
Checklist#
- Sign-up calls
POST /v1/memberswith your user id asexternal_idand anIdempotency-Key, and stores the memberid. - You send
phone_verified: trueonly after an SMS code check. - You handle
409 phone_in_usewith a message to the guest. - Phone changes go through your server.
- The app has account deletion that calls
DELETE /v1/members/me, and your server handlesmember.deleted.