Browse docs

Developers

API reference

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

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 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.
  • Errors share one envelope: { "error": { "code": "...", "message": "..." } }. The full code table is in 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.

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.

ParameterTypeRequiredDescription
amountinteger or stringYesUSDC base units: 2500000 = 2.50 USDC. Must be a positive integer.
descriptionstringYesShown on the hosted checkout page. 1–500 characters.
memostringNoFree-form note, up to 200 characters.
invoice_numberstringNoYour own reference, up to 100 characters.
recipientstringNoWho it is billed to: a label or email for your records, up to 200 characters.
due_atstringNoISO-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.

ParameterTypeRequiredDescription
limitintegerNo1–100, default 25.
beforestringNoISO 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.

ParameterTypeRequiredDescription
namestringYesThe plan/product name, 2–80 characters.
amountinteger or stringYesUSDC base units charged per period. Minimum 1000000 (1.00 USDC); below that returns 400 amount_too_small.
intervalstringYesOne of week, biweekly, month, year, custom.
interval_daysintegerNoRequired when interval is custom: days per billing period, 1–365.
descriptionstringNoOptional, up to 300 characters.
usage_typestringNolicensed (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.
meterstringNoRequired when usage_type is metered: the meter id (from POST /v1/meters or GET /v1/meters) whose per-period usage is billed.
pricing_modelobjectNoRequired 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.

ParameterTypeRequiredDescription
limitintegerNo1–100, default 25.
beforestringNoISO 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.

ParameterTypeRequiredDescription
limitintegerNo1–100, default 25.
beforestringNoISO 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.

ParameterTypeRequiredDescription
limitintegerNo1–100, default 25.
beforestringNoISO 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.

ParameterTypeRequiredDescription
keystringYesThe event name. Letters, numbers, dot, dash, underscore. Unique per account + mode.
display_namestringYesHuman name shown in the dashboard.
aggregationstringNoHow events roll into a period total: sum (default), count, max, or last.
event_value_keystringNoThe event property holding the quantity. Omit and each event counts as 1 (use with count).
unit_labelstringNoOptional display unit, e.g. "tokens", "requests".
descriptionstringNoOptional.
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.

ParameterTypeRequiredDescription
keystringYesThe key you check access by. Letters, numbers, dot, dash, underscore.
namestringYesHuman name.
typestringNoboolean (default) or metered.
meterstringNoRequired for a metered feature: the meter id whose usage counts against the allowance.
unit_labelstringNoOptional.
descriptionstringNoOptional.
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.

ParameterTypeRequiredDescription
limitintegerNo1–100, default 25.
beforestringNoISO 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.

ParameterTypeRequiredDescription
event_namestringYesThe meter's event name, as defined in the dashboard.
valueinteger or stringNoNon-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_idstringNoThe Soca customer the usage belongs to.
external_customer_idstringNoYour own stable customer identifier, which Soca upserts into a customer automatically.
timestampstringNoISO-8601 event time. Defaults to now.
idempotency_keystringNoDedupe key, up to 255 characters. Safe to retry with the same key.
propertiesobjectNoArbitrary 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":[{...},{...}]} (1500 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.

ParameterTypeRequiredDescription
amountinteger or stringYesUSDC base units to add to the balance. Must be positive.
customer_idstringNoThe Soca customer to credit.
external_customer_idstringNoYour own stable customer identifier, upserted automatically.
reasonstringNoLedger note, up to 300 characters. Defaults to "API grant".
expires_atstringNoISO-8601 expiry for the granted credits.
idempotency_keystringNoDedupe 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.

ParameterTypeRequiredDescription
amountinteger or stringYesUSDC base units to draw down. Must be positive.
customer_idstringNoThe Soca customer to debit.
external_customer_idstringNoYour own stable customer identifier.
reasonstringNoLedger note, up to 300 characters. Defaults to "API debit".
allow_negativebooleanNoAllow the balance to go negative instead of reporting a shortfall. Default false.
idempotency_keystringNoDedupe 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.

ParameterTypeRequiredDescription
idstringYesThe 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.

ParameterTypeRequiredDescription
feature_keystringYesThe feature to grant, as defined in the dashboard.
customer_idstringNoThe Soca customer to grant to.
external_customer_idstringNoYour own stable customer identifier, upserted automatically.
limit_valueinteger or stringNoFor metered features: the usage limit (non-negative integer).
active_untilstringNoISO-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.

ParameterTypeRequiredDescription
idstringYesThe Soca customer id (path parameter). Unknown ids return 404 unknown_customer.
feature_keystringYesThe 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.

Was this page helpful?

A quick signal helps us improve the right pages.