# 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](/percents-api/auth-security/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.

```typescript TypeScript
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',
});
```

```python Python
client = ExamplePercentsClient(
    base_url="https://sandbox.percents.com",
    authorization="token tok_11111111-1111-4111-8111-111111111111:api_test_secret",
)

cardholder_group = client.cardholder_groups.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"}],
    }],
})

merchants = client.merchants.list_for_cardholder_group(cardholder_group["id"])
merchant = next(row for row in merchants if row["id"] == "mp_33333333-3333-4333-8333-333333333333")

offers = client.offers.list_for_cardholder_group(
    chg_id=cardholder_group["id"],
    merchant_id=merchant["id"],
)

client.activations.activate_merchant_offer(
    chg_id=cardholder_group["id"],
    merchant_id=merchant["id"],
    offer_id=offers[0]["id"],
)

client.transactions.ingest_authorization({
    "id": "issuer-auth-0001",
    "amount": 5000,
    "currency": "usd",
    "cardholderId": "issuer-cardholder-001",
    "cardId": "issuer-card-001",
    "merchantId": merchant["id"],
    "state": "approved",
})

client.webhooks.expect("qualified_auth", reason_code="earn", timing="preview")

client.transactions.ingest_settlement({
    "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",
})

client.webhooks.expect("qualified_settlement", reason_code="earn", timing="posted")

balance = client.balances.get_for_merchant(chg_id=cardholder_group["id"], merchant_id=merchant["id"])
if balance["canSpendNow"]:
    client.transactions.ingest_settlement({
        "id": "issuer-txn-0002",
        "amount": 1000,
        "currency": "usd",
        "cardholderId": "issuer-cardholder-001",
        "cardId": "issuer-card-001",
        "merchantId": merchant["id"],
        "state": "settled",
    })
    client.webhooks.expect("qualified_settlement", reason_code="spend", timing="posted")

client.reconciliation.get_transaction_reward_detail(
    chg_id=cardholder_group["id"],
    txn_id="txn_66666666-6666-4666-8666-666666666666",
)
```

```go Go
ctx := context.Background()
client := NewExamplePercentsClient(ExamplePercentsConfig{
	BaseURL:       "https://sandbox.percents.com",
	Authorization: "token tok_11111111-1111-4111-8111-111111111111:api_test_secret",
})

cardholderGroup, err := client.CardholderGroups.Register(ctx, map[string]any{
	"externalId":      "issuer-group-001",
	"defaultLanguage": "en",
	"cardholders": []map[string]any{{
		"externalId": "issuer-cardholder-001",
		"cards": []map[string]any{{
			"externalId": "issuer-card-001",
			"last4":      "4242",
			"network":    "visa",
			"physical":   true,
			"billingZip": "10001",
		}},
	}},
})
if err != nil {
	return err
}

merchants, err := client.Merchants.ListForCardholderGroup(ctx, cardholderGroup.ID)
if err != nil {
	return err
}
merchant := findMerchant(merchants, "mp_33333333-3333-4333-8333-333333333333")

offers, err := client.Offers.ListForCardholderGroup(ctx, cardholderGroup.ID, merchant.ID)
if err != nil {
	return err
}

err = client.Activations.ActivateMerchantOffer(ctx, cardholderGroup.ID, merchant.ID, offers[0].ID)
if err != nil {
	return err
}

err = client.Transactions.IngestAuthorization(ctx, map[string]any{
	"id":           "issuer-auth-0001",
	"amount":       5000,
	"currency":     "usd",
	"cardholderId": "issuer-cardholder-001",
	"cardId":       "issuer-card-001",
	"merchantId":   merchant.ID,
	"state":        "approved",
})
if err != nil {
	return err
}
client.Webhooks.Expect(ctx, "qualified_auth", "earn", "preview")

err = client.Transactions.IngestSettlement(ctx, map[string]any{
	"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",
})
if err != nil {
	return err
}
client.Webhooks.Expect(ctx, "qualified_settlement", "earn", "posted")

balance, err := client.Balances.GetForMerchant(ctx, cardholderGroup.ID, merchant.ID)
if err != nil {
	return err
}
if balance.CanSpendNow {
	err = client.Transactions.IngestSettlement(ctx, map[string]any{
		"id":           "issuer-txn-0002",
		"amount":       1000,
		"currency":     "usd",
		"cardholderId": "issuer-cardholder-001",
		"cardId":       "issuer-card-001",
		"merchantId":   merchant.ID,
		"state":        "settled",
	})
	if err != nil {
		return err
	}
	client.Webhooks.Expect(ctx, "qualified_settlement", "spend", "posted")
}

_, err = client.Reconciliation.GetTransactionRewardDetail(ctx, cardholderGroup.ID, "txn_66666666-6666-4666-8666-666666666666")
return err
```

