---
title: "Hosted Payments"
description: "Embed Spark CRM's secure card form on your checkout pages and charge with single-use payment tokens."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.sparkcrm.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Hosted Payments

Hosted Payments lets you collect card details on **your** site without putting card data on your servers. Customers type into a secure iframe served by Spark CRM; you receive a single-use **token** and pass it to the Checkout API to create the order.

**Navigation**: Settings > Hosted Payments

---

## How It Works

```
Your checkout page
  ↓  loads spark-secure.js + mounts iframe
Secure card form  (secure.sparkcrm.io)
  ↓  customer enters card → createToken()
Single-use token  (UUID)
  ↓  your server
POST /checkout/orders   payment: { method: "token", token }
  ↓
Order charged  (token attached as card-on-file, then burned on success)
```

Card number and CVV never touch your page or your backend. Your servers only ever see the opaque token and masked card metadata (brand, last four, etc.).

---

## Prerequisites

1. An active Spark CRM account with a payment gateway (or sandbox) on your campaign
2. An [API token](/settings/api-tokens) with permission to create orders (`api:orders.create`)
3. A campaign + offer to charge against

---

## Settings

**Who can change these settings**: regenerating the publishable key and adding or removing allowed origins require the team owner, the Account Admin or Account Manager role, or any role granted the `gateway:update` permission; demo accounts cannot do either. Other team members can still open **Settings > Hosted Payments** and copy the publishable key, but the **Regenerate Key** button, the **Add Origin** form and the remove buttons are hidden for them.

### Publishable key

Your **publishable key** (`pk_…`) identifies your team to the hosted form. It is safe to put in frontend HTML — it grants **no** API access.

- Copy it from **Settings > Hosted Payments**
- If you regenerate the key, every embedded form using the old key stops loading immediately

### Allowed origins

List the exact browser origins of pages that embed the form (for example `https://shop.example.com` or `http://127.0.0.1:8080` for local demos).

- Use scheme + host (+ port if non-default). No paths or query strings.
- An empty list means **any** site can embed the form — add your real checkout origins in production.

---

## Embedding the Card Form

### 1. Include the script

Use the script URL shown on the Hosted Payments settings page (CDN in production, or the secure domain in local development):

```html
<script src="https://cdn.sparkcrm.io/js/v1/spark-secure.js"></script>

<div id="card-element"></div>
<button id="pay" type="button">Pay</button>
```

### 2. Initialize and tokenize

```html
<script>
  SparkSecure.init({
key: 'pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
container: '#card-element',
styles: {
  fontSize: '16px',
  borderColor: '#d1d5db',
  focusBorderColor: '#2563eb',
},
onReady: () => console.log('card form ready'),
onError: (msg) => console.warn(msg),
onValidation: (errors) => console.log(errors),
  });

  document.getElementById('pay').addEventListener('click', async () => {
try {
  const { token, card } = await SparkSecure.createToken();
  // Send token to YOUR server — never call the order API from the browser
  // with a secret API token.
  await fetch('/your-server/submit-order', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token, card }),
  });
} catch (err) {
  // Structured: { code: 'validation', errors } | { code: 'error', message }
  // | { code: 'timeout' }. Local usage failures reject with a plain Error,
  // so test for err.code before branching on it.
  console.error(err);
}
  });
</script>
```

### SparkSecure API

| Method | Description |
|--------|-------------|
| `init({ key, container, styles?, onReady?, onToken?, onValidation?, onError? })` | Mounts the secure iframe into `container` (CSS selector or element). |
| `createToken()` | Returns a Promise that resolves to `{ token, card }`. Validation, gateway and timeout failures reject with a structured `{ code, ... }` object; local usage failures (`init()` not called, the form failed to mount or is not ready yet, a tokenization already in flight, `destroy()` called mid-request) reject with a plain `Error` and no `code` — check `err.code` exists before branching on it. |
| `destroy()` | Removes the iframe and message listener. Call before unmounting the page. |

Optional `secureOrigin` overrides where the iframe loads from (local / same-origin testing only).

---

## Styling the Form

Pass a `styles` object to `init()`. These keys are applied (values are sanitized; any other key is ignored):

