> ## Documentation Index
> Fetch the complete documentation index at: https://docs.paygentic.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build in the sandbox

> Build a complete pricing setup with customers, usage and months of invoices in the sandbox, in the order the API needs.

This page builds a complete setup in the sandbox, from an empty account to two months of invoices. Follow the steps in order: each one uses IDs from the steps before it.

The example plan has a monthly platform fee of \$99, billed in advance, and API calls billed in arrears. Each month includes 10,000 API calls. Calls above that are billed on a volume ladder.

## Before you begin

* A sandbox API key (`sk_test_...`) that belongs to an organization owner or admin. The reset in step 1 needs that role. See [API keys](https://docs.paygentic.io/api-keys.md).
* Your merchant ID (`org_...`).
* `curl` and [`jq`](https://jqlang.org/).

Set them once for your shell:

```bash Set your key and IDs theme={null}
export PAYGENTIC_API="https://api.sandbox.paygentic.io"
export PAYGENTIC_KEY="sk_test_YOUR_API_KEY"
export MERCHANT_ID="org_YOUR_MERCHANT_ID"
```

Each step stores the IDs it creates in shell variables, such as `PRODUCT_ID`, for the steps after it.

> [!WARNING]
>
> Step 1 deletes every customer, subscription, invoice and usage event in the sandbox account. Use an account that holds nothing you need.

## 1. Reset the account

Start from an empty account, so a second run builds the same data as the first:

```bash Reset the sandbox account theme={null}
curl -sS -X POST "$PAYGENTIC_API/v0/merchants/$MERCHANT_ID/reset" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "merchant", "keepCatalog": false }'
```

`204 No Content` means every delete is done or scheduled. A `502 Bad Gateway` means the reset is incomplete: call it again with the same body.

Send no usage events while the reset runs: an event that arrives during the reset can survive it. Usage events finish deleting in the background, and the API doesn't report when that's done. The steps below create a new customer, so events left from before the reset don't count for it. If you set an `externalId` on your customers, wait a few minutes before you create a customer with an `externalId` you used before the reset.

To keep a catalog you already built, send `"keepCatalog": true` and skip step 2. See [Resetting sandbox data](https://docs.paygentic.io/getting-started/environments.md#resetting-sandbox-data).

## 2. Create the catalog

Create the catalog in dependency order: product, billable metric, feature, fee, prices, then the plan.

```bash Product theme={null}
PRODUCT_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/products" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"merchantId\": \"$MERCHANT_ID\",
    \"name\": \"Inference API\",
    \"description\": \"Hosted model inference\"
  }" | jq -r '.id')
```

The billable metric adds up the `calls` value of each `api.call` event, so one event can report a batch of calls. Aggregation values are uppercase.

```bash Billable metric theme={null}
METRIC_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/billableMetrics" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"merchantId\": \"$MERCHANT_ID\",
    \"productId\": \"$PRODUCT_ID\",
    \"name\": \"API calls\",
    \"description\": \"Inference API calls\",
    \"unit\": \"calls\",
    \"aggregation\": \"SUM\",
    \"eventType\": \"api.call\",
    \"valueProperty\": \"$.calls\"
  }" | jq -r '.id')
```

The 10,000 included calls are an allowance, so they need a metered feature:

```bash Metered feature theme={null}
FEATURE_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/features" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"merchantId\": \"$MERCHANT_ID\",
    \"productId\": \"$PRODUCT_ID\",
    \"name\": \"API calls\",
    \"key\": \"api-calls\",
    \"type\": \"metered\"
  }" | jq -r '.id')
```

The usage price sets the billable metric with `billableMetricId`, and the allowance with the feature's entitlement template. `isSoftLimit: true` keeps the customer's access past 10,000 calls and bills the calls above it. Total usage picks the band on the ladder, and only the calls above the allowance are billed. See [Volume (tiered ladder)](https://docs.paygentic.io/platform/pricing/prices.md#volume-tiered-ladder).

```bash Usage price theme={null}
USAGE_PRICE_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/prices" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"billableMetricId\": \"$METRIC_ID\",
    \"model\": \"volume\",
    \"paymentTerm\": \"in_arrears\",
    \"invoiceDisplayName\": \"API calls\",
    \"properties\": {
      \"tiers\": [
        { \"upTo\": 50000, \"unitPrice\": \"0.002\" },
        { \"upTo\": null, \"unitPrice\": \"0.001\" }
      ]
    },
    \"feature\": {
      \"featureId\": \"$FEATURE_ID\",
      \"entitlementTemplate\": {
        \"usagePeriod\": { \"interval\": \"P1M\" },
        \"issueAfterReset\": 10000,
        \"isSoftLimit\": true
      }
    }
  }" | jq -r '.id')
```

The platform fee is a fee with its own price:

```bash Fee and fee price theme={null}
FEE_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/fees" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"merchantId\": \"$MERCHANT_ID\",
    \"productId\": \"$PRODUCT_ID\",
    \"name\": \"Platform fee\",
    \"description\": \"Monthly platform access\"
  }" | jq -r '.id')

FEE_PRICE_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/prices" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"feeId\": \"$FEE_ID\",
    \"model\": \"standard\",
    \"paymentTerm\": \"in_advance\",
    \"billingCadence\": \"P1M\",
    \"invoiceDisplayName\": \"Platform fee\",
    \"properties\": { \"unitPrice\": \"99\" }
  }" | jq -r '.id')
```

The plan bundles both prices. Its currency must match your account's currency.

```bash Plan theme={null}
PLAN_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/plans" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"merchantId\": \"$MERCHANT_ID\",
    \"productId\": \"$PRODUCT_ID\",
    \"name\": \"Growth\",
    \"description\": \"Platform fee plus API calls\",
    \"invoiceDisplayName\": \"Growth plan\",
    \"billingCadence\": \"P1M\",
    \"currency\": \"USD\",
    \"prices\": [\"$FEE_PRICE_ID\", \"$USAGE_PRICE_ID\"]
  }" | jq -r '.id')
```

## 3. Create a customer

```bash Customer theme={null}
CUSTOMER_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/customers" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"merchantId\": \"$MERCHANT_ID\",
    \"consumer\": {
      \"name\": \"Acme Robotics\",
      \"email\": \"billing@acme-robotics.example\",
      \"address\": {
        \"line1\": \"1 Market Street\",
        \"city\": \"San Francisco\",
        \"state\": \"CA\",
        \"country\": \"US\",
        \"zipCode\": \"94105\"
      }
    }
  }" | jq -r '.customerId')
```

The postcode field is `zipCode`. An address field with another name is dropped without an error.

## 4. Create a test clock

A test clock lets a subscription start in the past and bill its periods as you move the clock forward. Start it on the first day of the month two months ago. The examples use 1 September 2026.

```bash Test clock theme={null}
CLOCK_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/testClocks" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Two months of history", "currentTime": "2026-09-01T00:00:00Z" }' \
  | jq -r '.id')
```

A clock moves forward only, at most 1 year per call. See [Simulating time](https://docs.paygentic.io/getting-started/environments.md#simulating-time).

## 5. Subscribe and activate

Create the subscription on the clock, starting at the clock's time:

```bash Subscription theme={null}
SUBSCRIPTION_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/subscriptions" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"customerId\": \"$CUSTOMER_ID\",
    \"planId\": \"$PLAN_ID\",
    \"name\": \"Growth\",
    \"startedAt\": \"2026-09-01T00:00:00Z\",
    \"testClockId\": \"$CLOCK_ID\"
  }" | jq -r '.id')
```

The plan has a fee billed in advance, so Paygentic issues Invoice 0 for the first month's fee, and the subscription is `pending_payment`. In the sandbox, activate it by recording Invoice 0 as paid outside Paygentic:

```bash Record Invoice 0 as paid theme={null}
INVOICE_0_ID=$(curl -sS "$PAYGENTIC_API/v2/invoices?subscriptionId=$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" | jq -r '.data[] | select(.sequenceNumber == 0) | .id')

curl -sS -X POST "$PAYGENTIC_API/v2/invoices/$INVOICE_0_ID/payment" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "method": "out_of_band", "reason": "Sandbox setup" }'
```

Paygentic activates the subscription in the background. Check that `status` is `active` before you send usage:

```bash Check the subscription theme={null}
curl -sS "$PAYGENTIC_API/v0/subscriptions/$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" | jq -r '.status'
```

See [Record a payment collected outside Paygentic](https://docs.paygentic.io/platform/billing/invoices.md#record-a-payment-collected-outside-paygentic).

## 6. Send usage and advance the clock

For each month, send usage dated at the clock's current time, then advance the clock one month. Usage counts once the clock moves past it.

This loop sends 12 events of 1,000 calls each, 12,000 calls in all. That is 2,000 calls above the allowance.

```bash Send one month of usage theme={null}
CLOCK_TIME=$(curl -sS "$PAYGENTIC_API/v0/testClocks/$CLOCK_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" | jq -r '.currentTime')

for i in $(seq 1 12); do
  curl -sS -o /dev/null -w "%{http_code}\n" -X POST "$PAYGENTIC_API/v0/events" \
    -H "Authorization: Bearer $PAYGENTIC_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"type\": \"api.call\",
      \"source\": \"sandbox-setup\",
      \"subject\": \"$CUSTOMER_ID\",
      \"timestamp\": \"$CLOCK_TIME\",
      \"idempotencyKey\": \"setup-$CLOCK_TIME-$i\",
      \"data\": { \"calls\": 1000 }
    }"
done
```

Each call should print `202`. The `idempotencyKey` is built from the clock's time and the loop counter, so if you run the loop twice for the same month, the second run doesn't count the calls again.

Check the meter before you advance the clock. Usage events are processed in the background, and the meter shows them once they are counted, usually within a minute. For the first month, it should show `12000`:

```bash Check the meter theme={null}
curl -sS "$PAYGENTIC_API/v0/billableMetrics/$METRIC_ID/meter?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z&subject=$CUSTOMER_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" | jq '.totalValue'
```

Then advance the clock one month:

```bash Advance the clock theme={null}
curl -sS -X PATCH "$PAYGENTIC_API/v0/testClocks/$CLOCK_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "advanceBy": "P1M" }'
```

Before the next month, wait until the month's invoice exists. Paygentic creates it in the background, usually within a minute, with the status `CLOSING`. If you advance again before then, the two months share one invoice.

```bash Wait for the month's invoice theme={null}
curl -sS "$PAYGENTIC_API/v2/invoices?subscriptionId=$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  | jq '.data[] | select(.sequenceNumber == 1) | {sequenceNumber, status}'
```

Pick the invoice by its `sequenceNumber`, because the list isn't guaranteed to be in any order: `1` for the first month, `2` for the second. Repeat this step for the second month. To see the second band of the ladder, send 60 events instead of 12: 60,000 calls select the `0.001` band, and the 50,000 calls above the allowance are billed at that rate. In the meter check, use `2026-10-01` to `2026-11-01`.

After the last month, finish the last invoice. A `CLOSING` invoice waits 12 hours of clock time for late usage, until its `gracePeriodEnd`, before Paygentic issues it. The earlier invoices pass that point when you advance the clock for the next month. For the last one, read its `gracePeriodEnd` and advance the clock past it:

```bash Finish the last invoice theme={null}
curl -sS "$PAYGENTIC_API/v2/invoices?subscriptionId=$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  | jq '.data[] | select(.sequenceNumber == 2) | {sequenceNumber, status, gracePeriodEnd}'

curl -sS -X PATCH "$PAYGENTIC_API/v0/testClocks/$CLOCK_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "advanceBy": "P1D" }'
```

## 7. Read the invoices

```bash List the invoices theme={null}
curl -sS "$PAYGENTIC_API/v2/invoices?subscriptionId=$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  | jq '.data[] | {sequenceNumber, status, periodStart, periodEnd, grandTotal}'
```

Invoices follow the clock in the background, so an invoice can take a few minutes to appear or change status after an advance.

Each monthly invoice bills the usage of the month that ended and the platform fee of the month that started, so its `periodStart` and `periodEnd` span two months. With the usage in step 6, you get these invoices:

| Invoice | Lines | `grandTotal` |
| - | - | - |
| 0 | September platform fee | `99.00` |
| 1 | October platform fee, and 2,000 September calls above the allowance at `0.002` | `103.00` |
| 2 | November platform fee, and 50,000 October calls above the allowance at `0.001` | `149.00` |

To see an invoice's lines, call `GET /v2/invoices/lineItems?invoiceId={id}`. To record an invoice as paid, use the same call as for Invoice 0. See [Invoices](https://docs.paygentic.io/platform/billing/invoices.md).

## 8. Track a cost

A cost records what serving the customer costs you. It reads the same usage events as a billable metric with the same `eventType`, including events you sent before you created the cost, so you can add it at any point. This one costs \$0.0004 per call.

```bash Cost theme={null}
COST_ID=$(curl -sS -X POST "$PAYGENTIC_API/v0/costs" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"merchantId\": \"$MERCHANT_ID\",
    \"productId\": \"$PRODUCT_ID\",
    \"type\": \"metered\",
    \"name\": \"GPU time\",
    \"unitCost\": \"0.0004\",
    \"currency\": \"USD\",
    \"aggregation\": \"SUM\",
    \"eventType\": \"api.call\",
    \"valueProperty\": \"$.calls\"
  }" | jq -r '.id')
```

Read it for the two months. It should show a `totalQuantity` of `72000` and a `totalCost` of `28.8`:

```bash Cost summary theme={null}
curl -sS "$PAYGENTIC_API/v0/costs/$COST_ID/summary?from=2026-09-01T00:00:00Z&to=2026-11-01T00:00:00Z&subject=$CUSTOMER_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" | jq '{totalQuantity, totalCost}'
```

A cost's aggregation works like a billable metric's. `COUNT` would count the 72 events, not the 72,000 calls.

* Use `SUM` or `COUNT` for a cost you want to total. A summary without `subject` is computed over every customer's events together, so for other aggregations it isn't a sum of each customer's cost.
* Revenue and cost are not matched by billing period. Compare them over a range of months, not a single invoice.

See [Cost tracking](https://docs.paygentic.io/platform/metering/cost-tracking.md).

## 9. Check the result

The meter shows what Paygentic counted for a customer over any time range:

```bash Usage counted for the customer theme={null}
curl -sS "$PAYGENTIC_API/v0/billableMetrics/$METRIC_ID/meter?from=2026-09-01T00:00:00Z&to=2026-11-01T00:00:00Z&subject=$CUSTOMER_ID" \
  -H "Authorization: Bearer $PAYGENTIC_KEY" | jq '.totalValue'
```

For the two months, it should show `72000`. Compare it with the usage lines on the invoices.

To see the allowance, find the customer's entitlement with `GET /v1/entitlements?customerId={id}`, then read its balance with `GET /v1/entitlements/{entitlementId}`. With the clock in November and no November usage, `balance` is `10000`. Read a balance only after the meter shows all the usage for the period.

## Make it re-runnable

A setup you can run again is easier to fix than one you repair by hand. Put the steps in one script:

* **Start each run with a reset**, as in step 1. If you keep the catalog instead, give each resource a fixed name and look for it before you create it.
* **List with `limit=100`.** List endpoints return 10 items by default, so a search without `limit` can miss a resource that exists and create a second one.
* **Save the IDs to a file** as you create them, so a later run, or another tool, can read them.
* **Follow the dependency order** of this page.
* **Stop at the first error.** A failed create leaves an empty ID, and every later call that uses it fails with a less useful message.

The script below runs steps 1 to 9 in that order and finishes with the results to check. Set `PAYGENTIC_API`, `PAYGENTIC_KEY` and `MERCHANT_ID` first, and change `START` and `MONTHS` to your own dates. It takes about five minutes, most of it spent waiting for the meter and the invoices.

```bash setup-sandbox.sh theme={null}
#!/usr/bin/env bash
# Rebuilds the "Build in the sandbox" setup from an empty account.
# Needs PAYGENTIC_API, PAYGENTIC_KEY and MERCHANT_ID. Writes the IDs to sandbox-ids.env.
set -euo pipefail

START="2026-09-01T00:00:00Z"   # first day of the month two months ago
MONTHS=("2026-09-01T00:00:00Z" "2026-10-01T00:00:00Z" "2026-11-01T00:00:00Z")
CALLS_PER_MONTH=(12 60)        # events of 1,000 calls each
IDS_FILE="sandbox-ids.env"

api() {
  local method=$1 path=$2 body=${3:-}
  if [ -n "$body" ]; then
    curl -sS --fail-with-body -X "$method" "$PAYGENTIC_API$path" \
      -H "Authorization: Bearer $PAYGENTIC_KEY" \
      -H "Content-Type: application/json" \
      -d "$body"
  else
    curl -sS --fail-with-body -X "$method" "$PAYGENTIC_API$path" \
      -H "Authorization: Bearer $PAYGENTIC_KEY"
  fi
}

save() { echo "$1=$2" >> "$IDS_FILE"; echo "$1=$2"; }

subscription_status() { api GET "/v0/subscriptions/$SUBSCRIPTION_ID" | jq -r '.status'; }
meter_total() { api GET "/v0/billableMetrics/$METRIC_ID/meter?from=$1&to=$2&subject=$CUSTOMER_ID" | jq -r '.totalValue'; }
invoice() { api GET "/v2/invoices?subscriptionId=$SUBSCRIPTION_ID" | jq -r ".data[] | select(.sequenceNumber == $1) | .$2"; }

# Wait until a command prints the expected value, checking every 5 seconds for up to 10 minutes.
wait_for() {
  local expected=$1; shift
  for _ in $(seq 1 120); do
    [ "$("$@")" = "$expected" ] && return 0
    sleep 5
  done
  echo "Timed out waiting for $expected" >&2
  return 1
}

# 1. Reset
api POST "/v0/merchants/$MERCHANT_ID/reset" '{ "scope": "merchant", "keepCatalog": false }'
: > "$IDS_FILE"
sleep 60

# 2. Catalog
PRODUCT_ID=$(api POST /v0/products "{
  \"merchantId\": \"$MERCHANT_ID\", \"name\": \"Inference API\",
  \"description\": \"Hosted model inference\"
}" | jq -er '.id'); save PRODUCT_ID "$PRODUCT_ID"

METRIC_ID=$(api POST /v0/billableMetrics "{
  \"merchantId\": \"$MERCHANT_ID\", \"productId\": \"$PRODUCT_ID\",
  \"name\": \"API calls\", \"description\": \"Inference API calls\", \"unit\": \"calls\",
  \"aggregation\": \"SUM\", \"eventType\": \"api.call\", \"valueProperty\": \"\$.calls\"
}" | jq -er '.id'); save METRIC_ID "$METRIC_ID"

FEATURE_ID=$(api POST /v0/features "{
  \"merchantId\": \"$MERCHANT_ID\", \"productId\": \"$PRODUCT_ID\",
  \"name\": \"API calls\", \"key\": \"api-calls\", \"type\": \"metered\"
}" | jq -er '.id'); save FEATURE_ID "$FEATURE_ID"

USAGE_PRICE_ID=$(api POST /v0/prices "{
  \"billableMetricId\": \"$METRIC_ID\", \"model\": \"volume\", \"paymentTerm\": \"in_arrears\",
  \"invoiceDisplayName\": \"API calls\",
  \"properties\": { \"tiers\": [
    { \"upTo\": 50000, \"unitPrice\": \"0.002\" },
    { \"upTo\": null, \"unitPrice\": \"0.001\" } ] },
  \"feature\": { \"featureId\": \"$FEATURE_ID\", \"entitlementTemplate\": {
    \"usagePeriod\": { \"interval\": \"P1M\" }, \"issueAfterReset\": 10000, \"isSoftLimit\": true } }
}" | jq -er '.id'); save USAGE_PRICE_ID "$USAGE_PRICE_ID"

FEE_ID=$(api POST /v0/fees "{
  \"merchantId\": \"$MERCHANT_ID\", \"productId\": \"$PRODUCT_ID\",
  \"name\": \"Platform fee\", \"description\": \"Monthly platform access\"
}" | jq -er '.id'); save FEE_ID "$FEE_ID"

FEE_PRICE_ID=$(api POST /v0/prices "{
  \"feeId\": \"$FEE_ID\", \"model\": \"standard\", \"paymentTerm\": \"in_advance\",
  \"billingCadence\": \"P1M\", \"invoiceDisplayName\": \"Platform fee\",
  \"properties\": { \"unitPrice\": \"99\" }
}" | jq -er '.id'); save FEE_PRICE_ID "$FEE_PRICE_ID"

PLAN_ID=$(api POST /v0/plans "{
  \"merchantId\": \"$MERCHANT_ID\", \"productId\": \"$PRODUCT_ID\",
  \"name\": \"Growth\", \"description\": \"Platform fee plus API calls\",
  \"invoiceDisplayName\": \"Growth plan\", \"billingCadence\": \"P1M\", \"currency\": \"USD\",
  \"prices\": [\"$FEE_PRICE_ID\", \"$USAGE_PRICE_ID\"]
}" | jq -er '.id'); save PLAN_ID "$PLAN_ID"

COST_ID=$(api POST /v0/costs "{
  \"merchantId\": \"$MERCHANT_ID\", \"productId\": \"$PRODUCT_ID\", \"type\": \"metered\",
  \"name\": \"GPU time\", \"unitCost\": \"0.0004\", \"currency\": \"USD\",
  \"aggregation\": \"SUM\", \"eventType\": \"api.call\", \"valueProperty\": \"\$.calls\"
}" | jq -er '.id'); save COST_ID "$COST_ID"

# 3. Customer
CUSTOMER_ID=$(api POST /v0/customers "{
  \"merchantId\": \"$MERCHANT_ID\",
  \"consumer\": { \"name\": \"Acme Robotics\", \"email\": \"billing@acme-robotics.example\",
    \"address\": { \"line1\": \"1 Market Street\", \"city\": \"San Francisco\",
      \"state\": \"CA\", \"country\": \"US\", \"zipCode\": \"94105\" } }
}" | jq -er '.customerId'); save CUSTOMER_ID "$CUSTOMER_ID"

# 4. Test clock
CLOCK_ID=$(api POST /v0/testClocks "{ \"name\": \"Two months of history\", \"currentTime\": \"$START\" }" \
  | jq -er '.id'); save CLOCK_ID "$CLOCK_ID"

# 5. Subscription, activated by recording Invoice 0 as paid
SUBSCRIPTION_ID=$(api POST /v0/subscriptions "{
  \"customerId\": \"$CUSTOMER_ID\", \"planId\": \"$PLAN_ID\", \"name\": \"Growth\",
  \"startedAt\": \"$START\", \"testClockId\": \"$CLOCK_ID\"
}" | jq -er '.id'); save SUBSCRIPTION_ID "$SUBSCRIPTION_ID"

INVOICE_0_ID=$(api GET "/v2/invoices?subscriptionId=$SUBSCRIPTION_ID" | jq -er '.data[] | select(.sequenceNumber == 0) | .id')
api POST "/v2/invoices/$INVOICE_0_ID/payment" '{ "method": "out_of_band", "reason": "Sandbox setup" }' > /dev/null
wait_for active subscription_status

# 6. Usage, one month at a time
for m in "${!CALLS_PER_MONTH[@]}"; do
  from=${MONTHS[$m]} to=${MONTHS[$((m + 1))]} events=${CALLS_PER_MONTH[$m]}
  for i in $(seq 1 "$events"); do
    api POST /v0/events "{
      \"type\": \"api.call\", \"source\": \"sandbox-setup\", \"subject\": \"$CUSTOMER_ID\",
      \"timestamp\": \"$from\", \"idempotencyKey\": \"setup-$from-$i\", \"data\": { \"calls\": 1000 }
    }" > /dev/null
  done
  wait_for "$((events * 1000))" meter_total "$from" "$to"
  api PATCH "/v0/testClocks/$CLOCK_ID" '{ "advanceBy": "P1M" }' > /dev/null
  # Wait for the month's invoice before the next advance, or two months share one invoice
  wait_for CLOSING invoice "$((m + 1))" status
done

# Finish the last invoice: wait for it to close, then advance past its grace period
LAST=${#CALLS_PER_MONTH[@]}
api PATCH "/v0/testClocks/$CLOCK_ID" "{ \"currentTime\": \"$(invoice "$LAST" gracePeriodEnd)\" }" > /dev/null
api PATCH "/v0/testClocks/$CLOCK_ID" '{ "advanceBy": "PT1H" }' > /dev/null
wait_for ISSUED invoice "$LAST" status

# 7 to 9. Results
api GET "/v2/invoices?subscriptionId=$SUBSCRIPTION_ID" \
  | jq -c '.data[] | {sequenceNumber, status, grandTotal}'
api GET "/v0/billableMetrics/$METRIC_ID/meter?from=${MONTHS[0]}&to=${MONTHS[$LAST]}&subject=$CUSTOMER_ID" \
  | jq -c '{meter: .totalValue}'
api GET "/v0/costs/$COST_ID/summary?from=${MONTHS[0]}&to=${MONTHS[$LAST]}&subject=$CUSTOMER_ID" \
  | jq -c '{totalQuantity, totalCost}'
```

It ends by printing the three invoices (`99.00`, `103.00` and `149.00`), the meter total (`72000`) and the cost (`72000` calls, `28.8`).

## Cautions

* **Usage dated before the allowance starts never counts.** The allowance measures usage from the subscription's start or from the moment you created it, whichever is later, and on a test clock that moment is the clock's time. Send usage dated at the clock's time. See [Usage dated in the past](https://docs.paygentic.io/platform/billing/metered-entitlements.md#usage-dated-in-the-past).
* **Unknown query parameters are ignored.** A misspelt filter returns an unfiltered list with `200 OK`, not an error. Check a filtered list's results before you rely on them.
* **Error messages say what was refused, not always why.** When a call fails, check the request against this page and the endpoint's page in the API reference.

## Next steps

* [Simulating time](https://docs.paygentic.io/getting-started/environments.md#simulating-time)
* [Metered entitlements](https://docs.paygentic.io/platform/billing/metered-entitlements.md)
* [Customer lifecycle](https://docs.paygentic.io/platform/billing/customer-lifecycle.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.