```java Java
ExamplePercentsClient client = new ExamplePercentsClient(
    "https://sandbox.percents.com",
    "token tok_11111111-1111-4111-8111-111111111111:api_test_secret"
);

CardholderGroup cardholderGroup = client.cardholderGroups().register(Payload.of(
    "externalId", "issuer-group-001",
    "defaultLanguage", "en",
    "cardholders", List.of(Payload.of(
        "externalId", "issuer-cardholder-001",
        "cards", List.of(Payload.of("externalId", "issuer-card-001", "last4", "4242", "network", "visa", "physical", true, "billingZip", "10001"))
    ))
));

Merchant merchant = client.merchants()
    .listForCardholderGroup(cardholderGroup.id())
    .stream()
    .filter(row -> row.id().equals("mp_33333333-3333-4333-8333-333333333333"))
    .findFirst()
    .orElseThrow();

Offer offer = client.offers()
    .listForCardholderGroup(cardholderGroup.id(), merchant.id())
    .get(0);

client.activations().activateMerchantOffer(cardholderGroup.id(), merchant.id(), offer.id());

client.transactions().ingestAuthorization(Payload.of(
    "id", "issuer-auth-0001",
    "amount", 5000,
    "currency", "usd",
    "cardholderId", "issuer-cardholder-001",
    "cardId", "issuer-card-001",
    "merchantId", merchant.id(),
    "state", "approved"
));
client.webhooks().expect("qualified_auth", "earn", "preview");

client.transactions().ingestSettlement(Payload.of(
    "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"
));
client.webhooks().expect("qualified_settlement", "earn", "posted");

MerchantBalance balance = client.balances().getForMerchant(cardholderGroup.id(), merchant.id());
if (balance.canSpendNow()) {
  client.transactions().ingestSettlement(Payload.of(
      "id", "issuer-txn-0002",
      "amount", 1000,
      "currency", "usd",
      "cardholderId", "issuer-cardholder-001",
      "cardId", "issuer-card-001",
      "merchantId", merchant.id(),
      "state", "settled"
  ));
  client.webhooks().expect("qualified_settlement", "spend", "posted");
}

client.reconciliation().getTransactionRewardDetail(
    cardholderGroup.id(),
    "txn_66666666-6666-4666-8666-666666666666"
);
```