| Style key | Controls |
|-----------|----------|
| `fontFamily` | Input font family |
| `fontSize` | Input font size |
| `borderRadius` | Input corner radius |
| `borderColor` | Default border |
| `focusBorderColor` | Focused border |
| `backgroundColor` | Input fill only |
| `textColor` | Input text |
| `errorColor` | Validation errors |
| `labelColor` | Field labels |
| `placeholderColor` | Placeholders |
| `inputPadding` | Input padding |
| `inputHeight` | Input height |
| `labelFontSize` | Field label font size |
| `labelFontWeight` | Field label font weight |
| `labelDisplay` | Show or hide field labels |
| `fieldGap` | Space between fields |

The iframe itself is always transparent. To tint the area behind the fields, style your own container (e.g. `#card-element { background: #fafafa; }`).

---

## Charging with the Token

From **your server**, call the Checkout API with a team API token (never expose that secret in the browser).

```json
{
  "customer": {
"email": "customer@example.com",
"first_name": "Jane",
"last_name": "Doe"
  },
  "shipping": {
"address1": "123 Main St",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US"
  },
  "campaign_id": "YOUR_CAMPAIGN_INTERNAL_ID",
  "currency": "USD",
  "products": [
{ "offer_id": "YOUR_OFFER_INTERNAL_ID", "quantity": 1 }
  ],
  "payment": {
"method": "token",
"token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  }
}
```

```bash
curl -X POST "https://api.sparkcrm.io/checkout/orders" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d @order.json
```

The same `payment: { method: "token", token }` shape works on:

- `POST /checkout/orders` (create full order)
- `POST /checkout/orders/payment` (process payment on an existing lead/order)

See [Basic DTC Checkout Flow](/integrations/dtc-checkout-flow) for the broader lead → payment → upsell sequence.

---

## Token Lifecycle

| Stage | Behavior |
|-------|----------|
| **Created** | `/v1/tokenize` returns a UUID valid for **30 minutes**, not yet tied to a customer |
| **Redeemed** | Order API attaches the card to the customer and makes it the customer's primary payment method — future subscription rebills will use it — then charges it. The primary flip happens at redemption, so it stands even if that charge declines. |
| **Burned** | On successful charge (`completed` / `authorized` / pending 3DS redirect), the token cannot be reused |
| **Decline** | Token stays valid until expiry so the customer can retry |
| **Expired** | Unused tokens are pruned automatically |

Treat tokens as **single-use secrets** for your server only — never log them in client analytics.

---

## Built-in Form UX

The hosted form includes Stripe-like behavior out of the box:

- Live card brand icons (Visa, Mastercard, Amex, Discover, Diners, JCB)
- Brand-aware PAN length and CVV length (Amex CID is 4 digits)
- Auto-advance between number → expiry → CVV; backspace walks back
- Expiry: typing `3` becomes `03`; live “card has expired” validation
- Field-level validation surfaced to `onValidation` / `createToken()` rejection

---

## Troubleshooting

| Symptom | What to check |
|---------|----------------|
| Blank form / `onError` after ~15s | Publishable key regenerated? Team suspended? Origin not allowed? |
| `createToken` “not ready” | Wait for `onReady`, or form failed to mount. This path rejects with a plain `Error` — no `code` property |
| `onError` / `{ code: "error" }` saying “Too many attempts. Please try again later.” | Tokenization is rate limited to 20 attempts per 10 minutes per team + IP, plus 60 requests per minute per IP. Expect it during load testing or from shared/CGNAT addresses; wait out the window and retry |
| `422` invalid / already used token | Token already charged, wrong team, or not a UUID |
| `422` payment token expired | Customer took longer than 30 minutes — tokenize again |
| CORS / API errors from the browser | Order API must be called from **your server** with the secret API token |

---

## Security Notes

- **Publishable key** (`pk_…`) → frontend only
- **API token** (Sanctum Bearer) → backend only
- Card PAN/CVV exist only inside the secure iframe and encrypted storage
- Prefer locking **Allowed origins** to your real storefront hosts

---

## Related

- [API Tokens](/settings/api-tokens) — create the secret token used by your server
- [Basic DTC Checkout Flow](/integrations/dtc-checkout-flow) — full checkout sequence
- [Payment Gateways](/payment-processing/gateways) — configure processors for the campaign

Source: https://docs.sparkcrm.io/settings/hosted-payments/index.mdx
