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'})