unPLUGDevelopers

Authentication

Your server calls unPLUG with a secret key. Your app calls unPLUG with a member token that your server hands it after your own sign-in. Set up both, and keep the secret key on your server.

Available at launch

Two credentials#

CredentialLooks likeWhere it livesWhat it can do
Secret keyuk_live_...
uk_test_...
Your server onlyEvery server call its scopes allow, for one brand. Test keys work only on the sandbox host, live keys only on production.
Member tokenmt_...Your app or web pageCalls about one member, for 15 minutes.

Both go in the same header:

Header
Authorization: Bearer uk_live_...      # from your server
Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS   # from your app

There's a third secret, the webhook signing secret (whsec_...). It doesn't authenticate requests; it verifies the webhooks we send you. See Webhooks.

Warning Never put a secret key in an app, a web page or a public repo. Anyone who has it can create orders and move points for your whole brand.

Scopes#

Each secret key carries a set of scopes. A full-access key has all of them. A call outside the key's scopes returns 403 missing_scope.

ScopeAllows
program:readRead the program, locations and the rewards catalog.
members:readRead and look up members, their activity and wallet.
members:writeCreate, update and delete members, change communication preferences, revoke a member's tokens, send app events from your server.
member_tokens:createMint member tokens with POST /v1/member_tokens.
points:adjustAdd or remove points with POST /v1/members/{id}/points_adjustments. Management API.
checkout:writePreviews, redemptions, orders and refunds.
webhooks:manageCreate and change webhook endpoints, read the event log, redeliver events.
offers:writeCreate, publish and grant offers. Management API. Coming soon
challenges:writeCreate and run challenges. Management API. Coming soon
reports:readRead reports. Management API. Coming soon

Give each service the smallest set it needs. An app backend that signs guests in and posts orders needs program:read, members:read, members:write, member_tokens:create and checkout:write. It never needs the Management API scopes, because the unPLUG Dashboard does that work. A guest support tool might only need members:read and points:adjust. Ask support for one key per service so you can revoke one without touching the others.

Calling for a member from your server#

Almost every App API call a member token can make also works from your server with a secret key, so you can run a server-to-server integration where the app only talks to you. The server versions take the member in the path or in a member_id field. Registering a device, recording a push open and showing the in-store code stay in the app, because they belong to the phone.

Warning With a secret key, unPLUG trusts whatever member id you send. Always take it from your own signed-in session, never from a value the app or browser sends, or one guest could read or spend another guest's points.

What a member token can call#

  • GET /v1/program, GET /v1/locations, GET /v1/rewards
  • Everything under /v1/members/me/...: profile, activity, wallet, devices, messages, communication preferences
  • POST /v1/checkouts/preview and POST /v1/redemptions for its own member
  • GET /v1/redemptions/{id} and POST /v1/redemptions/{id}/cancel for its own member's redemptions, so the app can release a hold when the guest leaves checkout
  • POST /v1/app_events

A member token can't create orders, adjust points, change the member's phone number or read any other member. Those calls need your server.

The member token flow#

Your guests sign in with your own login. unPLUG never sees their password. Once they're signed in, your server vouches for them by asking unPLUG for a token tied to their member.

Member token sequence The guest signs in to your app. The app asks your server for a member token. Your server calls POST /v1/member_tokens with the secret key and gets back a token that lasts 15 minutes, then passes it to the app. The app calls the unPLUG API directly with the token. When the token expires, the app asks your server again. Your app Your server unPLUG API 1. Sign in with your login 2. Ask for a member token POST /loyalty/token 3. POST /v1/member_tokens secret key, member_id 4. mt_... for 15 minutes 5. token, expires_at 6. GET /v1/members/me Bearer mt_... 7. Balance, tier, rewards On 401 token_expired, the app goes back to step 2.
The secret key never leaves your server. The app holds a token for one member that expires in 15 minutes.
  1. The guest signs in to your app with your own login: password, social sign-in, SMS code, whatever you use today.
  2. Your app asks your server for a member token, on a route you add, for example POST /loyalty/token. Your server checks the guest's session like it does for any other route.
  3. Your server calls POST /v1/member_tokens with the secret key and the guest's member id (or your external_id for them).
  4. unPLUG returns a token that lasts 15 minutes. There is no refresh token.
  5. Your server passes the token and expires_at to the app.
  6. Your app calls unPLUG directly with Authorization: Bearer mt_... to show the balance, wallet and rewards, preview a cart and register for push.

Mint a token#

POSTRequest
curl -X POST https://api.unplugdining.com/v1/member_tokens \
  -H "Authorization: Bearer $UNPLUG_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "member_id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40" }'
Response201
{
  "token": "mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
  "expires_at": "2026-11-03T18:37:11Z",
  "member_id": "3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40"
}

You can send { "external_id": "usr_10482" } instead of member_id. The key needs the member_tokens:create scope. A deleted member returns 410 member_deleted, and an unknown one returns 404 member_not_found.

The route on your server#

The whole route is a session check and one call. These examples assume your session middleware already loaded the signed-in user and that you stored their unPLUG member id at sign-up.

server.js (Express)
const UNPLUG_API = process.env.UNPLUG_API;  // https://api.unplugdining.com/v1
const UNPLUG_KEY = process.env.UNPLUG_KEY;  // uk_live_..., never shipped to the app

