Skip to main content
This page covers server-side operations. They use the secret Org-API-Key on your backend only - never ship it in an app, a mobile binary, or any client-side code. A request to any of these endpoints with a member token is rejected with 403. See Authentication.
Expys splits into two modes. Most data calls an app makes - browsing offers, redeeming, eligibility, wallet reads, concierge - run in member mode with a short-lived member token. A smaller, privileged set of operations run in server mode with the Org-API-Key, and those must execute only on a backend you control.

Which operations are server-side

Every operation below requires the Org-API-Key. A member token on any of them returns 403.
OperationSDK methodDocumented in
Exchange for a member tokenexchangeToken(...)Authentication
Credit points / mintcreditPoints(...)Points and wallet
Upsert a membersetMember(...)Members and tiers
Read a member summarygetMember(...)Members and tiers
Remove a memberremoveMember(...)Members and tiers
Program analyticsanalyticsSummary(), analyticsOffers(), analyticsTimeseries(...)Analytics
Manage webhookscreateWebhook(...), list, deleteWebhooks

Why the boundary exists

The two credentials carry very different authority, and that difference is the whole reason for the split.

Org-API-Key

A long-lived secret with full authority over your org: it can mint tokens for any member, move points, change tiers, and read every member’s data. It must stay on your backend.

Member token

A short-lived credential scoped to a single member. If it leaks it exposes one member for a few minutes; it cannot touch other members or any org-wide operation.
Because the Org-API-Key can do anything, it never leaves your backend. The app only ever holds member tokens, which your backend mints on demand with exchangeToken.

Member mode vs server mode

Member modeServer mode
CredentialMember token (Bearer)Org-API-Key (Bearer)
Runs inThe app / SDK clientYour backend only
ScopeOne member, short-livedThe whole org
Example operationslistOffers, createRedemption, checkEligibility, getWallet, conciergeexchangeToken, creditPoints, setMember, analyticsSummary, createWebhook
Wrong credentialServer-side endpoints return 403-

Construct the server client

Initialize the SDK with the Org-API-Key as its token. The server-mode methods are additionally guarded client-side against being called with a member token.
import { initialize } from "@expys/sdk";

const orgApiKey = process.env.EXPYS_ORG_API_KEY;
if (!orgApiKey) {
  throw new Error(
    "Set EXPYS_ORG_API_KEY (your secret Org-API-Key, e.g. expys_live_...). " +
      "Run this on a backend only, never in a client app.",
  );
}

// Construct the client with the machine credential as the token. Server-mode
// methods are guarded against member tokens client-side.
const expys = initialize({
  baseUrl: process.env.EXPYS_BASE_URL,
  environment: "sandbox",
  token: orgApiKey,
});

const externalUserID = process.env.EXPYS_EXTERNAL_USER_ID ?? "user_42";

async function main(): Promise<void> {
  // Mint a short-lived member token for your app to use (return this to the app,
  // never the Org-API-Key). Idempotent POST: a retry replays rather than re-mints.
  const grant = await expys.exchangeToken({ externalUserID });
  console.log(`minted member token expiring at ${grant.expiresAt}`);

  // Upsert the member's profile. PUT is idempotent by HTTP semantics (no key).
  const member = await expys.setMember(externalUserID, {
    displayName: "Ada Lovelace",
    tier: "gold",
  });
  console.log(`member ${member.externalUserID} is now tier=${member.tier}`);

  // Credit points to the member's wallet. Idempotent POST sends an Idempotency-Key.
  const credited = await expys.creditPoints({
    amount: 100,
    externalUserID,
    reason: "welcome bonus",
  });
  console.log(`new balance: ${credited.balance} ${credited.currency.symbol}`);

  // Register a webhook. The signingSecret is shown ONLY on creation - store it now.
  const webhook = await expys.createWebhook({
    events: ["redemption.created"],
    url: "https://example.com/expys/webhooks",
  });
  console.log(`webhook ${webhook.id} secret: ${webhook.signingSecret}`);

  // Org-wide analytics rollups.
  const summary = await expys.analyticsSummary();
  console.log(
    `members: ${summary.memberCount}, minted: ${summary.pointsMinted}`,
  );
}
curl
curl -X POST https://api.expys.com/v1/auth/exchange \
  -H "Authorization: Bearer YOUR_ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "externalUserID": "user_123" }'
The 403 is enforced on the server: even if a member token reached one of these endpoints, the API rejects it. The client-side guard in the SDK is a second layer that fails fast before the request is sent.

Security checklist

The Org-API-Key is read only on your backend, never bundled into an app.
The app receives only member tokens (from exchangeToken), with their expiry.
Server-side calls - mint, credit, member management, analytics, webhooks - originate from your backend.
The Org-API-Key is stored as a backend secret, not in client-shipped environment variables or source.
If an Org-API-Key is ever exposed, rotate it immediately - it carries full org authority.

Server-side pages

Authentication

Exchange the Org-API-Key for short-lived member tokens and refresh them.

Members and tiers

Upsert profiles and tiers, read member summaries, and remove members.

Points and wallet

Mint and credit points into a member’s wallet from your backend.

Analytics

Program-wide rollups: summary, per-offer, and timeseries.

Webhooks

Register signed, retried event deliveries to your backend.