```csharp C#
var client = new ExamplePercentsClient(
    baseUrl: "https://sandbox.percents.com",
    authorization: "token tok_11111111-1111-4111-8111-111111111111:api_test_secret");

var cardholderGroup = await client.CardholderGroups.RegisterAsync(new {
    externalId = "issuer-group-001",
    defaultLanguage = "en",
    cardholders = new[] {
        new {
            externalId = "issuer-cardholder-001",
            cards = new[] {
                new { externalId = "issuer-card-001", last4 = "4242", network = "visa", physical = true, billingZip = "10001" },
            },
        },
    },
});

var merchant = (await client.Merchants.ListForCardholderGroupAsync(cardholderGroup.Id))
    .Single(row => row.Id == "mp_33333333-3333-4333-8333-333333333333");
var offer = (await client.Offers.ListForCardholderGroupAsync(cardholderGroup.Id, merchant.Id)).First();

await client.Activations.ActivateMerchantOfferAsync(cardholderGroup.Id, merchant.Id, offer.Id);

await client.Transactions.IngestAuthorizationAsync(new {
    id = "issuer-auth-0001", amount = 5000, currency = "usd",
    cardholderId = "issuer-cardholder-001", cardId = "issuer-card-001",
    merchantId = merchant.Id, state = "approved",
});
await client.Webhooks.ExpectAsync("qualified_auth", reasonCode: "earn", timing: "preview");

await client.Transactions.IngestSettlementAsync(new {
    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.ExpectAsync("qualified_settlement", reasonCode: "earn", timing: "posted");

var balance = await client.Balances.GetForMerchantAsync(cardholderGroup.Id, merchant.Id);
if (balance.CanSpendNow) {
    await client.Transactions.IngestSettlementAsync(new {
        id = "issuer-txn-0002", amount = 1000, currency = "usd",
        cardholderId = "issuer-cardholder-001", cardId = "issuer-card-001",
        merchantId = merchant.Id, state = "settled",
    });
    await client.Webhooks.ExpectAsync("qualified_settlement", reasonCode: "spend", timing: "posted");
}

await client.Reconciliation.GetTransactionRewardDetailAsync(
    cardholderGroup.Id,
    "txn_66666666-6666-4666-8666-666666666666");
```

```ruby Ruby
client = ExamplePercentsClient.new(
  base_url: "https://sandbox.percents.com",
  authorization: "token tok_11111111-1111-4111-8111-111111111111:api_test_secret"
)

cardholder_group = client.cardholder_groups.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" }]
    }
  ]
)

merchants = client.merchants.list_for_cardholder_group(cardholder_group[:id])
merchant = merchants.find { |row| row[:id] == "mp_33333333-3333-4333-8333-333333333333" }
offers = client.offers.list_for_cardholder_group(chg_id: cardholder_group[:id], merchant_id: merchant[:id])

client.activations.activate_merchant_offer(
  chg_id: cardholder_group[:id],
  merchant_id: merchant[:id],
  offer_id: offers.first[:id]
)

client.transactions.ingest_authorization(
  id: "issuer-auth-0001",
  amount: 5000,
  currency: "usd",
  cardholderId: "issuer-cardholder-001",
  cardId: "issuer-card-001",
  merchantId: merchant[:id],
  state: "approved"
)
client.webhooks.expect("qualified_auth", reason_code: "earn", timing: "preview")

client.transactions.ingest_settlement(
  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"
)
client.webhooks.expect("qualified_settlement", reason_code: "earn", timing: "posted")

balance = client.balances.get_for_merchant(chg_id: cardholder_group[:id], merchant_id: merchant[:id])
if balance[:canSpendNow]
  client.transactions.ingest_settlement(
    id: "issuer-txn-0002",
    amount: 1000,
    currency: "usd",
    cardholderId: "issuer-cardholder-001",
    cardId: "issuer-card-001",
    merchantId: merchant[:id],
    state: "settled"
  )
  client.webhooks.expect("qualified_settlement", reason_code: "spend", timing: "posted")
end

client.reconciliation.get_transaction_reward_detail(
  chg_id: cardholder_group[:id],
  txn_id: "txn_66666666-6666-4666-8666-666666666666"
)
```