app.post("/loyalty/token", requireSession, async (req, res) => {
  const memberId = req.user.unplugMemberId;
  if (!memberId) return res.status(404).json({ error: "not_enrolled" });

  const r = await fetch(`${UNPLUG_API}/member_tokens`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${UNPLUG_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ member_id: memberId }),
    signal: AbortSignal.timeout(10_000),
  });

  if (!r.ok) {
    const problem = await r.json().catch(() => ({}));
    console.error("member token failed", r.status, problem.code, r.headers.get("request-id"));
    return res.status(502).json({ error: "loyalty_unavailable" });
  }

  const { token, expires_at } = await r.json();
  res.set("Cache-Control", "no-store").json({ token, expires_at });
});

Take the member id from the session, never from the request body. If the app could choose the member, any signed-in guest could get a token for someone else.

Use and renew the token in your app#

Keep the token in memory. It lasts 15 minutes, so there's no reason to write it to disk. When it expires, unPLUG returns 401 with code token_expired; ask your server for a new one and retry the call once.

JavaScript (app or web)
let current = null; // { token, expires_at }

async function memberToken({ force = false } = {}) {
  const stillGood = current && Date.parse(current.expires_at) - Date.now() > 60_000;
  if (!force && stillGood) return current.token;
  const r = await fetch("/loyalty/token", { method: "POST", credentials: "include" });
  if (!r.ok) throw new Error("loyalty token unavailable");
  current = await r.json();
  return current.token;
}

async function unplug(path, init = {}) {
  const call = (token) => fetch(`https://api.unplugdining.com/v1${path}`, {
    ...init,
    headers: { ...init.headers, Authorization: `Bearer ${token}` },
  });

  let r = await call(await memberToken());
  if (r.status === 401) {
    const problem = await r.clone().json().catch(() => ({}));
    if (problem.code === "token_expired") r = await call(await memberToken({ force: true }));
  }
  return r;
}

// const me = await (await unplug("/members/me")).json();

Renewing a minute early saves a round trip on most calls; the 401 path covers the rest, for example after the app wakes from the background. If a fresh token also gets a 401, stop and show a signed-out loyalty state instead of looping.

Sign out and revoke#

  • Sign out on one device. The app calls DELETE /v1/members/me/devices/{device_id} while it still has a token, then forgets the token. Without the device delete, the next guest on that phone could get the previous guest's pushes.
  • Sign out everywhere. Your server calls POST /v1/members/{id}/revoke_tokens. Every member token issued for that member stops working at once. Use it when a guest changes their password or reports a lost phone.
POSTRequest
curl -X POST https://api.unplugdining.com/v1/members/3f2b8c1e-6d0a-4e7b-9f3c-2a1d5e8b7c40/revoke_tokens \
  -H "Authorization: Bearer $UNPLUG_KEY"

Revoking doesn't stop your server from minting a new token. Make sure your own session is gone too, or the app will get a new token on its next request.

Rotate a key#

Two secret keys can be active at once, so you can rotate without downtime.

  1. Ask support for a new key with the same scopes. Say which key it replaces by its environment and the service using it.
  2. Deploy the new key to every server and job that uses the old one.
  3. Watch your logs until no request goes out with the old key.
  4. Ask support to revoke the old key. Calls with it then return 401 invalid_api_key.

Rotate when someone with access to the key leaves, and any time you suspect a leak.

IP allowlist#

You can lock a key to your servers' egress IPs. Requests with that key from any other address are rejected. Send support the IPs or CIDR ranges and the key they apply to. Keep the list current before you move servers or change your NAT gateway; otherwise your own calls get rejected.

If a key leaks#

Treat any key that reached an app build, a web bundle, a public repo, a ticket or a chat as leaked, even if you deleted it afterwards.

  1. Email support right away at support@unplugdining.com with a subject starting "Security:". Say which key (environment and service), where it leaked and when. Never include the key itself.
  2. Ask for a replacement key in the same email. We revoke the leaked key within one hour on business days.
  3. Deploy the replacement as soon as you get it.
  4. Check what the key did. Look for unexpected adjust entries in member activity (every adjustment records the key that made it) and for orders you don't recognize. Send us anything odd with its Request-Id.

Coming soon We'll join GitHub's secret scanning partner program, so a key pushed to a public repo is reported to us automatically.

unPLUG-hosted sign-in#

Coming soon

For brands that don't have their own login. Your app opens an unPLUG sign-in page with your logo in the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android), the guest enters their phone number and an SMS code, and your app gets a member token plus a refresh token.

  • OAuth 2.0 authorization code flow with PKCE, per RFC 8252 for native apps.
  • Authorize: GET https://auth.unplugdining.com/authorize
  • Token: POST https://auth.unplugdining.com/token
  • Refresh tokens rotate: each refresh returns a new one and the old one stops working.

If you have your own login, use the member token flow above. It's available now and keeps the guest in one account system.

Developer dashboard#

Coming soon

A self-serve page for credentials: create, roll and revoke keys, manage webhook endpoints, upload push credentials and read request logs. It manages credentials only. Program settings such as earn rate, tiers and rewards stay in the unPLUG dashboard.

What needs support today#

Until the developer dashboard ships, email support@unplugdining.com for:

  • Sandbox and live keys, extra keys per service, and scope changes
  • Rotating or revoking a key, and leaks (subject starting "Security:")
  • IP allowlists
  • Higher rate limits
  • Push credentials (through the secure upload link we send, never as an attachment)

Include your brand name, the environment and any Request-Id. We never ask for a secret key, so never send one.

Checklist#

  • The secret key is only on your server, loaded from a secrets manager or environment variable.
  • Your token route, and every server call that names a member, takes the member from the session, not from the request.
  • The app keeps the token in memory and renews on 401 token_expired, once.
  • Sign out deletes the device, then forgets the token.
  • Password changes and account locks call revoke_tokens.
  • You've written down how to rotate a key and who emails support if one leaks.