# API reference

> Every Soca REST endpoint: invoices, plans, payments, subscriptions, events, usage meters and metering, prepaid credits, and entitlements, with parameters, requests, and responses.

Part of the Soca documentation (https://soca.finance/docs). Human version: https://soca.finance/docs/developers/api-reference
The Soca API is a versioned REST surface under `/v1`. Base URL: `https://soca.to`. Authenticate every request with `Authorization: Bearer sk_test_...` or `sk_live_...`. See [Authentication](/docs/developers/authentication) for keys, mode scoping, and rate limits.

- **Amounts are USDC base units.** USDC on Solana (base units, 6 decimals: $9.00 = 9000000). Send them as an integer or a numeric string; responses return them as strings.
- **Field names are snake_case** in both requests and responses. Responses carry a `wc-version` header (currently `2026-07-01`).
- **List endpoints share one envelope:** `{ "object": "list", "data": [...], "has_more": true, "next_cursor": "2026-07-17T12:00:00.000Z" }`. Page with `?limit=` (1–100, default 25) and `?before=` (the `next_cursor` from the previous page). A pagination recipe is in [Errors and testing](/docs/developers/errors-and-testing).
- **Errors share one envelope:** `{ "error": { "code": "...", "message": "..." } }`. The full code table is in [Errors and testing](/docs/developers/errors-and-testing).
- **Creation settles to your on-file wallet.** The destination wallet is never accepted over the API. Changing it requires a fresh signature in the dashboard.

> Prefer typed calls? Every endpoint here has a 1:1 method in the official TypeScript client. See [The Soca SDK](/docs/developers/sdk).

## Payments objects

### POST /v1/invoices

Create a one-time invoice. Returns a hosted, non-custodial `checkout_url` to send the customer; no money moves until they pay, then an `invoice.paid` webhook fires.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `amount` | integer or string | Yes | USDC base units: `2500000` = 2.50 USDC. Must be a positive integer. |
| `description` | string | Yes | Shown on the hosted checkout page. 1–500 characters. |
| `memo` | string | No | Free-form note, up to 200 characters. |
| `invoice_number` | string | No | Your own reference, up to 100 characters. |
| `recipient` | string | No | Who it is billed to: a label or email for your records, up to 200 characters. |
| `due_at` | string | No | ISO-8601 due date. |

Request:

```
curl -X POST https://soca.to/v1/invoices \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"amount":"2500000","description":"Acme Pro, March","invoice_number":"INV-1041"}'
```

Response:

```
// 201 Created
{
  "object": "invoice",
  "id": "inv_...",
  "amount": "2500000",
  "status": "open",
  "checkout_url": "https://soca.to/invoice/inv_..."
}
```

### GET /v1/invoices

List invoices, newest first. Standard list envelope with cursor pagination.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | 1–100, default 25. |
| `before` | string | No | ISO timestamp cursor. Pass the previous page's `next_cursor`. |

Request:

```
curl "https://soca.to/v1/invoices?limit=25" \
  -H "Authorization: Bearer sk_live_..."
```

Response:

```
// 200 OK
{
  "object": "list",
  "data": [ { "id": "inv_...", "amount": "2500000", "status": "open", ... } ],
  "has_more": true,
  "next_cursor": "2026-07-17T12:00:00.000Z"
}
```

### POST /v1/plans

Create a subscription plan (product + price + platform-signed on-chain plan). Returns a hosted `checkout_url` for subscribers; the first successful charge fires `subscription.active`. Minimum price is 1.00 USDC. Plans are create-only; there is no GET /v1/plans yet.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The plan/product name, 2–80 characters. |
| `amount` | integer or string | Yes | USDC base units charged per period. Minimum `1000000` (1.00 USDC); below that returns `400 amount_too_small`. |
| `interval` | string | Yes | One of `week`, `biweekly`, `month`, `year`, `custom`. |
| `interval_days` | integer | No | Required when `interval` is `custom`: days per billing period, 1–365. |
| `description` | string | No | Optional, up to 300 characters. |
| `usage_type` | string | No | `licensed` (default) charges the fixed `amount` each period. `metered` bills the customer’s usage each period instead, and `amount` becomes the per-period **cap** (the most that can be pulled). Metered plans take no charge at subscribe. |
| `meter` | string | No | Required when `usage_type` is `metered`: the meter id (from POST /v1/meters or GET /v1/meters) whose per-period usage is billed. |
| `pricing_model` | object | No | Required when `usage_type` is `metered`: how usage is priced. One of `{"kind":"per_unit","unitAmount":100,"includedUnits":0}`, `{"kind":"package","unitAmount":...,"packageSize":...}`, `{"kind":"tiered","mode":"graduated"|"volume","tiers":[{"upTo":1000,"unitAmount":...},{"upTo":null,"unitAmount":...}]}`, or `{"kind":"flat","amount":...}`. Amounts are USDC base units; the last tier’s `upTo` must be `null`. |

Request:

```
curl -X POST https://soca.to/v1/plans \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Pro","amount":"9000000","interval":"month"}'

# Usage-based (metered) plan: bills usage each period, capped at amount
curl -X POST https://soca.to/v1/plans \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"API","amount":"100000000","interval":"month","usage_type":"metered","meter":"meter_...","pricing_model":{"kind":"per_unit","unitAmount":100}}'
```

Response:

```
// 201 Created
{
  "object": "plan",
  "id": "price_...",
  "amount": "9000000",
  "interval": "month",
  "product": { "id": "prod_...", "name": "Pro", "description": null },
  "checkout_url": "https://soca.to/pay/price_..."
}
```

### GET /v1/payments

List confirmed on-chain payments, newest first.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | 1–100, default 25. |
| `before` | string | No | ISO timestamp cursor from `next_cursor`. |

Request:

```
curl https://soca.to/v1/payments \
  -H "Authorization: Bearer sk_live_..."
```

Response:

```
// 200 OK
{ "object": "list", "data": [ ... ], "has_more": false, "next_cursor": null }
```

### GET /v1/subscriptions

List subscriptions, newest first.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | 1–100, default 25. |
| `before` | string | No | ISO timestamp cursor from `next_cursor`. |

Request:

```
curl https://soca.to/v1/subscriptions \
  -H "Authorization: Bearer sk_live_..."
```

Response:

```
// 200 OK
{ "object": "list", "data": [ ... ], "has_more": false, "next_cursor": null }
```

### GET /v1/events

List the event feed backing your webhooks, newest first. Poll it or reconcile deliveries against it. Event types are listed in [Webhooks and events](/docs/developers/webhooks).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | 1–100, default 25. |
| `before` | string | No | ISO timestamp cursor from `next_cursor`. |

Request:

```
curl https://soca.to/v1/events \
  -H "Authorization: Bearer sk_live_..."
```

Response:

```
// 200 OK
{ "object": "list", "data": [ { "id": "evt_...", "type": "invoice.paid", ... } ], "has_more": false, "next_cursor": null }
```

## Usage billing

Meters are defined in the dashboard; your backend streams usage against them by `event_name`. Customers can be referenced by Soca `customer_id` or by `external_customer_id`: your own stable identifier, which Soca upserts into a customer record on first sight.

### POST /v1/meters

Create a meter — the named thing usage is recorded against. Its `key` is the `event_name` you send events with. Idempotent by key (a repeat returns the existing meter). Create a meter before a metered plan or a metered feature can reference it.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | Yes | The event name. Letters, numbers, dot, dash, underscore. Unique per account + mode. |
| `display_name` | string | Yes | Human name shown in the dashboard. |
| `aggregation` | string | No | How events roll into a period total: `sum` (default), `count`, `max`, or `last`. |
| `event_value_key` | string | No | The event property holding the quantity. Omit and each event counts as 1 (use with `count`). |
| `unit_label` | string | No | Optional display unit, e.g. "tokens", "requests". |
| `description` | string | No | Optional. |

Request:

```
curl -X POST https://soca.to/v1/meters \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"key":"api_calls","display_name":"API calls","aggregation":"sum","unit_label":"calls"}'
```

Response:

```
// 201 Created
{ "object": "meter", "id": "meter_...", "key": "api_calls", "display_name": "API calls", "aggregation": "sum", "unit_label": "calls" }
```

### POST /v1/features

Create a feature — a capability you gate with entitlements. `boolean` (has it or not) or `metered` (has it, up to an allowance enforced against a meter). Idempotent by key. Create a feature before granting or checking it.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | Yes | The key you check access by. Letters, numbers, dot, dash, underscore. |
| `name` | string | Yes | Human name. |
| `type` | string | No | `boolean` (default) or `metered`. |
| `meter` | string | No | Required for a `metered` feature: the meter id whose usage counts against the allowance. |
| `unit_label` | string | No | Optional. |
| `description` | string | No | Optional. |

Request:

```
curl -X POST https://soca.to/v1/features \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"key":"pro_models","name":"Pro models","type":"boolean"}'
```

Response:

```
// 201 Created
{ "object": "feature", "id": "feat_...", "key": "pro_models", "name": "Pro models", "type": "boolean", "meter_id": null }
```

### GET /v1/meters

List your usage meters, newest first. Standard list envelope.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | 1–100, default 25. |
| `before` | string | No | ISO timestamp cursor from `next_cursor`. |

Request:

```
curl https://soca.to/v1/meters \
  -H "Authorization: Bearer sk_live_..."
```

Response:

```
// 200 OK
{ "object": "list", "data": [ ... ], "has_more": false, "next_cursor": null }
```

### POST /v1/meter-events

Ingest usage: one event, or a batch of up to 500 as `{ "events": [...] }`. Idempotent per `idempotency_key` (a body field per event, or an `Idempotency-Key` header for single events): retrying with the same key returns 200 with `deduplicated: true` instead of double-counting. The meter must exist first, or you get `404 unknown_meter`. Rate limit: 600 requests/min.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `event_name` | string | Yes | The meter's event name, as defined in the dashboard. |
| `value` | integer or string | No | Non-negative usage amount. Required for most meters; count-aggregated meters default each event to 1, while other aggregations return `400 missing_value` without it. |
| `customer_id` | string | No | The Soca customer the usage belongs to. |
| `external_customer_id` | string | No | Your own stable customer identifier, which Soca upserts into a customer automatically. |
| `timestamp` | string | No | ISO-8601 event time. Defaults to now. |
| `idempotency_key` | string | No | Dedupe key, up to 255 characters. Safe to retry with the same key. |
| `properties` | object | No | Arbitrary JSON metadata stored with the event. |

Request:

```
curl -X POST https://soca.to/v1/meter-events \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"event_name":"api_call","external_customer_id":"user_812","value":3,"idempotency_key":"req_7f3a1c"}'
```

Response:

```
// 201 Created (200 with "deduplicated": true on an idempotent replay)
{
  "object": "meter_event",
  "id": "me_...",
  "meter": "api_call",
  "customer_id": "cus_...",
  "value": "3",
  "event_timestamp": "2026-07-21T09:30:00.000Z",
  "deduplicated": false
}

// Batch request body: {"events":[{...},{...}]} (1–500 events)
// Batch response: 200 OK, per-event results in order:
{ "object": "list", "accepted": 2, "rejected": 0, "data": [ {...}, {...} ] }
```

## Credits

Prepaid balances per customer, backed by an immutable ledger. Grant credits in, debit them as usage happens, and read the balance back. Grants and debits are idempotent per `idempotency_key` (body field or `Idempotency-Key` header).

### POST /v1/credit_grants

Top up a customer's prepaid credit balance. One of `customer_id` / `external_customer_id` is required (`400 customer_required` otherwise). Emits a `credit.granted` event.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `amount` | integer or string | Yes | USDC base units to add to the balance. Must be positive. |
| `customer_id` | string | No | The Soca customer to credit. |
| `external_customer_id` | string | No | Your own stable customer identifier, upserted automatically. |
| `reason` | string | No | Ledger note, up to 300 characters. Defaults to "API grant". |
| `expires_at` | string | No | ISO-8601 expiry for the granted credits. |
| `idempotency_key` | string | No | Dedupe key, up to 255 characters. |

Request:

```
curl -X POST https://soca.to/v1/credit_grants \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"external_customer_id":"user_812","amount":"50000000","reason":"Starter pack"}'
```

Response:

```
// 201 Created (200 with "deduplicated": true on an idempotent replay)
{
  "object": "credit_grant",
  "customer_id": "cus_...",
  "granted": "50000000",
  "balance": "50000000",
  "entry_id": "...",
  "deduplicated": false
}
```

### POST /v1/credit_debits

Draw down a customer's prepaid credits for usage. By default the debit covers only what the balance allows and returns the remainder as `shortfall` for you to bill separately; pass `allow_negative: true` to let the balance go below zero instead. The customer must already have a credit account (`404 no_credit_account` otherwise). Emits a `credit.debited` event. Rate limit: 120 requests/min.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `amount` | integer or string | Yes | USDC base units to draw down. Must be positive. |
| `customer_id` | string | No | The Soca customer to debit. |
| `external_customer_id` | string | No | Your own stable customer identifier. |
| `reason` | string | No | Ledger note, up to 300 characters. Defaults to "API debit". |
| `allow_negative` | boolean | No | Allow the balance to go negative instead of reporting a shortfall. Default false. |
| `idempotency_key` | string | No | Dedupe key, up to 255 characters. |

Request:

```
curl -X POST https://soca.to/v1/credit_debits \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"external_customer_id":"user_812","amount":"1250000","reason":"July overage"}'
```

Response:

```
// 200 OK
{
  "object": "credit_debit",
  "customer_id": "cus_...",
  "requested": "1250000",
  "covered": "1250000",
  "shortfall": "0",
  "balance": "48750000",
  "deduplicated": false
}
```

### GET /v1/customers/{id}/credit_balance

The customer's current prepaid balance plus ledger totals. A customer with no credit account returns a plain zero balance. Rate limit: 240 requests/min.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | The Soca customer id (path parameter). Unknown ids return `404 unknown_customer`. |

Request:

```
curl https://soca.to/v1/customers/cus_.../credit_balance \
  -H "Authorization: Bearer sk_live_..."
```

Response:

```
// 200 OK
{
  "object": "credit_balance",
  "customer_id": "cus_...",
  "account_id": "...",
  "balance": "48750000",
  "granted": "50000000",
  "consumed": "1250000",
  "currency": "usdc"
}

// No credit account yet:
{ "object": "credit_balance", "customer_id": "cus_...", "balance": "0", "currency": "usdc" }
```

## Entitlements

Entitlements map what a customer has paid for to what your product should let them do. Features are defined in the dashboard by `feature_key`; grants come from plans automatically or from this API manually; your backend asks the check endpoint before serving a feature.

### POST /v1/entitlement_grants

Grant a feature to a customer, optionally with a usage limit and an expiry. The feature must exist (`404 unknown_feature`), and one of `customer_id` / `external_customer_id` is required. Emits an `entitlement.granted` event.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `feature_key` | string | Yes | The feature to grant, as defined in the dashboard. |
| `customer_id` | string | No | The Soca customer to grant to. |
| `external_customer_id` | string | No | Your own stable customer identifier, upserted automatically. |
| `limit_value` | integer or string | No | For metered features: the usage limit (non-negative integer). |
| `active_until` | string | No | ISO-8601 timestamp for when the entitlement lapses. |

Request:

```
curl -X POST https://soca.to/v1/entitlement_grants \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"external_customer_id":"user_812","feature_key":"api_access","limit_value":"10000"}'
```

Response:

```
// 201 Created
{
  "object": "entitlement_grant",
  "id": "...",
  "feature": "api_access",
  "customer_id": "cus_..."
}
```

### GET /v1/customers/{id}/entitlements/{feature_key}

The access-control check your backend calls before serving a feature answers two questions: is this customer entitled right now, and for metered features, how much of the limit remains? `reason` is one of `active`, `grace`, `no_grant`, `not_started`, `revoked`, `expired`, `limit_exceeded`. Built for the hot path: 600 requests/min.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | The Soca customer id (path parameter). Unknown ids return `404 unknown_customer`. |
| `feature_key` | string | Yes | The feature to check (path parameter). Unknown keys return `404 unknown_feature`. |

Request:

```
curl https://soca.to/v1/customers/cus_.../entitlements/api_access \
  -H "Authorization: Bearer sk_live_..."
```

Response:

```
// 200 OK
{
  "object": "entitlement",
  "customer_id": "cus_...",
  "feature": "api_access",
  "entitled": true,
  "reason": "active",
  "limit": "10000",
  "used": "3120",
  "remaining": "6880"
}
```

> Anything not listed here does not exist yet. `GET /v1/{resource}` on an unknown resource returns `404 resource_not_found`; POSTing to a read-only resource (payments, subscriptions, events, meters) returns `405 method_not_allowed`.