```rust Rust
let client = ExamplePercentsClient::new(
    "https://sandbox.percents.com",
    "token tok_11111111-1111-4111-8111-111111111111:api_test_secret",
);

let cardholder_group = client.cardholder_groups().register(json!({
    "externalId": "issuer-group-001",
    "defaultLanguage": "en",
    "cardholders": [{
        "externalId": "issuer-cardholder-001",
        "cards": [{"externalId": "issuer-card-001", "last4": "4242", "network": "visa", "physical": true, "billingZip": "10001"}]
    }]
})).await?;

let merchants = client.merchants().list_for_cardholder_group(cardholder_group.id()).await?;
let merchant = merchants.iter()
    .find(|row| row.id == "mp_33333333-3333-4333-8333-333333333333")
    .ok_or("merchant not found")?;

let offers = client.offers()
    .list_for_cardholder_group(cardholder_group.id(), &merchant.id)
    .await?;

client.activations()
    .activate_merchant_offer(cardholder_group.id(), &merchant.id, &offers[0].id)
    .await?;

client.transactions().ingest_authorization(json!({
    "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", "earn", "preview").await?;

client.transactions().ingest_settlement(json!({
    "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", "earn", "posted").await?;

let balance = client.balances().get_for_merchant(cardholder_group.id(), &merchant.id).await?;
if balance.can_spend_now {
    client.transactions().ingest_settlement(json!({
        "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", "spend", "posted").await?;
}

client.reconciliation()
    .get_transaction_reward_detail(cardholder_group.id(), "txn_66666666-6666-4666-8666-666666666666")
    .await?;
```

## Full HTTP Examples

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

```typescript TypeScript HTTP
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',
  };
}
```

```python Python HTTP
import requests

BASE_URL = "https://sandbox.percents.com"
HEADERS = {
    "Authorization": "token tok_11111111-1111-4111-8111-111111111111:api_test_secret",
    "Content-Type": "application/json",
}

def request(method, path, json=None):
    response = requests.request(method, f"{BASE_URL}{path}", json=json, headers=HEADERS, timeout=10)
    response.raise_for_status()
    return response.json() if response.text else None

def transaction_payload(**values):
    return {
        **values,
        "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",
    }

cardholder_group = 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",
})

merchants = request("GET", f"/api/v2/chg/{cardholder_group['id']}/merchants?page=1&pageSize=25")
merchant = next(row for row in merchants["data"] if row["id"] == "mp_33333333-3333-4333-8333-333333333333")
offers = request("GET", f"/api/v2/chg/{cardholder_group['id']}/merchant/{merchant['id']}/offers?page=1&pageSize=10")

request("PUT", f"/api/v2/chg/{cardholder_group['id']}/merchant/{merchant['id']}/activation", {
    "presentedOfferId": offers["data"][0]["id"],
    "source": "issuer_host_app",
    "idempotencyKey": "activation-001",
})

request("POST", "/api/v1/incoming-auth", transaction_payload(
    id="issuer-auth-0001",
    amount=5000,
    merchantId=merchant["id"],
    state="approved",
))
# Expect: qualified_auth webhook with rewardEffects[0].timing == "preview".

request("POST", "/api/v1/incoming-txn", transaction_payload(
    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.

balance = request("GET", f"/api/v2/chg/{cardholder_group['id']}/merchant/{merchant['id']}/balance")
if balance["canSpendNow"]:
    request("POST", "/api/v1/incoming-txn", transaction_payload(
        id="issuer-txn-0002",
        amount=1000,
        merchantId=merchant["id"],
        state="settled",
    ))
    # Expect: qualified_settlement webhook with a spend effect and posted timing.

request("GET", f"/api/v2/chg/{cardholder_group['id']}/transaction/txn_66666666-6666-4666-8666-666666666666/reward-detail")
```

