Billing API Endpoints

REST API endpoints for managing credits and subscriptions. All authenticated endpoints use the auth token to identify the user — no userId parameter is needed.

Get Balance Status

Get current Dollarinos balance and subscription info.

GET /api/v1/billing/balance

Example

curl https://clanker.net/api/v1/billing/balance \
  -H "x-auth-token: YOUR_AUTH_TOKEN"

Response

{
  "balance": 450,
  "totalSpent": 50,
  "canExecute": true,
  "isAdmin": false
}

Response Fields

FieldTypeDescription
balancenumberTotal available Dollarinos
totalSpentnumberTotal Ds spent all-time
canExecutebooleanWhether user has sufficient credits to execute
isAdminbooleanWhether user is an admin

List Credit Packs

Get available credit packs for purchase.

GET /api/v1/billing/packs

Example

curl https://clanker.net/api/v1/billing/packs \
  -H "x-auth-token: YOUR_AUTH_TOKEN"

Response

{
  "packs": [
    { "id": "pack_100", "name": "100 Ds", "dollarinos": 100, "priceCents": 100 },
    { "id": "pack_250", "name": "250 Ds", "dollarinos": 250, "priceCents": 225 },
    { "id": "pack_500", "name": "500 Ds", "dollarinos": 500, "priceCents": 425 },
    { "id": "pack_1000", "name": "1000 Ds", "dollarinos": 1000, "priceCents": 800 },
    { "id": "pack_5000", "name": "5000 Ds", "dollarinos": 5000, "priceCents": 3750 }
  ]
}

Purchase Credits

Purchase a credit pack.

POST /api/v1/billing/purchase

Request Body

FieldTypeRequiredDescription
packIdstringYesPack ID to purchase
emailstringYesUser email for receipt
successUrlstringYesRedirect URL on success
cancelUrlstringYesRedirect URL on cancel

Example

curl -X POST https://clanker.net/api/v1/billing/purchase \
  -H "x-auth-token: YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "packId": "pack_500",
    "email": "user@example.com",
    "successUrl": "https://clanker.net/payment-success",
    "cancelUrl": "https://clanker.net/payment-cancel"
  }'

Response

{
  "checkoutUrl": "https://checkout.example.com/c/pay/cs_xxxxxxxxxxxx"
}

Errors

Statuserror.codeDescription
401UNAUTHORIZEDMissing or invalid credentials
400INVALID_INPUTUnknown packId, or checkout could not be created
500INTERNAL_ERRORCheckout session creation failed unexpectedly

Everything downstream of the redirect — card decline, abandonment, refunds — is settled by the payment provider and reported back through the webhook, not by this call. A checkoutUrl means the session was created, nothing more.

Return URLs

successUrl and cancelUrl are where the payment provider sends the browser when checkout finishes. Which pair you send depends on where checkout was opened from:

ClientsuccessUrl / cancelUrl
Webhttps://clanker.net/payment-success / https://clanker.net/payment-cancel — app screens served by the SPA
Native (iOS/Android)https://clanker.net/api/v1/billing/return/success / https://clanker.net/api/v1/billing/return/cancel

The example above is the web pair: the browser is already inside the app, so the provider can redirect straight to a screen.

Native cannot use those — the checkout runs in a system browser with no route back into the app. It sends the provider to /api/v1/billing/return/:outcome instead, where :outcome is success or cancel. That endpoint needs no auth and returns an HTML page that redirects to the net.clanker://payment-success (or …/payment-cancel) deep link, with a tappable fallback link for browsers that block the automatic hop.

Neither page settles the payment. Credits are granted by the webhook, so a user who closes the browser before the redirect still gets what they paid for.


Get Transaction History

Get credit transaction history.

GET /api/v1/billing/transactions

Example

curl https://clanker.net/api/v1/billing/transactions \
  -H "x-auth-token: YOUR_AUTH_TOKEN"

Response

Returns the 50 most recent transactions, newest first. There is no pagination parameter.

{
  "transactions": [
    {
      "id": 1,
      "userId": "3ab91c22-…",
      "amount": -45,
      "type": "usage",
      "description": "README Generator execution",
      "executionId": "b71e0c94-…",
      "stripeSessionId": null,
      "createdAt": "2024-01-15T10:31:30.000Z"
    },
    {
      "id": 2,
      "userId": "3ab91c22-…",
      "amount": 500,
      "type": "purchase",
      "description": "500 Ds",
      "executionId": null,
      "stripeSessionId": "cs_xxxxxxxxxxxx",
      "createdAt": "2024-01-10T14:00:00.000Z"
    }
  ]
}

