Authentication, scopes, rate limits and the core endpoints.
Everything the App API exposes today.
The App API is how your app reads the merchant's data. Version 1 is read-only — every endpoint is a GET, anything else returns 405 method_not_allowed. Write capabilities are granted case by case after review.
Base URL: https://api.spuke.com/v1/app · Auth: Authorization: Bearer spk_at_live_… (see Authentication).
Endpoints
| Method |
Path |
Scope |
What it gives you |
| GET |
/v1/merchant |
merchant:read |
Profile of the merchant that installed your app: company, country, VAT number, status. Call once after install to label the connection. |
| GET |
/v1/payments |
payments:read |
Processed payments, newest first. Live traffic supports ?status=succeeded. |
| GET |
/v1/payments/{id} |
payments:read |
One payment. The {id} is the charge_id in live and the row id in sandbox. |
| GET |
/v1/refunds |
refunds:read |
Sandbox: refund rows. Live: refunded charges with the running amount_refunded. |
| GET |
/v1/disputes |
disputes:read |
Chargebacks including evidence_due_by and past_due. |
| GET |
/v1/payouts |
payouts:read |
Bank settlements. Restricted scope — reconciliation tools only. |
| GET |
/v1/balance |
payouts:read |
Available and pending balance of the merchant. |
| GET |
/v1/products |
products:read |
The merchant's catalog (no prices in v1). |
| GET |
/v1/products/{id} |
products:read |
One product. |
| GET |
/v1/invoices |
invoices:read |
Issued invoices. Live only — sandbox returns an empty list. |
| GET |
/v1/invoices/{id} |
invoices:read |
One invoice. |
| GET |
/v1/subscriptions |
subscriptions:read |
Recurring agreements with status and period end. |
| GET |
/v1/analytics/summary |
analytics:read |
Aggregated volume for a window: ?days=30 (1–365). |
Field-by-field descriptions of every response are in Objects & fields.
Query parameters
| Parameter |
Where |
Behaviour |
limit |
all list endpoints |
1–100, default 25. |
offset |
all list endpoints |
Row offset, default 0. |
status |
/v1/payments (live) |
Exact match, e.g. ?status=succeeded. |
days |
/v1/analytics/summary |
1–365, default 30. |
Unknown query parameters are ignored, not rejected. There are no date-range filters in v1 — page by offset and stop at the first object older than your cursor.
Pagination
curl "https://api.spuke.com/v1/app/v1/payments?limit=50&offset=100" \
-H "Authorization: Bearer spk_at_live_…"
{ "object": "list", "data": [ … ], "has_more": true }
Lists are sorted newest first and carry no total count. Iterate until has_more is false. New objects arrive while you page, so for exports read the first page frequently rather than deep-paging a moving list.
Test vs live
The token decides the data source: a sandbox token reads your environment's simulated data, a live token reads the merchant's real data. Field names differ between the two — see the table at the top of Objects & fields.
Amounts and timestamps
Amounts are integer minor units with a lowercase ISO currency: { "amount": 2500, "currency": "eur" } is €25.00. Never use floats. Timestamps are ISO-8601 UTC; arrival_date, issue_date and due_date are dates without a time part.
Errors
{ "error": { "type": "insufficient_scope", "message": "Missing required scope: payments:read" } }
See Error & status codes.
The shape of every resource you can read.
This page describes the actual JSON the App API returns, field by field.
One thing to internalise first: the API serves two different data sources depending on the token.
| Token |
Data source |
Field style |
spk_at_test_… (sandbox environment) |
Your simulated environment data |
amount_cents, created_at, UUID id |
spk_at_live_… (installed merchant) |
The merchant's real processed data |
amount, charge_created_at, processor IDs |
Both are documented below. Write your parser against both, or gate features by livemode.
Global rules
| Rule |
What it means |
| Amounts are integers in minor units |
2500 with "currency": "eur" is €25.00. Never use floats. |
| Currencies are lowercase ISO-4217 |
eur, chf, usd. |
| Timestamps are ISO-8601 UTC strings |
2026-08-17T09:41:12Z. Date-only fields (arrival_date, due_date) have no time part. |
| IDs are opaque |
Sandbox rows use UUIDs, live rows use processor IDs (ch_…, pi_…, po_…). Never parse them. |
| Objects are additive |
Ignore unknown fields instead of failing. |
object names the type |
payment, product, invoice, merchant, balance, analytics_summary, list. |
Merchant — GET /v1/merchant
Who installed your app.
{ "object": "merchant", "id": "…", "business_name": "Ada GmbH", "email": "ops@ada.de",
"country": "DE", "city": "Berlin", "vat_number": "DE123456789", "reference": "M-10423",
"status": "approved", "plan_id": "…", "livemode": true, "created_at": "2026-05-02T08:00:00Z" }
| Field |
Meaning |
business_name |
Legal/company name from onboarding. |
reference |
Short merchant reference used in support tickets. |
status |
Onboarding state — only approved merchants process live payments. |
plan_id |
The merchant's spuke plan; null in sandbox. |
livemode |
false means you are talking to a sandbox test merchant. |
Payment — GET /v1/payments, /v1/payments/{id}
One processed payment. In sandbox the row is a simulated checkout session; in live it is the real charge.
Live
{ "object": "payment", "livemode": true, "charge_id": "ch_3Nk91x", "payment_intent_id": "pi_3Nk91x",
"amount": 2500, "amount_refunded": 0, "currency": "eur", "status": "succeeded", "refunded": false,
"customer_email": "ada@example.com", "customer_name": "Ada Lovelace", "description": "Order #1042",
"payment_method_type": "card", "card_brand": "visa", "card_last4": "4242", "card_country": "DE",
"receipt_url": "https://…", "failure_code": null, "failure_message": null,
"charge_created_at": "2026-08-17T09:41:12Z" }
| Field |
Meaning |
charge_id |
The identifier of this payment. Use it for /v1/payments/{id} and to match refunds/disputes. |
payment_intent_id |
The intent the charge belongs to; matches payment_intent in webhook payloads. |
amount |
Amount charged, minor units. Never changes. |
amount_refunded |
Total refunded so far. amount - amount_refunded is what the merchant kept. |
refunded |
true once fully refunded. |
status |
succeeded (money captured), pending, failed. Only succeeded means paid. |
customer_email / customer_name |
Buyer details from checkout — personal data. |
payment_method_type |
card, twint, sepa_debit, paypal, … |
card_brand / card_last4 / card_country |
Display only. Full card data never leaves the processor. |
receipt_url |
Hosted receipt you can link to. |
failure_code / failure_message |
Populated on failed payments. |
charge_created_at |
When the payment was processed. Also the sort key of the list. |
Sandbox
{ "object": "payment", "livemode": false, "id": "…uuid", "amount_cents": 2500,
"amount_refunded_cents": 0, "fee_cents": 74, "net_cents": 2426, "currency": "eur",
"status": "completed", "customer_email": "test@spuke.test", "customer_name": "Test Buyer",
"description": "Order #1", "payment_method_type": "card", "card_brand": "visa", "card_last4": "4242",
"failure_code": null, "failure_message": null, "disputed": false,
"payment_intent_id": "pi_test_…", "charge_id": "ch_test_…", "created_at": "2026-08-17T09:41:12Z" }
| Extra field |
Meaning |
fee_cents / net_cents |
Simulated spuke fee and net amount credited to the test balance. |
disputed |
true once you simulated a dispute on it. |
status |
Sandbox uses completed / open / failed / canceled. |
Filter live payments with ?status=succeeded.
Refund — GET /v1/refunds
Sandbox returns real refund rows:
{ "id": "…uuid", "amount_cents": 500, "currency": "eur", "status": "succeeded",
"reason": "requested_by_customer", "charge_id": "ch_test_…", "payment_intent_id": "pi_test_…",
"created_at": "2026-08-18T07:02:10Z" }
Live returns the refunded charges (one row per charge, not per refund):
{ "charge_id": "ch_3Nk91x", "amount_refunded": 500, "currency": "eur",
"refunded": false, "updated_at": "2026-08-18T07:02:10Z" }
refunded: false with a non-zero amount_refunded means a partial refund. amount_refunded is the authoritative running total for that payment.
Dispute — GET /v1/disputes
A chargeback: the disputed amount is held and there is a hard deadline.
Live
{ "dispute_id": "dp_7c", "charge_id": "ch_3Nk91x", "amount": 2500, "currency": "eur",
"status": "needs_response", "reason": "fraudulent", "evidence_due_by": "2026-08-31T23:59:59Z",
"has_evidence": false, "past_due": false, "dispute_created_at": "2026-08-19T11:15:00Z" }
| Field |
Meaning |
status |
needs_response (act now) → under_review → won (funds returned) or lost (funds gone plus a fee). |
reason |
Why the bank reversed it — drives which evidence helps. |
evidence_due_by |
Hard deadline. Missing it is an automatic loss. Surface it prominently. |
has_evidence |
Whether evidence was already submitted. |
past_due |
true once the deadline passed. |
Sandbox adds id, session_id, amount_cents, fee_cents, evidence_submitted_at, closed_at, created_at.
Payout — GET /v1/payouts
A transfer of the merchant's balance to their bank. It bundles many payments — never 1:1 with a payment. Requires the restricted payouts:read scope.
{ "payout_id": "po_44", "amount": 128400, "currency": "eur", "status": "paid",
"method": "standard", "arrival_date": "2026-08-21",
"destination_bank_name": "Commerzbank", "destination_last4": "4021",
"payout_created_at": "2026-08-19T02:00:00Z" }
| Field |
Meaning |
amount |
Net amount sent to the bank, after fees, refunds and reserves. |
status |
pending → in_transit → paid, or failed (returned to the balance). |
arrival_date |
Expected bank value date (date only). |
destination_* |
Which bank account received it. |
Sandbox rows use id, amount_cents, created_at plus failure_code / failure_message.
Balance — GET /v1/balance
Money currently held for the merchant. Same scope as payouts.
{ "object": "balance", "livemode": false,
"available": [{ "amount": 240000, "currency": "eur" }],
"pending": [{ "amount": 35000, "currency": "eur" }] }
| Field |
Meaning |
available |
Matured funds that can be paid out. |
pending |
Funds still inside the settlement delay (T+2 in sandbox). |
Live balances are served as empty arrays unless the installation has settlement access.
Product — GET /v1/products, /v1/products/{id}
{ "object": "product", "id": "…uuid", "name": "Pro plan", "description": "…", "active": true,
"images": ["https://…"], "tags": ["saas"], "unit_label": "seat", "url": "https://…",
"created_at": "…", "updated_at": "…" }
| Field |
Meaning |
active |
false = archived. Hide from new purchases, keep for history. |
unit_label |
What one unit is called on the checkout page. |
images / tags / url |
Listing data maintained by the merchant. |
Prices are not embedded — they belong to the merchant's price list and are not exposed in v1.
Invoice — GET /v1/invoices, /v1/invoices/{id}
Live only; sandbox returns an empty list.
{ "object": "invoice", "id": "…uuid", "invoice_number": "2026-0301", "status": "paid",
"currency": "eur", "subtotal_amount": 1900, "tax_amount": 361, "total_amount": 2261,
"customer_name": "Ada GmbH", "customer_email": "ops@ada.de", "customer_type": "business",
"issue_date": "2026-08-01", "due_date": "2026-09-01", "paid_at": "2026-08-04T10:00:00Z",
"created_at": "2026-08-01T09:00:00Z" }
| Field |
Meaning |
invoice_number |
Legally sequential number. Show this to humans, use id in code. |
subtotal_amount / tax_amount / total_amount |
Net, tax and gross, minor units. |
customer_type |
individual or business — drives tax treatment. |
status |
draft (not sent) · open (sent, unpaid) · paid · void (cancelled) · uncollectible (written off). |
issue_date / due_date |
Dates only. |
paid_at |
null until settled. |
Subscription — GET /v1/subscriptions
{ "id": "…uuid", "status": "active", "currency": "eur", "amount_minor": 1900, "quantity": 1,
"customer_email": "ada@example.com", "customer_name": "Ada Lovelace",
"current_period_start": "2026-08-17T09:41:12Z", "current_period_end": "2026-09-17T09:41:12Z",
"cancel_at_period_end": false, "created_at": "2026-05-02T08:00:00Z" }
| Field |
Meaning |
amount_minor × quantity |
What is billed each period. |
current_period_end |
When the next invoice is generated — use it as the access expiry date. |
cancel_at_period_end |
true = still active now, ends at current_period_end. Don't revoke access yet. |
status |
trialing / active → grant access. past_due → dunning, grace period is your call. canceled / unpaid → revoke. |
Analytics summary — GET /v1/analytics/summary?days=30
Pre-aggregated volume; far cheaper than paging every payment. days is clamped to 1–365 (default 30).
{ "object": "analytics_summary", "livemode": true, "period_days": 30, "payment_count": 512,
"gross_amount": 1284000, "refunded_amount": 21500, "net_amount": 1262500, "currency": "eur" }
| Field |
Meaning |
period_days |
Length of the window, ending now. |
payment_count |
Number of succeeded payments in the window. |
gross_amount |
Sum of succeeded payments. |
refunded_amount |
Refunded amount in the window. |
net_amount |
gross_amount - refunded_amount. Processor fees are not deducted here. |
currency |
Currency of the first row; null when there was no volume. |
Aggregation reads at most the 1 000 most recent payments of the window.
List envelope
Every list endpoint wraps results the same way:
{ "object": "list", "data": [ … ], "has_more": true }
| Field |
Meaning |
data |
The array of objects for this page. |
has_more |
true = keep paging with a higher offset. There is no total count and no cursor. |
Error envelope
{ "error": { "type": "insufficient_scope", "message": "Missing required scope: payments:read" } }