```go Go HTTP
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
)

const baseURL = "https://sandbox.percents.com"
const authorization = "token tok_11111111-1111-4111-8111-111111111111:api_test_secret"

func main() {
	if err := run(); err != nil {
		panic(err)
	}
}

func run() error {
	ctx := context.Background()

	cardholderGroup := request(ctx, "POST", "/api/v2/chg", map[string]any{
		"externalId":      "issuer-group-001",
		"defaultLanguage": "en",
		"active":          true,
		"cardholders": []map[string]any{{
			"externalId": "issuer-cardholder-001",
			"active":     true,
			"cards": []map[string]any{{
				"externalId": "issuer-card-001",
				"last4":      "4242",
				"bin":        "411111",
				"network":    "visa",
				"physical":   true,
				"billingZip": "10001",
				"active":     true,
			}},
		}},
		"idempotencyKey": "setup-001",
	})
	chgID := cardholderGroup["id"].(string)

	merchants := request(ctx, "GET", fmt.Sprintf("/api/v2/chg/%s/merchants?page=1&pageSize=25", chgID), nil)
	merchantID := "mp_33333333-3333-4333-8333-333333333333"
	offers := request(ctx, "GET", fmt.Sprintf("/api/v2/chg/%s/merchant/%s/offers?page=1&pageSize=10", chgID, merchantID), nil)
	offerID := offers["data"].([]any)[0].(map[string]any)["id"].(string)
	_ = merchants

	request(ctx, "PUT", fmt.Sprintf("/api/v2/chg/%s/merchant/%s/activation", chgID, merchantID), map[string]any{
		"presentedOfferId": offerID,
		"source":           "issuer_host_app",
		"idempotencyKey":   "activation-001",
	})

	request(ctx, "POST", "/api/v1/incoming-auth", transactionPayload(map[string]any{
		"id":         "issuer-auth-0001",
		"amount":     5000,
		"merchantId": merchantID,
		"state":      "approved",
	}))
	// Expect: qualified_auth webhook with rewardEffects[0].timing == "preview".

	request(ctx, "POST", "/api/v1/incoming-txn", transactionPayload(map[string]any{
		"id":             "issuer-txn-0001",
		"externalAuthId": "issuer-auth-0001",
		"amount":         5000,
		"merchantId":     merchantID,
		"state":          "settled",
	}))
	// Expect: qualified_settlement webhook with an earn effect and posted timing.

	balance := request(ctx, "GET", fmt.Sprintf("/api/v2/chg/%s/merchant/%s/balance", chgID, merchantID), nil)
	if balance["canSpendNow"].(bool) {
		request(ctx, "POST", "/api/v1/incoming-txn", transactionPayload(map[string]any{
			"id":         "issuer-txn-0002",
			"amount":     1000,
			"merchantId": merchantID,
			"state":      "settled",
		}))
		// Expect: qualified_settlement webhook with a spend effect and posted timing.
	}

	request(ctx, "GET", fmt.Sprintf("/api/v2/chg/%s/transaction/%s/reward-detail", chgID, "txn_66666666-6666-4666-8666-666666666666"), nil)
	return nil
}

func request(ctx context.Context, method string, path string, payload any) map[string]any {
	body, _ := json.Marshal(payload)
	if payload == nil {
		body = nil
	}
	req, _ := http.NewRequestWithContext(ctx, method, baseURL+path, bytes.NewReader(body))
	req.Header.Set("Authorization", authorization)
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil || res.StatusCode < 200 || res.StatusCode >= 300 {
		panic(fmt.Sprintf("%s %s failed", method, path))
	}
	defer res.Body.Close()
	var result map[string]any
	json.NewDecoder(res.Body).Decode(&result)
	return result
}

func transactionPayload(values map[string]any) map[string]any {
	payload := map[string]any{
		"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",
	}
	for key, value := range values {
		payload[key] = value
	}
	return payload
}
```

