SpicePay

Payments

The payment lifecycle and the three ways to integrate checkout.

A payment in SpicePay represents one purchase intent. Whichever connector processes it, the payment is the stable object your systems track.

Lifecycle

Payment lifecycle A payment moves from requires_payment_method to requires_confirmation to processing to succeeded. From processing it can branch to requires_customer_action and back, or to a terminal failed, cancelled, or expired state. requires_payment_method requires_confirmation processing succeeded requires_customer_action failed / cancelled / expired
Status Meaning
requires_payment_method Payment created, waiting for the customer to choose/enter a method
requires_confirmation Method attached, waiting for confirm
requires_customer_action Customer must complete an action (e.g. 3DS challenge, wallet or provider redirect)
processing With the processor, result pending (crypto payments sit here during confirmations)
succeeded Payment completed — refunds and disputes can now reference it
failed The processor declined or errored; can be retried with a new attempt
cancelled / expired Voided by you, or timed out before completion

The timeline in the Control Center records more than status transitions. When a customer uses a native pane it also carries native_pane_selected, native_pane_tab_opened, native_pane_tab_blocked and native_pane_abandoned. None of these move the payment — a customer who picks Apple Pay, comes back, and pays with card stays payable throughout; the trail just makes that visible to support.

Attaching a customer (optional)

You don't have to create a customer first. Pass customer details on the create call and SpicePay creates the customer record — or reuses the existing one — automatically:

  • customer_id — attach an existing customer.
  • customer — an object with id, name, email, phone, phone_country_code (all optional). If the id already exists the record is reused; otherwise it's created with the details you provide.

Either way, the payment shows up on the customer's history and their saved payment methods become available at checkout. See Customers.

Ways to accept a payment

All three integration methods use the same backend API call to create a payment — they differ only in how the customer pays.

1. Hosted Checkout

Create the payment on your server, then redirect the customer to the hosted checkout:

curl -X POST 'https://api.spicepay.net/payments' \
  -H 'api-key: YOUR_SECRET_KEY' \
  -H 'Content-Type: application/json' \
  -d @body.json
{
  "amount": 1000,
  "currency": "USD",
  "profile_id": "YOUR_SHOP_ID",
  "payment_link": true,
  "customer": {
    "email": "ada@example.com",
    "name": "Ada Lovelace"
  },
  "return_url": "https://your-shop.example/return",
  "metadata": { "order_id": "1001" },
  "test_mode": false
}

metadata is the join back to your own records — webhooks carry it, so your order id belongs here. test_mode picks the environment: set it from your app's own environment and a staging deploy can never take a real card. Omit it and the processor's setting decides. See Environments.

The response includes the payment_id and merchant_id. Send the customer to:

https://checkout.spicepay.net/pay/{merchant_id}/{payment_id}

The hosted page handles payment input, 3DS, and redirects to your return_url on completion. Checkout branding — logo, colors, typography — is configured per shop in the Control Center.

2. Iframe Embed

Same create call as Hosted Checkout — then embed the checkout inside your own page instead of redirecting:

<iframe
  src="https://checkout.spicepay.net/pay/{merchant_id}/{payment_id}"
  width="100%"
  height="640"
  frameborder="0"
  allow="payment"
></iframe>

Add the parent origin (e.g. https://your-shop.example) to your shop's Iframe allowed origins list in the Control Center — the checkout page sends a frame-ancestors CSP header derived from it, so embedding from any origin not on the list is blocked by the browser.

Wallets inside an iframe

Stripe only offers Apple Pay, Google Pay and Link when the top-level page's domain is registered as a payment method domain on your Stripe account. Inside an iframe that page is your shop, not the SpicePay checkout — so Stripe hides them, silently and without an error.

Native panes solve it. Mark a payment method as a native pane on your Stripe connector (see Connectors) and the embedded checkout draws its own tile for it; clicking that tile opens the SpicePay checkout in a new tab, at the top level, showing only that method. Nothing extra to register, and it works the same for a non-wallet method such as Klarna or iDEAL.

The new tab is opened with window.open, so your embedding page must permit pop-ups:

<iframe
  src="https://checkout.spicepay.net/pay/{merchant_id}/{payment_id}"
  allow="payment *"
  sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation"
></iframe>

A sandbox attribute without allow-popups blocks the tab. The checkout degrades to a plain link the customer can click rather than a dead tile, and the blocked pop-up is recorded on the payment's timeline.

While the tab is open the iframe polls the payment for up to 20 minutes. When the payment settles (succeeded, requires_capture for a manual-capture authorization, cancelled, …) it reloads to the result screen; a failed attempt instead returns the customer to the method tiles to retry (a failed status message is informational — a later succeeded supersedes it). After 20 minutes it asks the customer to refresh instead. If your origin is on the shop's Iframe allowed origins list it also posts a message to your page:

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://checkout.spicepay.net') return;
  const data = event.data;
  if (data?.source !== 'spicepay-checkout' || data.type !== 'payment_status') return;
  // data.payment_id, data.status
});

