unPLUGDevelopers

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.

  1. Create your own account first. Your user id becomes the external_id.
  2. Call POST /v1/members from your server with an Idempotency-Key.
  3. Store the returned id on your user record. You need it for tokens, orders and lookups.
POSTRequest
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
    }
  }'

The call is an upsert on external_id:

  • 201: a new member was created.
  • 200: a member with this external_id already 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:

Response200
{
  "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: true only 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_id already has the number, you get 409 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.

GETRequest
curl "https://api.unplugdining.com/v1/members?external_id=usr_10482" \
  -H "Authorization: Bearer $UNPLUG_KEY"
Response200
{
  "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:

FieldMember token (PATCH /v1/members/me)Secret key (PATCH /v1/members/{id})
first_name, last_name, emailYesYes
phone, phone_verifiedNoYes
birthdayOnce, when emptyYes, to correct it
metadataYesYes
PATCHRequest
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 }'

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.

PATCHRequest
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" }'

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:

PATCHRequest
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 }'
Response200
{
  "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.

PATCHRequest
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" } }'

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:

DELETERequest
curl -X DELETE https://api.unplugdining.com/v1/members/me \
  -H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS"
  • 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.deleted webhook 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 same external_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.

Webhookmember.deleted
{
  "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/me or GET /v1/members/{id}. If you cache it, update it from points.changed webhooks, which carry balance_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/members with your user id as external_id and an Idempotency-Key, and stores the member id.
  • You send phone_verified: true only after an SMS code check.
  • You handle 409 phone_in_use with 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 handles member.deleted.