```java Java HTTP
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class PercentsHttpExample {
  static final String BASE_URL = "https://sandbox.percents.com";
  static final String AUTHORIZATION = "token tok_11111111-1111-4111-8111-111111111111:api_test_secret";
  static final HttpClient HTTP = HttpClient.newHttpClient();

  public static void main(String[] args) throws Exception {
    String cardholderGroup = 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"}
      """);

    String chgId = "chg_22222222-2222-4222-8222-222222222222";
    String merchantId = "mp_33333333-3333-4333-8333-333333333333";
    request("GET", "/api/v2/chg/" + chgId + "/merchants?page=1&pageSize=25", null);
    request("GET", "/api/v2/chg/" + chgId + "/merchant/" + merchantId + "/offers?page=1&pageSize=10", null);

    request("PUT", "/api/v2/chg/" + chgId + "/merchant/" + merchantId + "/activation", """
      {"presentedOfferId":"mpo_44444444-4444-4444-8444-444444444444",
       "source":"issuer_host_app","idempotencyKey":"activation-001"}
      """);

    request("POST", "/api/v1/incoming-auth", transactionPayload("issuer-auth-0001", 5000, merchantId, "approved", null));
    // Expect: qualified_auth webhook with rewardEffects[0].timing == "preview".

    request("POST", "/api/v1/incoming-txn", transactionPayload("issuer-txn-0001", 5000, merchantId, "settled", "issuer-auth-0001"));
    // Expect: qualified_settlement webhook with an earn effect and posted timing.

    String balance = request("GET", "/api/v2/chg/" + chgId + "/merchant/" + merchantId + "/balance", null);
    if (balance.contains("\"canSpendNow\":true")) {
      request("POST", "/api/v1/incoming-txn", transactionPayload("issuer-txn-0002", 1000, merchantId, "settled", null));
      // Expect: qualified_settlement webhook with a spend effect and posted timing.
    }

    request("GET", "/api/v2/chg/" + chgId + "/transaction/txn_66666666-6666-4666-8666-666666666666/reward-detail", null);
    if (cardholderGroup.isBlank()) throw new IllegalStateException("empty response");
  }

  static String request(String method, String path, String body) throws Exception {
    HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(BASE_URL + path))
        .header("Authorization", AUTHORIZATION)
        .header("Content-Type", "application/json");
    HttpRequest request = body == null
        ? builder.method(method, HttpRequest.BodyPublishers.noBody()).build()
        : builder.method(method, HttpRequest.BodyPublishers.ofString(body)).build();
    HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() < 200 || response.statusCode() >= 300) {
      throw new IllegalStateException(method + " " + path + " failed: " + response.statusCode());
    }
    return response.body();
  }

  static String transactionPayload(String id, int amount, String merchantId, String state, String externalAuthId) {
    String authPart = externalAuthId == null ? "" : "\"externalAuthId\":\"" + externalAuthId + "\",";
    return """
      {"id":"%s",%s"amount":%d,"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",
       "merchantId":"%s","state":"%s"}
      """.formatted(id, authPart, amount, merchantId, state);
  }
}
```

