REST API

Stripe integration

Connect a site to Stripe, drive the import of its billing history, and read back the connection status. These endpoints power the same connect → align currency → select products → import flow the web Revenue page runs, so both surfaces stay in lock-step.

A site is addressed by {siteKey} (UUID or domain). All endpoints require Authorization: Bearer {token} — see Conventions.

Security

  • The status endpoint NEVER returns the API key or webhook secret. Both are stored encrypted and hidden on the model; the status shape carries only connection state, the account currency, the product allowlist, and sync progress.
  • Connect with a Stripe RESTRICTED secret key (rk_live_… / rk_test_…), never your full secret key. The key is stored encrypted and is never returned by any endpoint.
  • A key with the wrong prefix, a test key used in production, or a key Stripe itself rejects is rejected with 422.

Get integration status

GET /api/sites/{siteKey}/integrations/stripe

The current Stripe connection status for the site, shaped identically for the API and MCP so they can never drift. Backed by App\Support\Integrations\StripeIntegrationStatus, wrapped in StripeIntegrationResource. Never exposes the API key or webhook secret.

When disconnected the shape is minimal. When connected it adds the live account currency, the product allowlist, and sync progress.

Response

Field Type Description
connected bool Whether a Stripe integration exists for the site.
connected_at string (ISO 8601) | null When it was connected; null when disconnected.
account_currency string | null Stripe account settlement currency (read live). null when the restricted key lacks account read. Present only when connected.
products array Product allowlist. Each: external_id, name, selected (bool). Present only when connected.
sync object Import progress: running (bool) and resources[], each resource, status, processed_count, error (string|null), stranded (bool). Present only when connected.

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/integrations/stripe" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/integrations/stripe', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/integrations/stripe', headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Connect Stripe

POST /api/sites/{siteKey}/integrations/stripe

Connect (or reconnect) the site's Stripe restricted key. Backed by App\Actions\Integrations\ConnectStripeIntegration. On success the key is stored encrypted and the status shape is returned; the key is never echoed back.

Body parameters

Parameter Type Required Description
key string Yes A Stripe restricted secret key (rk_live_… / rk_test_…). Max 255 chars. Stored encrypted, never returned.

A bad prefix, a test key used in production, or a key Stripe rejects each returns 422 with the same validation error the web form shows.

Response — the integration status (as above). Status: 200 OK.

curl -X POST "https://clickbase.so/api/sites/example.com/integrations/stripe" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"key":"rk_live_..."}'
await fetch('https://clickbase.so/api/sites/example.com/integrations/stripe', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ key: 'rk_live_...' }) })
requests.post('https://clickbase.so/api/sites/example.com/integrations/stripe', json={'key':'rk_live_...'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Align reporting currency

PUT /api/sites/{siteKey}/integrations/stripe/currency

Step two of the connection flow: set the site's reporting currency to the Stripe account's settlement currency. Backed by App\Actions\Integrations\AlignSiteCurrency — which applies the same off-currency-payments guard as the site currency endpoint (a currency change is refused when it would strand already-ingested payments in another currency).

Body parameters

Parameter Type Required Description
currency string Yes ISO 4217 currency code (exactly 3 chars).

Response — the integration status (as above). Status: 200 OK.

curl -X PUT "https://clickbase.so/api/sites/example.com/integrations/stripe/currency" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"currency":"usd"}'
await fetch('https://clickbase.so/api/sites/example.com/integrations/stripe/currency', { method: 'PUT', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ currency: 'usd' }) })
requests.put('https://clickbase.so/api/sites/example.com/integrations/stripe/currency', json={'currency':'usd'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Select products and start the import

PUT /api/sites/{siteKey}/integrations/stripe/products

Step three: persist the product allowlist and then start the backfill — in that order. Backed by App\Actions\Integrations\UpdateStripeProductSelection followed by App\Actions\Integrations\StartStripeBackfill.

The order is forced, not stylistic: the allowlist filters ingestion only and never removes already-ingested history, so a backfill that ran before product selection would leave a permanently unfilterable history. Select, then import.

Body parameters

Parameter Type Required Description
products string[] Yes The allowlist of Stripe product ids to ingest. Must be present (may be empty). Each item max 255 chars.

Response — the integration status, now reflecting the running sync. Status: 200 OK.

curl -X PUT "https://clickbase.so/api/sites/example.com/integrations/stripe/products" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"products":["prod_ABC","prod_DEF"]}'
await fetch('https://clickbase.so/api/sites/example.com/integrations/stripe/products', { method: 'PUT', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ products: ['prod_ABC', 'prod_DEF'] }) })
requests.put('https://clickbase.so/api/sites/example.com/integrations/stripe/products', json={'products':['prod_ABC','prod_DEF']}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Retry a stranded resource

POST /api/sites/{siteKey}/integrations/stripe/retry

Retry one failed or stranded resource of the import. Backed by App\Actions\Integrations\ResumeStripeResourceSync.

This RESUMES from the resource's persisted cursor — it never restarts the import from page zero. A retry can never re-run a 40,000-charge import from the beginning. The resource must already have a sync row; a resource with no sync row returns 404.

Body parameters

Parameter Type Required Description
resource string Yes One of the StripeResource values (see below).

resource is one of: products, charges, refunds, disputes, invoices, subscriptions, customers. Any other value returns 422.

Response — the integration status. Status: 200 OK.

curl -X POST "https://clickbase.so/api/sites/example.com/integrations/stripe/retry" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"resource":"charges"}'
await fetch('https://clickbase.so/api/sites/example.com/integrations/stripe/retry', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ resource: 'charges' }) })
requests.post('https://clickbase.so/api/sites/example.com/integrations/stripe/retry', json={'resource':'charges'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Disconnect Stripe

DELETE /api/sites/{siteKey}/integrations/stripe

Disconnect the site's Stripe integration. Wipes Stripe-derived payments, subscriptions, MRR movements, invoice MRR observations, lifecycle events, and settlement FX rates (WipeStripeRevenueData), then deletes the remote webhook (best-effort) and the local integration row. API/manual payments are kept. Backed by App\Actions\Integrations\DisconnectStripeIntegration. Returns the minimal disconnected status (connected: false, connected_at: null).

Response

Field Type Description
connected bool Always false after disconnect.
connected_at null Always null after disconnect.

Status: 200 OK.

curl -X DELETE "https://clickbase.so/api/sites/example.com/integrations/stripe" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/integrations/stripe', { method: 'DELETE', headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.delete('https://clickbase.so/api/sites/example.com/integrations/stripe', headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})