Treat it as a UI hint — the webhook remains the source of truth.

Single-method links

Append ?pane={method} to the checkout URL to open the checkout on a single payment method. A method configured as a native pane gets the focused one-button view — this is what a native pane tile opens. Otherwise the checkout is pinned to that method: card, paypal, crypto_currency, or any method from a local-methods catalog by its key or vendor code. Methods that exist only as a tab inside Stripe's Payment Element (iDEAL, Bancontact, P24 and the like, unless you promoted them to a native pane) cannot be isolated — Stripe owns that surface — and such a link shows a "Show all payment methods" notice instead. You can put the same link behind your own button:

https://checkout.spicepay.net/pay/{merchant_id}/{payment_id}?pane=apple_pay

Open it at the top level — a new tab, or a full-page navigation — and call window.open synchronously inside the click handler or the pop-up blocker eats it. A method that isn't available on the payment at all shows a notice with a visible "Show all payment methods" action, so a stale link is never a dead end.

3. Build your own checkout

No hosted page, no iframe: create and confirm the payment in one call, passing the payment method data yourself and pinning a specific connector via routing. The call is the same for every provider — only payment_method, payment_method_type, payment_method_data, and the pinned connector change. Pick a connector to see its exact request:

curl -X POST 'https://api.spicepay.net/payments' \
  -H 'api-key: YOUR_SECRET_KEY' \
  -H 'Content-Type: application/json' \
  -d @body.json
{
  "amount": 1000,
  "currency": "USD",
  "profile_id": "YOUR_SHOP_ID",
  "confirm": true,
  "capture_method": "automatic",
  "return_url": "https://your-shop.example/return",
  "payment_method": "wallet",
  "payment_method_type": "paypal",
  "payment_method_data": {
    "wallet": {
      "paypal_redirect": {}
    }
  },
  "routing": {
    "type": "single",
    "data": {
      "connector": "paypal"
    }
  }
}
curl -X POST 'https://api.spicepay.net/payments' \
  -H 'api-key: YOUR_SECRET_KEY' \
  -H 'Content-Type: application/json' \
  -d @body.json
{
  "amount": 1000,
  "currency": "USD",
  "profile_id": "YOUR_SHOP_ID",
  "confirm": true,
  "capture_method": "automatic",
  "return_url": "https://your-shop.example/return",
  "payment_method": "crypto",
  "payment_method_type": "crypto_currency",
  "payment_method_data": {
    "crypto": {}
  },
  "routing": {
    "type": "single",
    "data": {
      "connector": "nowpayments"
    }
  }
}
curl -X POST 'https://api.spicepay.net/payments' \
  -H 'api-key: YOUR_SECRET_KEY' \
  -H 'Content-Type: application/json' \
  -d @body.json
{
  "amount": 1000,
  "currency": "USD",
  "profile_id": "YOUR_SHOP_ID",
  "confirm": true,
  "capture_method": "automatic",
  "return_url": "https://your-shop.example/return",
  "payment_method": "crypto",
  "payment_method_type": "crypto_currency",
  "payment_method_data": {
    "crypto": {}
  },
  "routing": {
    "type": "single",
    "data": {
      "connector": "cryptomus"
    }
  }
}

For redirect-based methods the response has status requires_customer_action and a next_action.redirect_to_url pointing at the provider's page. Send the shopper there with a top-level navigation (not an iframe — providers like PayPal block framing):

window.location.href = payment.next_action.redirect_to_url;

The provider returns the shopper to your return_url. The selected connector must be enabled on the shop. Crypto works the same way with "payment_method": "crypto" and "payment_method_type": "crypto_currency".

No code: WordPress

Running a WordPress site? The SpicePay plugin adds hosted checkout to your store — products, orders, and refunds are managed in the WordPress admin, with no API calls to write.

Idempotency

Pass your own payment_id when creating a payment — a retried create with the same ID returns the existing payment instead of charging twice.

Related