Skip to content

Hosted Payments

Embed Spark CRM's secure card form on your checkout pages and charge with single-use payment tokens.

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 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):

<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.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 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

Navigation

Type to search…

↑↓ navigate↵ selectEsc close