Skip to content
Last updated

Code Examples

These examples use the same Percents API sandbox scenario across TypeScript, Go, Java, and C#.

A cardholder group (CHG) is a collection of cardholders that Percents treats as one reward entity. Cardholders in the same CHG share merchant activation, cashback, and branded balance state, so they can earn rewards and spend branded balance as if they were one.

  • Base URL: https://sandbox.percents.com
  • API authentication header: Authorization: token tok_11111111-1111-4111-8111-111111111111:api_test_secret
  • Cardholder group id: chg_22222222-2222-4222-8222-222222222222
  • Merchant id: mp_33333333-3333-4333-8333-333333333333
  • Offer id: mpo_44444444-4444-4444-8444-444444444444
  • Card authorization event id: issuer-auth-0001
  • Earn settlement id: issuer-txn-0001
  • Spend settlement id: issuer-txn-0002

The merchantId field on ingested card authorization event and settled transaction event payloads is normal transaction data representing the merchant identifier from the ISO 8583 transaction message. In sandbox only, Percents overloads merchantId as a forced-match input: pass the Percents mp_ merchant partner id for the merchant the transaction should match. It is separate from API authentication.

The flow is:

  1. Register a cardholder group.
  2. Fetch merchants and offers.
  3. Activate the merchant through an offer.
  4. Ingest a card authorization event and settled transaction.
  5. Process the expected qualified_auth and qualified_settlement webhooks.
  6. Ingest a follow-up settled transaction at the same merchant after branded balance is available.
  7. Process the expected qualified_settlement webhook containing a spend reward effect.
  8. Read balances or transaction reward detail for reconciliation.

Webhook payload shapes, signing, and retry behavior are documented on Webhooks. The examples below show where an integration should wait for qualified_auth and qualified_settlement events.

Simplified Client Examples

These examples use simplified client method names to make the flow readable. Use the full HTTP examples or generated API reference for exact request shape until official SDKs are published.

const client = createExamplePercentsClient({
  baseUrl: 'https://sandbox.percents.com',
  authorization: 'token tok_11111111-1111-4111-8111-111111111111:api_test_secret',
});

const cardholderGroup = await client.cardholderGroups.register({
  externalId: 'issuer-group-001',
  defaultLanguage: 'en',
  cardholders: [
    {
      externalId: 'issuer-cardholder-001',
      cards: [
        {
          externalId: 'issuer-card-001',
          last4: '4242',
          network: 'visa',
          physical: true,
          billingZip: '10001',
        },
      ],
    },
  ],
});

const merchants = await client.merchants.listForCardholderGroup(cardholderGroup.id);
const merchant = merchants.find((row) => row.id === 'mp_33333333-3333-4333-8333-333333333333');

const offers = await client.offers.listForCardholderGroup({
  chgId: cardholderGroup.id,
  merchantId: merchant.id,
});

await client.activations.activateMerchantOffer({
  chgId: cardholderGroup.id,
  merchantId: merchant.id,
  offerId: offers[0].id,
});

await client.transactions.ingestAuthorization({
  id: 'issuer-auth-0001',
  amount: 5000,
  currency: 'usd',
  cardholderId: 'issuer-cardholder-001',
  cardId: 'issuer-card-001',
  merchantId: merchant.id,
  state: 'approved',
});

await client.webhooks.expect('qualified_auth', {
  reasonCode: 'earn',
  timing: 'preview',
});

await client.transactions.ingestSettlement({
  id: 'issuer-txn-0001',
  externalAuthId: 'issuer-auth-0001',
  amount: 5000,
  currency: 'usd',
  cardholderId: 'issuer-cardholder-001',
  cardId: 'issuer-card-001',
  merchantId: merchant.id,
  state: 'settled',
});

await client.webhooks.expect('qualified_settlement', {
  reasonCode: 'earn',
  timing: 'posted',
});

const balance = await client.balances.getForMerchant({
  chgId: cardholderGroup.id,
  merchantId: merchant.id,
});

