Usage
Report how much of a metered feature a customer used, one total per customer, feature and day. PayKit rates each usage period after it closes, using the metered price on the plan version, and charges any overage to the customer's default card. A write sets the day's total, so a re-sent day replaces the earlier value.
/v1/clients/{clientId}/projects/{projectId}/customers/{externalRef}/usage/{featureCode}/days/{day}Set one day's usage
Set the total used on one day. This replaces the day's value, it does not add to it. Requires an Idempotency-Key header.
Path parameters
| clientId | string | Your PayKit client id. |
| projectId | string | The project. |
| externalRef | string | Your id for the user. |
| featureCode | string | Code of a feature whose kind is metered. |
| day | string | yyyy-MM-dd: a calendar date in the metered price's time zone, not UTC. |
Request body
| quantity * | string | integer | The total for the day, from 0 to 9007199254740991 (2^53 - 1), as a decimal string or a JSON integer. |
Errors: 400 invalid_body (bad quantity, including one above 2^53 - 1), 400 invalid_request (bad day), 400 invalid_customer_ref (empty externalRef), 404 customer_not_found, 404 feature_not_found, 409 customer_erased, 422 feature_not_metered, 422 day_in_future.
409 usage_period_closed: the day lies in a period that is already rated, or rating claimed it while the write landed, in which case your value may or may not be in the rated total. error.details.period is { usagePeriodId, subscriptionId, periodStart, periodEnd, firstDay, lastDay, status }. Reconcile against the usage read, by hand.
A customer with no subscription that meters the feature is accepted: the day is stored and billed only if a metered subscription later counts it.
curl -X PUT "https://api.paykit.co.za/v1/clients/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/projects/f6e5d4c3b2a1908172635445362718a9/customers/user_123/usage/bandwidth_bytes/days/2026-10-11" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"quantity":"39728447488"}'{
"usageDay": {
"customerId": "c_4d5e…",
"externalRef": "user_123",
"featureCode": "bandwidth_bytes",
"day": "2026-10-11",
"quantity": "39728447488",
"previousQuantity": "39000000000",
"changed": true,
"updatedAt": "2026-10-12T01:00:03.000Z"
}
}/v1/clients/{clientId}/projects/{projectId}/usage/batchSet many days
Set up to 500 day totals in one call. Each record is applied on its own, exactly as the single write, and results come back in request order. Requires an Idempotency-Key header.
Path parameters
| clientId | string | Your PayKit client id. |
| projectId | string | The project. |
Request body
| records * | array | 1 to 500 records of { externalRef, featureCode, day, quantity }. |
An empty batch, more than 500 records or a malformed body is 400 invalid_body and nothing is written.
invalid_record: the record failed validation (bad quantity, including one above 2^53 - 1, bad day, empty externalRef). duplicate_in_batch: the same externalRef, featureCode and day appeared earlier in the batch, so every occurrence after the first fails.
curl -X POST "https://api.paykit.co.za/v1/clients/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/projects/f6e5d4c3b2a1908172635445362718a9/usage/batch" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"records":[{"externalRef":"user_123","featureCode":"bandwidth_bytes","day":"2026-10-11","quantity":"39728447488"}]}'{
"results": [
{
"index": 0,
"status": "ok",
"usageDay": {
"customerId": "c_4d5e…",
"externalRef": "user_123",
"featureCode": "bandwidth_bytes",
"day": "2026-10-11",
"quantity": "39728447488",
"previousQuantity": null,
"changed": true,
"updatedAt": "2026-10-12T01:00:03.000Z"
}
},
{
"index": 1,
"status": "error",
"error": {
"code": "usage_period_closed",
"message": "That period has already been rated",
"details": {
"period": {
"usagePeriodId": "up_7c8d…",
"status": "paid"
}
}
}
}
],
"counts": {
"ok": 1,
"failed": 1,
"changed": 1
}
}/v1/clients/{clientId}/projects/{projectId}/customers/{externalRef}/usageRead usage
The stored days for one customer and feature in a range, plus every usage period that overlaps it with its status and, once rated, its billing figures.
Path parameters
| clientId | string | Your PayKit client id. |
| projectId | string | The project. |
| externalRef | string | Your id for the user. |
Query parameters
| featureCode | string | The metered feature. Required. |
| from | string | First day, inclusive (yyyy-MM-dd). Required. |
| to | string | Last day, inclusive (yyyy-MM-dd). Required, at most 366 days after from. |
A missing or invalid query parameter is 400 invalid_request. 404 customer_not_found and 404 feature_not_found also apply.
A day counts only when the subscription was active, past_due or grace at that day's first instant in the price's time zone. trialing, pending_action, incomplete, suspended, paused, cancelled and expired days never count. A day counts for at most one subscription of a customer; when two of them could count it, which one does is not defined.
curl -X GET "https://api.paykit.co.za/v1/clients/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/projects/f6e5d4c3b2a1908172635445362718a9/customers/user_123/usage" \ -H "Authorization: Bearer YOUR_API_KEY"
{
"customerId": "c_4d5e…",
"externalRef": "user_123",
"featureCode": "bandwidth_bytes",
"days": [
{
"day": "2026-09-01",
"quantity": "1288490188",
"updatedAt": "2026-09-02T01:00:02.000Z"
}
],
"periods": [
{
"usagePeriodId": "up_7c8d…",
"subscriptionId": "0f1e2d3c4b5a6978869504132231ffee",
"usagePeriod": "calendar_month",
"timeZone": "Africa/Johannesburg",
"periodStart": "2026-08-31T22:00:00.000Z",
"periodEnd": "2026-09-30T22:00:00.000Z",
"firstDay": "2026-09-01",
"lastDay": "2026-09-30",
"status": "paid",
"quantity": "39728447488",
"includedQuantity": "26843545600",
"unitSize": "1073741824",
"rounding": "up",
"unitAmountMinor": "600",
"maxChargeMinor": "104700",
"billableUnits": "12",
"amountMinor": "7200",
"capped": false,
"currency": "ZAR",
"ratesAt": "2026-10-03T22:00:00.000Z",
"ratedAt": "2026-10-03T22:07:12.000Z",
"attempts": 1,
"nextAttemptAt": null,
"failureCode": null,
"invoiceId": "99887766554433221100ffeeddccbbaa",
"transactionId": "aa11bb22cc33dd44ee55ff6677889900"
}
]
}/v1/clients/{clientId}/projects/{projectId}/usage-periods/{usagePeriodId}/retryRetry an uncollectible usage charge
One immediate attempt on a usage period whose charge ran out of retries, against the customer's current default card. Reinstating a subscription does not collect unpaid usage, so retry this first. Requires an Idempotency-Key header.
Path parameters
| clientId | string | Your PayKit client id. |
| projectId | string | The project. |
| usagePeriodId | string | A usage period with status uncollectible. |
402 charge_failed: declined or no usable card. error.details.failureCode says which, and the period stays uncollectible.
404 usage_period_not_found. 409 usage_period_not_retryable: its status is not uncollectible.
502 gateway_error, two cases. The gateway could not be reached after the charge was sent: the period is left charging and resolved by verifying its reference, never by charging again. Or the attempt was refused on PayKit's side or failed before it was sent: it is handed back, not counted, and the period stays uncollectible, so retry later.
A paid retry recovers a subscription this debt escalated from active: once no escalated usage period is unpaid, and it is still past_due or grace and has not been active since, it returns to active with its original renewal date (subscription.recovered). A subscription already suspended stays suspended until you reinstate it.
curl -X POST "https://api.paykit.co.za/v1/clients/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/projects/f6e5d4c3b2a1908172635445362718a9/usage-periods/up_7c8d%E2%80%A6/retry" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)"
{
"usagePeriod": {
"usagePeriodId": "up_7c8d…",
"status": "paid",
"amountMinor": "7200",
"currency": "ZAR",
"invoiceId": "99887766554433221100ffeeddccbbaa",
"transactionId": "aa11bb22cc33dd44ee55ff6677889900"
}
}