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
- An active Spark CRM account with a payment gateway (or sandbox) on your campaign
- An API token with permission to create orders (
api:orders.create) - 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):
<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
<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).
{
"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"
}
}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.jsonThe 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 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
3becomes03; 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 — create the secret token used by your server
- Basic DTC Checkout Flow — full checkout sequence
- Payment Gateways — configure processors for the campaign