if (balance.canSpendNow) {
  await client.transactions.ingestSettlement({
    id: 'issuer-txn-0002',
    amount: 1000,
    currency: 'usd',
    cardholderId: 'issuer-cardholder-001',
    cardId: 'issuer-card-001',
    merchantId: merchant.id,
    state: 'settled',
  });

  await client.webhooks.expect('qualified_settlement', {
    reasonCode: 'spend',
    timing: 'posted',
  });
}

await client.reconciliation.getTransactionRewardDetail({
  chgId: cardholderGroup.id,
  txnId: 'txn_66666666-6666-4666-8666-666666666666',
});

Full HTTP Examples

These examples use raw HTTP calls. The helper functions keep the flow readable while still showing the actual endpoints.

const baseUrl = 'https://sandbox.percents.com';
const authorization = 'token tok_11111111-1111-4111-8111-111111111111:api_test_secret';

async function request(method: string, path: string, body?: unknown) {
  const response = await fetch(`${baseUrl}${path}`, {
    method,
    headers: { Authorization: authorization, 'Content-Type': 'application/json' },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const text = await response.text();
  if (!response.ok) throw new Error(`${method} ${path} failed: ${response.status} ${text}`);
  return text ? JSON.parse(text) : undefined;
}

const cardholderGroup = await request('POST', '/api/v2/chg', {
  externalId: 'issuer-group-001',
  defaultLanguage: 'en',
  active: true,
  cardholders: [
    {
      externalId: 'issuer-cardholder-001',
      active: true,
      cards: [
        {
          externalId: 'issuer-card-001',
          last4: '4242',
          bin: '411111',
          network: 'visa',
          physical: true,
          billingZip: '10001',
          active: true,
        },
      ],
    },
  ],
  idempotencyKey: 'setup-001',
});

const merchants = await request(
  'GET',
  `/api/v2/chg/${cardholderGroup.id}/merchants?page=1&pageSize=25`,
);
const merchant = merchants.data.find(
  (row: { id: string }) => row.id === 'mp_33333333-3333-4333-8333-333333333333',
);
const offers = await request(
  'GET',
  `/api/v2/chg/${cardholderGroup.id}/merchant/${merchant.id}/offers?page=1&pageSize=10`,
);

await request('PUT', `/api/v2/chg/${cardholderGroup.id}/merchant/${merchant.id}/activation`, {
  presentedOfferId: offers.data[0].id,
  source: 'issuer_host_app',
  idempotencyKey: 'activation-001',
});

await request(
  'POST',
  '/api/v1/incoming-auth',
  transactionPayload({
    id: 'issuer-auth-0001',
    amount: 5000,
    merchantId: merchant.id,
    state: 'approved',
  }),
);
// Expect: qualified_auth webhook with rewardEffects[0].timing === 'preview'.

await request(
  'POST',
  '/api/v1/incoming-txn',
  transactionPayload({
    id: 'issuer-txn-0001',
    externalAuthId: 'issuer-auth-0001',
    amount: 5000,
    merchantId: merchant.id,
    state: 'settled',
  }),
);
// Expect: qualified_settlement webhook with an earn effect and posted timing.

const balance = await request(
  'GET',
  `/api/v2/chg/${cardholderGroup.id}/merchant/${merchant.id}/balance`,
);
if (balance.canSpendNow) {
  await request(
    'POST',
    '/api/v1/incoming-txn',
    transactionPayload({
      id: 'issuer-txn-0002',
      amount: 1000,
      merchantId: merchant.id,
      state: 'settled',
    }),
  );
  // Expect: qualified_settlement webhook with a spend effect and posted timing.
}

await request(
  'GET',
  `/api/v2/chg/${cardholderGroup.id}/transaction/txn_66666666-6666-4666-8666-666666666666/reward-detail`,
);

function transactionPayload(input: {
  id: string;
  amount: number;
  merchantId: string;
  state: string;
  externalAuthId?: string;
}) {
  return {
    ...input,
    currency: 'usd',
    merchantDescriptor: 'Sandbox Merchant',
    mcc: '5814',
    createdAt: '2026-07-01T18:42:00.000Z',
    cardLast4: '4242',
    network: 'visa',
    cardholderId: 'issuer-cardholder-001',
    cardId: 'issuer-card-001',
    authMethod: 'online',
    authTimestamp: '2026-07-01T18:42:00.000Z',
  };
}