The client_secret authorises one specific browser session to load the hosted checkout for that session. It is scoped to a single cs_… id and cannot be used to create, modify, refund, or list anything.
It is safe to send to the browser (embed page, redirect URL, iframe src). It is not safe to log publicly or share across users — anyone with the value can open that particular checkout.
Lifetime
Event
Effect on client_secret
Session created
Valid for 24 hours or until session status changes.
Session complete / expired / canceled
Immediately invalid — loading the checkout returns session_expired.
Session paid via async method (SEPA, Klarna)
Immediately invalid; buyer is redirected to success_url.
There is no way to renew a client_secret. If it expires, create a new Checkout Session and redirect to the new checkout_url.
Correct usage
Backend (your server):
const session = await fetch("https://api.spuke.com/v1/checkout/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPUKE_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `order_${orderId}`,
},
body: JSON.stringify({ amount: 4990, currency: "eur", customer_name, customer_email }),
}).then(r => r.json());
// Redirect the buyer:
res.redirect(303, session.checkout_url);
// OR return only what the browser needs:
res.json({ embed_url: session.embed_url });
Every POST endpoint in the spuke API accepts an Idempotency-Key header. Use it to safely retry a request after a network error without creating duplicate resources.
How it works
The uniqueness scope is (merchant_id, method, path, Idempotency-Key). When the API receives a POST with an Idempotency-Key:
If the key has never been used on this endpoint → the request is processed normally, and its response (status + body) is stored for 24 hours.
If the key was already used with the same request body → the original response is returned verbatim. The endpoint is not re-executed.
If the key was already used with a different request body → the API returns 409 Conflict:
{
"error": {
"type": "idempotency_error",
"code": "idempotency_key_reused",
"message": "Idempotency-Key reused with a different request body"
}
}
Keys expire after 24 hours. After that, the same key can be reused for a new request.
Choosing a good key
Deterministic per business action: order_12345, sub_2026-07-18_001, refund_ch_abc_partial_1.
Do not use timestamps or random UUIDs generated per retry — the whole point is that a retry uses the same key.
1–255 characters, ASCII.
Endpoints that support it
Method
Path
POST
/v1/checkout/sessions
POST
/v1/checkout/sessions/{id}/expire
POST
/v1/subscriptions
POST
/v1/subscriptions/{id}/cancel
POST
/v1/products
POST
/v1/products/{id}
POST
/v1/products/{id}/prices
POST
/v1/products/{id}/default_price
GET and DELETE requests ignore the header — they are already idempotent by definition.
Retries
Combine Idempotency-Key with exponential backoff for 5xx and 429 responses:
Create one-off payments programmatically — the only public payments endpoint today.
Checkout Sessions
A Checkout Session is the primary API endpoint for taking a payment. You create it from your server, redirect the customer to checkout_url, and receive a webhook when it's paid.
Base URL: https://api.spuke.com/v1
Auth: Authorization: Bearer sk_live_… (or sk_test_…)
line_items[].price_id is optional — pass it when the item comes from a product you created in the dashboard, so it appears in receipts and analytics with its catalog link. The actual amount charged is amount — the price is not fetched from the catalog.
Fees are calculated in EUR on your active plan and deducted from the merchant's balance in the transaction currency (converted at live FX). See Fees, currency & minimum amounts.
The Payment object returned by the API and delivered on webhooks.
Payments
A Payment represents a single money movement from a customer to a merchant. It is created automatically when a Checkout Session completes or an Invoice is paid — you don't create it directly.
Fetch the parent session/invoice — the payment is embedded in its response:
GET /v1/checkout/sessions/{id}
A dedicated GET /v1/payments/{id} endpoint is planned for the next API release.
Currency & fees
Every fee is computed in EUR against your plan's rate for the actual card region and payment method, then converted back to the charge currency. See Fees, currency & minimum amounts.
How invoices work in spuke, how customers pay them, and the roadmap for the invoice API.
Invoices
Invoices in spuke are created in the merchant dashboard (/dashboard/invoices/new). Each invoice generates a public payment link that anyone with the link can pay — no API key needed on the customer side.
A public REST API for create / list / send invoice is on the roadmap. Today you can automate invoice payments through Checkout Sessions and use the dashboard for invoicing.
Anatomy
Field
Description
number
Auto-generated SPK-INV-YYYY-NNNN.
currency
ISO code — locked to the currency of any product line-item you add.
line_items
Free-form or imported from your Products catalog. Product-sourced items are locked to spuke values (price, quantity, VAT, currency).
total_amount
Sum incl. VAT. Minimum 2.50 in invoice currency (below that, PSP + spuke fees can't be covered).
status
draft → open → paid / void / uncollectible.
public_token
Random token used in the customer-facing URL.
Customer payment flow
Merchant creates the invoice and clicks Send — status becomes open, public_token is minted.
Customer opens https://payments.spuke.com/i/{public_token} (link is emailed automatically).
The page calls the internal function invoice-checkout with { action: "create_pi", token }, which:
creates a PaymentIntent as a destination charge on the merchant's connected account,
resolves enabled payment methods for the invoice currency (Klarna, iDEAL, Bancontact, etc. are filtered per currency),
applies the correct application-fee amount via the shared fee engine.
On success the invoice is marked paid, a receipt is emailed and a invoice.paid webhook is delivered to your endpoints.
Adding products to an invoice
In the New invoice page, click Add from products → pick a product. spuke will:
copy name, description, image, price, currency, VAT from your catalog;
lock those fields (marked "From product · locked to spuke values");
lock the invoice currency to the product currency;
disable recurring products (subscriptions are API-only, coming soon).
Webhooks
Configure your endpoints in Dashboard → Developers → Webhooks.
Create recurring subscriptions for customers using a recurring price from your catalog. Subscriptions run on your connected spuke account; spuke deducts the platform fee automatically from each renewal invoice.
Base URL: https://api.spuke.com/v1
Auth: Authorization: Bearer sk_live_… (or sk_test_…)
Prerequisites
Create a product and a recurring price (see Products and Prices).
The price must have recurring.interval set (day, week, month, year).
The subscription is created in incomplete state. Use the returned client_secret on the customer's browser to confirm the first payment. Once confirmed the status transitions to active (or trialing if trial_days was set) and a customer.subscription.updated webhook is fired.
Retrieve
GET /v1/subscriptions/{id}
List
GET /v1/subscriptions?limit=20
Returns up to 100 subscriptions ordered by creation date (newest first).
Keep active until the current period ends, then cancel.
at_period_end: false
—
Cancel immediately. No further invoices.
Status model
Status
Meaning
incomplete
Waiting for the first payment confirmation.
incomplete_expired
First payment was not confirmed within 23 h.
trialing
Free trial in progress.
active
Paid and current.
past_due
Renewal failed. Dunning in progress.
unpaid
All retries exhausted, subscription frozen.
canceled
Terminated.
Fees
Recurring invoices carry a platform fee computed from your plan and payment method. Fees are settled in EUR and deducted per invoice — same rules as one-off Checkout Sessions. See Fees, currency & minimum amounts.
Idempotency
Send Idempotency-Key: <your-key> on POST /v1/subscriptions and POST /v1/subscriptions/{id}/cancel. Keys are scoped to (merchant_id, method, path) and expire after 24 hours.
Same key + same body → original response replayed verbatim.
Same key + different body → 409 Conflict with type: "idempotency_error", code: "idempotency_key_reused". See Error reference.
Errors
HTTP
error.type
error.code
Meaning
400
invalid_request
field-specific
Missing / malformed field, or price is not recurring.
Create products in the dashboard or via API — they live directly on your spuke account.
Products
A Product describes something you sell — a shoe, a subscription tier, a service. Products in spuke are created directly on your spuke account — either via the dashboard (Dashboard → Products → New) or via the public API described below.
Fields
Field
Description
name
Public name shown on receipts, invoices and checkout.
description
Long description.
images[]
Up to 8 image URLs. First image is used as the thumbnail.
tax_code
Optional spuke tax code (txcd_…).
unit_label
e.g. "seat", "month".
statement_descriptor
What appears on the buyer's card statement (≤ 22 chars).
url
Link back to your product page.
shippable
Physical goods = true.
package_dimensions
Length / width / height (cm) and weight (g).
metadata
Free-form key/value.
tags[]
spuke-internal — used for catalog auto-grouping.
default_price
The Price object linked as the default.
Minimum price
Every unit_amount must be ≥ 250 minor units (2.50) in the price's currency. Smaller amounts don't cover PSP + spuke fees.
API — Create a product
POST https://api.spuke.com/v1/products
Authenticate with a secret API key (sk_live_… / sk_test_…) — scope products:write. Pass an optional Idempotency-Key header to make retries safe.
The returned prod_… and price_… IDs are what you use everywhere else — catalog management and invoice product selection. Recurring prices define the billing terms, but automatic subscription billing requires a recurring-billing flow.
API — Other product operations
Method
Path
Description
GET
/v1/products
List products (params: limit, starting_after). Scope products:read.
Archive the product and all its prices (active=false).
POST
/v1/products/{id}/default_price
Body { "price": "price_…" } — change the default price.
API — Prices on a product
Method
Path
Description
POST
/v1/products/{id}/prices
Create a new Price on this product. Fields: currency, unit_amount, nickname, recurring, tax_behavior, lookup_key, set_as_default, metadata.
GET
/v1/products/{id}/prices
List all prices on this product.
Prices are immutable except for active, nickname, tax_behavior, lookup_key, metadata. To change amount or currency, create a new Price and mark the old one inactive.
In an invoice: New invoice → Add from products → the item is locked to the product's spuke values.
In a Checkout Session: pass line_items[].price_id: "price_…" so receipts and analytics link back to the catalog entry.
Product ID vs Price ID
ID
What it is
When you use it
prod_…
The container — name, description, images.
Reporting, catalog management.
price_…
The billable price — amount, currency, one-time or recurring.
Every transaction and invoice line-item.
Rule of thumb: to charge someone you always need a price (or an ad-hoc amount), never just a product.
Idempotency
All POST endpoints (/v1/products, /v1/products/{id}, /v1/products/{id}/prices, /v1/products/{id}/default_price) accept Idempotency-Key. Same key + same body replays the original response; same key + different body returns 409 Conflict with type: "idempotency_error", code: "idempotency_key_reused". See Error reference.
A Price represents how much and how often a product costs. Every product needs at least one price to be sellable.
Types
Type
recurring
Use for
one_time
null
Physical goods, single services, invoices.
recurring
{ interval, interval_count }
Subscriptions (API-only, coming soon).
Fields
Field
Description
unit_amount
Amount in minor units (cents). Must be ≥ 250 (= 2.50).
currency
3-letter ISO code, lowercase.
product
The parent prod_… ID.
type
one_time or recurring.
recurring.interval
day, week, month, year.
recurring.interval_count
e.g. 3 → every 3 months.
nickname
Internal label (e.g. "Pro monthly EUR").
active
false archives it — existing subscriptions keep billing.
Currency lock
Once a price exists, its currency is immutable (spuke rule). To sell in another currency, create a second price for the same product with a different currency.
Recurring prices in invoices
Recurring products are disabled in the invoice UI — invoices are one-shot documents. To bill on a schedule, use the upcoming Subscriptions API (/v1/subscriptions, currently in design).
Creating a price
Prices are created automatically when you create a product from the dashboard. To add additional prices to an existing product, use Dashboard → Products → {product} → Add price (coming soon) or call spuke directly on your connected account.
Finding your Price ID
Dashboard → Products — the list shows both the Product ID (prod_…) and the Price ID (price_…) with copy buttons.
The products list has a category dropdown that filters by the category's tag. Pagination kicks in at 20 products.
API
Not exposed publicly. All catalog operations go through the dashboard function merchant-products (list_categories, create_category, update_category, delete_category, update_product_tags).