Objects & fields
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" } }