```csharp C# HTTP
using System.Net.Http.Json;
using System.Text.Json;

var http = new HttpClient { BaseAddress = new Uri("https://sandbox.percents.com") };
http.DefaultRequestHeaders.Add(
    "Authorization",
    "token tok_11111111-1111-4111-8111-111111111111:api_test_secret");

async Task<JsonElement> RequestAsync(HttpMethod method, string path, object? payload = null)
{
    using var request = new HttpRequestMessage(method, path);
    if (payload is not null) request.Content = JsonContent.Create(payload);

    using var response = await http.SendAsync(request);
    var content = await response.Content.ReadAsStringAsync();
    response.EnsureSuccessStatusCode();
    return string.IsNullOrEmpty(content) ? default : JsonDocument.Parse(content).RootElement.Clone();
}

object TransactionPayload(string id, int amount, string merchantId, string state, string? externalAuthId = null) => new {
    id,
    externalAuthId,
    amount,
    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",
    merchantId,
    state,
};

var cardholderGroup = await RequestAsync(HttpMethod.Post, "/api/v2/chg", new {
    externalId = "issuer-group-001",
    defaultLanguage = "en",
    active = true,
    cardholders = new[] {
        new {
            externalId = "issuer-cardholder-001",
            active = true,
            cards = new[] {
                new { externalId = "issuer-card-001", last4 = "4242", bin = "411111", network = "visa", physical = true, billingZip = "10001", active = true },
            },
        },
    },
    idempotencyKey = "setup-001",
});

var chgId = cardholderGroup.GetProperty("id").GetString()!;
var merchantId = "mp_33333333-3333-4333-8333-333333333333";
await RequestAsync(HttpMethod.Get, $"/api/v2/chg/{chgId}/merchants?page=1&pageSize=25");
var offers = await RequestAsync(HttpMethod.Get, $"/api/v2/chg/{chgId}/merchant/{merchantId}/offers?page=1&pageSize=10");
var offerId = offers.GetProperty("data")[0].GetProperty("id").GetString()!;

await RequestAsync(HttpMethod.Put, $"/api/v2/chg/{chgId}/merchant/{merchantId}/activation", new {
    presentedOfferId = offerId,
    source = "issuer_host_app",
    idempotencyKey = "activation-001",
});

await RequestAsync(HttpMethod.Post, "/api/v1/incoming-auth",
    TransactionPayload("issuer-auth-0001", 5000, merchantId, "approved"));
// Expect: qualified_auth webhook with rewardEffects[0].timing == "preview".

await RequestAsync(HttpMethod.Post, "/api/v1/incoming-txn",
    TransactionPayload("issuer-txn-0001", 5000, merchantId, "settled", "issuer-auth-0001"));
// Expect: qualified_settlement webhook with an earn effect and posted timing.

var balance = await RequestAsync(HttpMethod.Get, $"/api/v2/chg/{chgId}/merchant/{merchantId}/balance");
if (balance.GetProperty("canSpendNow").GetBoolean()) {
    await RequestAsync(HttpMethod.Post, "/api/v1/incoming-txn",
        TransactionPayload("issuer-txn-0002", 1000, merchantId, "settled"));
    // Expect: qualified_settlement webhook with a spend effect and posted timing.
}

await RequestAsync(HttpMethod.Get,
    $"/api/v2/chg/{chgId}/transaction/txn_66666666-6666-4666-8666-666666666666/reward-detail");
```

```ruby Ruby HTTP
require "json"
require "net/http"
require "uri"

BASE_URL = "https://sandbox.percents.com"
AUTHORIZATION = "token tok_11111111-1111-4111-8111-111111111111:api_test_secret"

def request(method, path, body = nil)
  uri = URI("#{BASE_URL}#{path}")
  request = Net::HTTP.const_get(method.capitalize).new(uri)
  request["Authorization"] = AUTHORIZATION
  request["Content-Type"] = "application/json"
  request.body = body.to_json if body

  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
  raise "#{method} #{path} failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
  response.body.empty? ? nil : JSON.parse(response.body)
end

def transaction_payload(values)
  {
    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"
  }.merge(values)
end

cardholder_group = 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"
})

chg_id = cardholder_group["id"]
merchants = request("get", "/api/v2/chg/#{chg_id}/merchants?page=1&pageSize=25")
merchant = merchants["data"].find { |row| row["id"] == "mp_33333333-3333-4333-8333-333333333333" }
offers = request("get", "/api/v2/chg/#{chg_id}/merchant/#{merchant["id"]}/offers?page=1&pageSize=10")

request("put", "/api/v2/chg/#{chg_id}/merchant/#{merchant["id"]}/activation", {
  presentedOfferId: offers["data"].first["id"],
  source: "issuer_host_app",
  idempotencyKey: "activation-001"
})

request("post", "/api/v1/incoming-auth", transaction_payload(
  id: "issuer-auth-0001", amount: 5000, merchantId: merchant["id"], state: "approved"
))
# Expect: qualified_auth webhook with rewardEffects[0].timing == "preview".

request("post", "/api/v1/incoming-txn", transaction_payload(
  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.

balance = request("get", "/api/v2/chg/#{chg_id}/merchant/#{merchant["id"]}/balance")
if balance["canSpendNow"]
  request("post", "/api/v1/incoming-txn", transaction_payload(
    id: "issuer-txn-0002", amount: 1000, merchantId: merchant["id"], state: "settled"
  ))
  # Expect: qualified_settlement webhook with a spend effect and posted timing.
end

request("get", "/api/v2/chg/#{chg_id}/transaction/txn_66666666-6666-4666-8666-666666666666/reward-detail")
```