A row is a delta, not a balance snapshot — amount is signed and there is no running-total field. Read the current balance from GET /api/v1/billing/balance.

Transaction Types

TypeDescription
usageDs spent on a run (negative amount, usually linked by executionId)
purchaseDs added — a credit pack, or a Based Mode activation/renewal grant

Get Subscription Tier

Get current subscription mode.

GET /api/v1/billing/subscription/tier

Example

curl https://clanker.net/api/v1/billing/subscription/tier \
  -H "x-auth-token: YOUR_AUTH_TOKEN"

Response

{
  "tier": "paygmode",
  "info": {
    "name": "PAYG MODE",
    "priceCents": 0,
    "priceDisplay": "Free",
    "monthlyCredits": 0,
    "packDiscount": 0
  },
  "subscriptionExpiresAt": null,
  "isTestMode": false
}

Modes

ModePriceMonthly DsPack Discount
byokmodeFree0
paygmodeFree0
basedmode$29/mo2,00020%
sitelicenseLicensed0

sitelicense can come back from this endpoint but is deliberately absent from /subscription/info, which lists only the three self-serve modes.

Tier derivation: the tier is computed at runtime — not stored as a column. A valid subscriptionExpiresAt in the future → basedmode. A stored BYOK key → byokmode. Otherwise → paygmode. There is no public endpoint to set the mode directly; it follows from your BYOK key and subscription state.


Get Subscription Info

Get pricing and details for each billing mode.

GET /api/v1/billing/subscription/info

Example

curl https://clanker.net/api/v1/billing/subscription/info \
  -H "x-auth-token: YOUR_AUTH_TOKEN"

Response

{
  "modes": {
    "byokmode": { "name": "BYOK MODE", "priceCents": 0, "priceDisplay": "Free", "monthlyCredits": 0, "packDiscount": 0 },
    "paygmode": { "name": "PAYG MODE", "priceCents": 0, "priceDisplay": "Free", "monthlyCredits": 0, "packDiscount": 0 },
    "basedmode": { "name": "BASED MODE", "priceCents": 2900, "priceDisplay": "$29/mo", "monthlyCredits": 2000, "packDiscount": 0.2 }
  }
}

BYOK Key Management

Manage your Bring-Your-Own-Key (BYOK) LLM API key. Available on every mode — a key is the only way to reach a provider this deployment holds no key for. What a key changes is billing (0 Ds charged for calls that ride on it) and which provider your requests are routed to; storing one promotes paygmode to byokmode.

Get BYOK Status

GET /api/v1/byok

Example

curl https://clanker.net/api/v1/byok \
  -H "x-auth-token: YOUR_AUTH_TOKEN"

Response

{
  "hasKey": true,
  "keyPreview": "sk-...xxxx",
  "provider": "anthropic"
}

provider is null when no key is stored.


Set BYOK Key

Store or replace your LLM API key. Switches the account to byokmode.

PUT /api/v1/byok

Request Body

FieldTypeRequiredDescription
keystringYesLLM API key. sk-ant-… → Anthropic, any other sk-… → OpenAI

The provider is decided by the key’s prefix, not by anything you send — the sk-ant- test runs first, because Anthropic keys also start with sk-.

Example

curl -X PUT https://clanker.net/api/v1/byok \
  -H "x-auth-token: YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "key": "sk-ant-..." }'

Response

{
  "hasKey": true,
  "keyPreview": "sk-...xxxx",
  "provider": "anthropic"
}

Errors

Statuserror.codeDescription
400INVALID_INPUTkey missing, not a string, or not a recognised sk- prefix
404NOT_FOUNDUser record not found

Delete BYOK Key

Remove the stored BYOK key. Account reverts to paygmode.

DELETE /api/v1/byok

Example

curl -X DELETE https://clanker.net/api/v1/byok \
  -H "x-auth-token: YOUR_AUTH_TOKEN"

Response

{ "hasKey": false, "keyPreview": null }

Get Publishable Key

Get the publishable key for client-side payment forms.

GET /api/v1/billing/publishable-key

Example

curl https://clanker.net/api/v1/billing/publishable-key \
  -H "x-auth-token: YOUR_AUTH_TOKEN"

Response

{
  "publishableKey": "pk_live_xxxxxxxxxxxx"
}