> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.thepennyinc.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.thepennyinc.com/_mcp/server.

# Simulating a Card Purchase

This walks through simulating a card purchase end to end: creating it, settling it, and confirming the outcome. See [Simulation Scenarios](/simulation/simulation-scenarios) first if the endpoints and event names are new.

All requests go to Penny Simulation (`https://simulate.sandbox.api.thepennyinc.com`).

## Prerequisites

You need an active Sandbox card funded by a Capital Account with enough available balance for the purchase. Simulation does not create them. Create the Capital Account on Penny Banking, fund it with a simulated inbound payment from a linked account (`POST /transaction/payment` with `"settle": true`), and issue the card with [Issuing a Card](/issuing/issuing-a-card). The card must belong to the business you authenticate as.

## 1. Create the purchase

`POST /transaction/card` runs the simulated purchase through Penny's authorization — account and card eligibility, available funds, spend controls, and any configured live decisioning callback — and the simulator confirms the result the way a card network would. A decline is still a successful response that contains a declined transaction.

**`cURL`**

```bash title="cURL"
curl -X POST https://simulate.sandbox.api.thepennyinc.com/transaction/card \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: 3f29c1a2-8b7e-4e51-9a2e-2b6a1c9d7e10" \
  -H "Content-Type: application/json" \
  -d '{
    "card_id": "card_01937f5d-a015-7a55-818b-a015a5518b97",
    "amount": "120.00",
    "currency": "USD",
    "merchant": {
      "merchant_id": "simulated-store-001",
      "name": "Simulator grocery store",
      "category_code": "5411",
      "city": "New York",
      "zip_code": "10001",
      "country": "US",
      "subdivision": "US-NY"
    },
    "acquirer": {"id": "simulated-acquirer", "name": "Simulator Acquiring Bank"}
  }'
```

**`Python`**

```python title="Python"
import requests

SIMULATION = "https://simulate.sandbox.api.thepennyinc.com"
headers = {"Authorization": f"Bearer {access_token}"}

purchase = requests.post(
    f"{SIMULATION}/transaction/card",
    headers={**headers, "Idempotency-Key": "3f29c1a2-8b7e-4e51-9a2e-2b6a1c9d7e10"},
    json={
        "card_id": card_id,
        "amount": "120.00",
        "currency": "USD",
        "merchant": {
            "merchant_id": "simulated-store-001",
            "name": "Simulator grocery store",
            "category_code": "5411",
            "city": "New York",
            "zip_code": "10001",
            "country": "US",
            "subdivision": "US-NY",
        },
        "acquirer": {"id": "simulated-acquirer", "name": "Simulator Acquiring Bank"},
    },
).json()
transaction_id = purchase["transaction_id"]
```

**`JavaScript`**

```javascript title="JavaScript"
const SIMULATION = "https://simulate.sandbox.api.thepennyinc.com";
const headers = {
  Authorization: `Bearer ${accessToken}`,
  "Content-Type": "application/json",
};

const purchase = await fetch(`${SIMULATION}/transaction/card`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": "3f29c1a2-8b7e-4e51-9a2e-2b6a1c9d7e10" },
  body: JSON.stringify({
    card_id: cardId,
    amount: "120.00",
    currency: "USD",
    merchant: {
      merchant_id: "simulated-store-001",
      name: "Simulator grocery store",
      category_code: "5411",
      city: "New York",
      zip_code: "10001",
      country: "US",
      subdivision: "US-NY",
    },
    acquirer: { id: "simulated-acquirer", name: "Simulator Acquiring Bank" },
  }),
}).then((res) => res.json());
const transactionId = purchase.transaction_id;
```

Every following event call targets the response's `transaction_id`. Store it.

> **Info**
>
> Add `"force": true` to skip authorization and record the purchase as settled, modeling an offline or store-and-forward capture. You cannot request a specific decision: without `force`, the outcome is whatever Penny's authorization decides. To test a decline, configure a [spend control](/spend-controls/setting-spend-controls) that the purchase breaks.

## 2. Settle it

**`cURL`**

```bash title="cURL"
curl -X POST https://simulate.sandbox.api.thepennyinc.com/transaction/update \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: 6c1e2a9d-2b6a-4c9d-9a2e-8b7e4e519a2e" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "'"$TRANSACTION_ID"'",
    "event": "transaction.settled",
    "amount": "120.00"
  }'
```

**`Python`**

```python title="Python"
requests.post(
    f"{SIMULATION}/transaction/update",
    headers={**headers, "Idempotency-Key": "6c1e2a9d-2b6a-4c9d-9a2e-8b7e4e519a2e"},
    json={
        "transaction_id": transaction_id,
        "event": "transaction.settled",
        "amount": "120.00",
    },
).json()
```

**`JavaScript`**

```javascript title="JavaScript"
await fetch(`${SIMULATION}/transaction/update`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": "6c1e2a9d-2b6a-4c9d-9a2e-8b7e4e519a2e" },
  body: JSON.stringify({
    transaction_id: transactionId,
    event: "transaction.settled",
    amount: "120.00",
  }),
}).then((res) => res.json());
```

Settlement amounts are deltas, not cumulative totals. To exercise a multi-part settlement, send several `transaction.settled` events, each with its own `Idempotency-Key` and a partial `amount` (for example, `"80.00"` and then `"40.00"`).

## 3. Confirm it

Read the simulated transaction through Penny Banking's transaction endpoint:

**`cURL`**

```bash title="cURL"
curl https://sandbox.api.thepennyinc.com/transactions/$TRANSACTION_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

**`Python`**

```python title="Python"
requests.get(f"https://sandbox.api.thepennyinc.com/transactions/{transaction_id}", headers=headers).json()
```

**`JavaScript`**

```javascript title="JavaScript"
await fetch(`https://sandbox.api.thepennyinc.com/transactions/${transactionId}`, { headers }).then((res) =>
  res.json()
);
```

See [Transactions](/banking/transactions) for its shape and [Reading Transactions & Ledger Entries](/banking/reading-transactions) for how to find and reconcile it with other Sandbox transaction activity.

## Next steps

* [Simulation Scenarios](/simulation/simulation-scenarios) — the full set of transaction kinds and events, including funding payments
* [Retries & Idempotency](/get-started/idempotency) — why each event gets its own key, and how to retry one safely
* [Subscribing to Webhooks](/platform/subscribing-to-webhooks) — get notified as a simulated transaction moves through its lifecycle instead of polling for it