```rust Rust HTTP
use reqwest::blocking::Client;
use serde_json::{json, Value};

const BASE_URL: &str = "https://sandbox.percents.com";
const AUTHORIZATION: &str = "token tok_11111111-1111-4111-8111-111111111111:api_test_secret";

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let http = Client::new();

    let cardholder_group = request(&http, "POST", "/api/v2/chg", Some(json!({
        "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"
    })))?;

    let chg_id = cardholder_group["id"].as_str().unwrap();
    let merchants = request(&http, "GET", &format!("/api/v2/chg/{chg_id}/merchants?page=1&pageSize=25"), None)?;
    let merchant_id = merchants["data"].as_array().unwrap()
        .iter()
        .find(|row| row["id"] == "mp_33333333-3333-4333-8333-333333333333")
        .unwrap()["id"].as_str().unwrap();
    let offers = request(&http, "GET", &format!("/api/v2/chg/{chg_id}/merchant/{merchant_id}/offers?page=1&pageSize=10"), None)?;
    let offer_id = offers["data"][0]["id"].as_str().unwrap();

    request(&http, "PUT", &format!("/api/v2/chg/{chg_id}/merchant/{merchant_id}/activation"), Some(json!({
        "presentedOfferId": offer_id,
        "source": "issuer_host_app",
        "idempotencyKey": "activation-001"
    })))?;

    request(&http, "POST", "/api/v1/incoming-auth", Some(transaction_payload(json!({
        "id": "issuer-auth-0001",
        "amount": 5000,
        "merchantId": merchant_id,
        "state": "approved"
    }))))?;
    // Expect: qualified_auth webhook with rewardEffects[0].timing == "preview".

    request(&http, "POST", "/api/v1/incoming-txn", Some(transaction_payload(json!({
        "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.

    let balance = request(&http, "GET", &format!("/api/v2/chg/{chg_id}/merchant/{merchant_id}/balance"), None)?;
    if balance["canSpendNow"].as_bool().unwrap_or(false) {
        request(&http, "POST", "/api/v1/incoming-txn", Some(transaction_payload(json!({
            "id": "issuer-txn-0002",
            "amount": 1000,
            "merchantId": merchant_id,
            "state": "settled"
        }))))?;
        // Expect: qualified_settlement webhook with a spend effect and posted timing.
    }

    request(&http, "GET", &format!("/api/v2/chg/{chg_id}/transaction/txn_66666666-6666-4666-8666-666666666666/reward-detail"), None)?;
    Ok(())
}

fn request(client: &Client, method: &str, path: &str, body: Option<Value>) -> Result<Value, Box<dyn std::error::Error>> {
    let url = format!("{BASE_URL}{path}");
    let builder = match method {
        "GET" => client.get(&url),
        "POST" => client.post(&url),
        "PUT" => client.put(&url),
        _ => return Err("unsupported method".into()),
    }
    .header("Authorization", AUTHORIZATION)
    .header("Content-Type", "application/json");

    let response = if let Some(value) = body { builder.json(&value).send()? } else { builder.send()? };
    if !response.status().is_success() {
        return Err(format!("{method} {path} failed: {}", response.status()).into());
    }
    Ok(response.json().unwrap_or_else(|_| json!({})))
}

fn transaction_payload(values: Value) -> Value {
    let mut payload = json!({
        "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"
    });
    payload.as_object_mut().unwrap().extend(values.as_object().unwrap().clone());
    payload
}
```