# build: 2026-05-22T23:16:34.326Z This is the full developer documentation for Swft # Swft Docs > Edge-rendered checkout. Headless cart. Partner-first API. A booking checkout in 12 lines ```ts import { Swft } from '@swft-checkout/js' const swft = new Swft({ apiKey: merchant.api_key }) const session = await swft.sessions.create({ cart: { items: [{ id: 'class:reformer-100', name: '60-min Reformer', quantity: 1, price: 2500 }], extensions: { booking: { class_schedule_id: 'cs_8f...', user_id: 'usr_31...', studio_id: 'stu_4d...', payment_type: 'single', }, }, }, }) window.location.href = session.sessionUrl ``` ## Why teams pick Swft [Section titled “Why teams pick Swft”](#why-teams-pick-swft) 164 ms TTFB Checkout loads in under 200 ms globally via Cloudflare’s edge network. Sessions pre-create on cart update — zero API round-trip on redirect. WordPress optional Drop the plugin in WooCommerce and Swft handles every checkout. Or skip WordPress entirely — Next.js, Medusa, and vanilla JS adapters ship in the box. Modules, not monolith Dunning, KYC, gift cards, fraud rules, bundles, donations, subscriptions. Each module is opt-in and configurable from the dashboard. Partner-first API Onboard your own merchants. Stripe Connect, custom domains, pre-confirmation hooks, Standard Webhooks. One bearer token, one OpenAPI spec. ## Popular guides [Section titled “Popular guides”](#popular-guides) [Installation ](/getting-started/installation/)Get the plugin running on WooCommerce in under 10 minutes. [First Checkout ](/getting-started/first-checkout/)Connect Stripe, place a test order, ship the live one. [Connecting Stripe ](/getting-started/connecting-stripe/)Standard Connect, application fees, on\_behalf\_of, the lot. [Templates ](/swft-checkout/templates/)Five drop-in templates, all editable in the dashboard. [Custom Domains ](/swft-checkout/custom-domains/)Serve your checkout from \`checkout.yourstore.com\`. [Webhooks ](/integrations/webhooks/)Standard Webhooks signing, delivery log, manual replay. ## For partners [Section titled “For partners”](#for-partners) The v2 Partner API lets a SaaS platform onboard its own customers as Swft merchants, drive checkout sessions on their behalf, and receive lifecycle webhooks. [Authentication ](/partners/01-authentication/)Opaque bearer tokens, scopes, sandbox vs livemode, idempotency. [Merchant Onboarding ](/partners/03-merchant-onboarding/)Bundled merchant create + Stripe Connect in one call. [Booking Extensions ](/partners/04-booking-extensions/)Opaque metadata preserved end-to-end through every event. [Pre-confirmation Hook ](/partners/05-pre-confirmation/)Capacity checks fire before Stripe is charged — no refunds. [Webhooks ](/partners/06-webhooks/)Standard Webhooks signing, retry, replay, inspect. [Unstack × Swft ](/examples/unstack-pilates/)Worked Pilates-booking example on Next.js + Supabase. ## Discoverable to AI tools [Section titled “Discoverable to AI tools”](#discoverable-to-ai-tools) The full docs and partner API spec are available as `llms.txt` so any LLM-backed IDE can ingest the surface in one fetch. * [`/llms.txt`](https://docs.swft.co.uk/llms.txt) — concise index * [`/llms-full.txt`](https://docs.swft.co.uk/llms-full.txt) — every page in one file * [`/openapi.json`](https://docs.swft.co.uk/api/) — the OpenAPI 3.1 spec for the partner API # Page not found > That page doesn't exist (or we moved it during the docs rebuild). ## Common destinations [Section titled “Common destinations”](#common-destinations) * [Installation](/getting-started/installation/) * [Connecting Stripe](/getting-started/connecting-stripe/) * [Templates](/swft-checkout/templates/) * [Custom Domains](/swft-checkout/custom-domains/) * [Webhooks reference](/integrations/webhooks/) * [Partner API authentication](/partners/01-authentication/) * [Changelog](/changelog/) Still stuck? Search any page (top-right) — Pagefind indexes every word in the docs. # AI Checkout — Merchant Beta Guide Welcome to the Swft AI Checkout pilot. This guide takes you from zero to a working AI checkout in about 30 minutes. ## What you’re getting [Section titled “What you’re getting”](#what-youre-getting) Three modes, all served from a single iframe overlay on your store: * **Mode A — Conversational checkout.** AI replaces the form-based checkout with a friendly chat. Customer types email, picks shipping, pays. No catalog or policy knowledge needed. **Always on.** * **Mode B — Sales assistant.** AI answers product questions (“does this come in blue?”) using your WooCommerce catalog. Cites products and renders cards inline. **Opt-in, costs \~$0.001/question.** * **Mode C — Objection handler.** AI answers concerns (“how long is shipping?”) using your policy pages and any custom knowledge you add. Always cites the source. **Opt-in, costs \~$0.005/objection.** ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * WooCommerce store with the Swft Checkout plugin installed and connected (you should already be using standard Swft checkout) * Your Swft API key (find it in the dashboard sidebar or `wp-admin → Settings → Swft`) * Stripe Connect onboarded for live payments ## 1. Enable AI Checkout [Section titled “1. Enable AI Checkout”](#1-enable-ai-checkout) 1. Open your Swft Dashboard at 2. Click **AI Checkout** in the sidebar 3. Open the **Settings** tab 4. Set **Monthly cap** to your desired ceiling (default $50, change anytime) 5. Set **Alert email** to where you want the 80%-threshold warning sent 6. Leave **Mode B** and **Mode C** off for now — you’ll turn them on after step 3. ## 2. Verify Mode A is live [Section titled “2. Verify Mode A is live”](#2-verify-mode-a-is-live) Mode A is on automatically once your monthly cap is greater than 0. 1. Open any product page on your store in an incognito window 2. Look for the **“Buy via AI”** button next to “Add to cart” 3. Click it — the chat should open in a slide-out panel 4. Walk through: email → address → shipping → payment 5. Use Stripe’s test card `4242 4242 4242 4242` (any future date / any CVC) 6. Verify the order appears in WooCommerce as normal If the button doesn’t appear, check your dashboard’s `Settings → Monthly cap` is greater than 0 and `swft_api_key` is configured. ## 3. Sync your catalog (Mode B) [Section titled “3. Sync your catalog (Mode B)”](#3-sync-your-catalog-mode-b) Mode B needs your products embedded into the AI’s knowledge base. 1. Trigger the bulk catalog scan from the WP admin: ```plaintext POST https://yourstore.com/wp-admin/admin-ajax.php?action=swft_ai_chat_catalog_scan&cursor=0&page_size=200 ``` (Or use a tool like Postman; you’ll need a logged-in admin cookie.) 2. Iterate `cursor` until the response shows `next_cursor: null` 3. Confirm via the dashboard `Knowledge` tab — chunk count should match approximately your published product count Subsequent product saves auto-sync. No further action needed. ## 4. Enable Mode B [Section titled “4. Enable Mode B”](#4-enable-mode-b) 1. Dashboard → AI Checkout → Settings → toggle **Mode B** 2. Save 3. Test: open a product page, click “Buy via AI”, and ask a question about another product (“do you sell anything for hiking?”) 4. The AI should answer with product cards from your catalog. Verify the answer doesn’t quote prices (prices live on the cards, not in the answer text). ## 5. Sync your policies (Mode C) [Section titled “5. Sync your policies (Mode C)”](#5-sync-your-policies-mode-c) Mode C grounds objection answers in your real policy pages. 1. Make sure your policy pages have one of these slugs: `shipping`, `returns`, `refund`, `faq`, `sizing`, `delivery`, `policy` (e.g. `/shipping-policy`, `/returns`, `/faq`) 2. Save the page in WP — the plugin will auto-sync it 3. For custom knowledge that isn’t in a page (e.g. “We’re a UK family business since 1994”), use the dashboard’s `Knowledge → Manual entries` panel 4. Confirm via `Knowledge` tab — “Other chunks (policies, FAQ)” should be > 0 ## 6. Enable Mode C [Section titled “6. Enable Mode C”](#6-enable-mode-c) 1. Dashboard → AI Checkout → Settings → toggle **Mode C** 2. Save 3. Test: open the chat, ask “how long is shipping?” — the AI should answer with a citation linking to your shipping policy page ## 7. Watch the dashboard [Section titled “7. Watch the dashboard”](#7-watch-the-dashboard) For the first week of beta, check daily: * **Overview** tab — completed orders, conversion rate, drop-off chart * **Transcripts** tab — review at least 5 chats per day. Look for: * Hallucinated product info (Mode B) * Uncited or fabricated policy answers (Mode C) * Customers giving up at a specific step * **Cost** in the Settings tab — make sure your spend trajectory is sustainable ## What to do if things go wrong [Section titled “What to do if things go wrong”](#what-to-do-if-things-go-wrong) | Symptom | Likely cause | Action | | ----------------------------------------------------- | ------------------------------------------ | --------------------------------------------------- | | ”Buy via AI” button missing | Monthly cap = 0, or Swft plugin disabled | Check dashboard Settings + plugin | | Chat opens but errors immediately | API unreachable / merchant API key invalid | Hit the health endpoint (see below) | | Mode B says “I don’t have a match” for known products | Catalog not synced | Re-run the bulk scan | | Mode C makes up policy details | Policy pages not synced | Verify the page slugs match the auto-extract list | | Spend climbing too fast | LLM-heavy traffic | Lower the monthly cap or set Mode B/C off | | Customers complaining about chat quality | Eval suite below threshold | Email with transcript IDs | ## Health check [Section titled “Health check”](#health-check) Hit the health endpoint to see the system’s view of your store: ```bash curl -H "x-swft-api-key: YOUR_KEY" https://api.swft.co.uk/health/ai-chat ``` Returns JSON with LLM/embedding status, knowledge stats, mode toggles, and current cost vs cap. Useful for debugging. ## What’s not in the beta [Section titled “What’s not in the beta”](#whats-not-in-the-beta) The following are explicitly out of scope and may break or no-op: * Voice mode (mic icon greyed) * Cross-merchant identity (“Swft Buyer Profile”) * Helpdesk integrations (Zendesk, Helpscout) * File uploads to KB (PDF/docx) * URL-list KB ingestion * Branding (accent colour, intro message) * Mid-chat add-to-cart from product cards (today they’re informational only) These ship after beta. ## Feedback [Section titled “Feedback”](#feedback) Email with: * Your store URL * Approximate dates of the issue * Specific transcript IDs (visible in dashboard `Transcripts` tab) — we can look them up directly Your transcripts are already auto-stored for 30 days; we don’t need screenshots. # AI Checkout — Operational Runbook For Swft engineers responding to AI Checkout incidents. ## Quick health check [Section titled “Quick health check”](#quick-health-check) Per-merchant: `GET https://api.swft.co.uk/health/ai-chat` with `x-swft-api-key`. Returns: * `status`: `ok | degraded | paused` * LLM + embedding configuration (just the booleans, not the keys) * Knowledge stats * Monthly cost vs cap ## Common issues [Section titled “Common issues”](#common-issues) ### ”AI Checkout silently doesn’t work for merchant X” [Section titled “”AI Checkout silently doesn’t work for merchant X””](#ai-checkout-silently-doesnt-work-for-merchant-x) 1. Hit health endpoint with their API key 2. Check `cost.monthly_cap_cents`. If 0 → merchant explicitly disabled. If `pct_used >= 1` → cap reached for the month. 3. Check `llm.configured` and `embeddings.configured`. If false → env var missing on Railway. 4. Check `knowledge.product_chunks` and `total_chunks`. If 0 and merchant enabled Mode B/C → catalog/policy sync hasn’t run. ### ”Customer reports a hallucinated answer” [Section titled “”Customer reports a hallucinated answer””](#customer-reports-a-hallucinated-answer) 1. Find the chat in `ai_chat_sessions` (via dashboard `Transcripts` tab, filter by date). 2. Check `messages` for the offending answer. 3. Check `mode_b_detour` / `mode_c_detour` events for that `chat_id` — `props.results_count` shows what the search returned. 4. If `results_count: 0`, the AI fell through to the “I don’t have a match” fallback. Customer concerns are likely about a product/policy that isn’t synced. 5. If `results_count > 0`, the AI may have hallucinated despite citations. Forward to ML team with the transcript ID. ### ”Stripe webhook not flipping chat to complete” [Section titled “”Stripe webhook not flipping chat to complete””](#stripe-webhook-not-flipping-chat-to-complete) 1. Find the chat by `payment_intent_id`: ```sql select * from ai_chat_sessions where payment_intent_id = 'pi_xxx'; ``` 2. Check the webhook logs (Railway → API service). Look for `"AI chat ${id} completed via PI ${pi.id}"`. 3. If not present, check the PI metadata — `swft_chat_id` must be set, otherwise the AI chat handler doesn’t fire. ### ”Cost spiking unexpectedly” [Section titled “”Cost spiking unexpectedly””](#cost-spiking-unexpectedly) 1. Hit health endpoint, note `cost.monthly_spend_cents`. 2. Query `ai_chat_events` for the merchant grouped by `event_type`: ```sql -- running_cost_cents is cumulative per chat — use count() for volume, -- and sum on ai_chat_sessions.llm_cost_cents for actual dollars. select event_type, count(*) as events from ai_chat_events where merchant_id = 'm-xxx' and ts > now() - interval '24 hours' group by event_type order by events desc; ``` 3. If `mode_c_detour` is high, Sonnet is the culprit (5× cost). Disable Mode C if needed. 4. If `state_transition` is high, narration cost. Mode A defaults are cheap; if elevated, check for runaway loops. ### ”Plugin not auto-syncing products” [Section titled “”Plugin not auto-syncing products””](#plugin-not-auto-syncing-products) 1. WordPress error log on the merchant’s host 2. Check `swft_enabled` and `swft_api_key` options 3. Check `swft_ai_chat_enabled` option 4. Try a manual save on a product — should fire `woocommerce_update_product` hook 5. Check Cloudflare API logs for the merchant’s `POST /merchants/ai-chat/knowledge/products` requests ## Rate limits to be aware of [Section titled “Rate limits to be aware of”](#rate-limits-to-be-aware-of) * Anthropic: tier-1 default 50 RPM for Haiku. We’re not close in practice but a viral product could spike. * OpenAI embeddings: tier-1 default 3,000 RPM. Bulk catalog scans throttle to 20 RPS. * Stripe webhooks: at-least-once delivery. Idempotency is enforced via the `current_state === 'complete'` early-return in `webhooks.ts`. ## Eval gates before releases [Section titled “Eval gates before releases”](#eval-gates-before-releases) Before any production release that touches LLM-adjacent code: ```bash cd api ANTHROPIC_API_KEY=sk-ant-... npm run eval:classifier ``` Should report ≥85% accuracy. Below that → block release, investigate prompt drift. ```bash OPENAI_API_KEY=sk-... EVAL_MERCHANT_ID= npm run eval:retrieval-products OPENAI_API_KEY=sk-... EVAL_MERCHANT_ID= npm run eval:retrieval-policies ``` Should both report ≥80% Recall\@5. Below that → check embedding model version + fixture data. ## Killswitch [Section titled “Killswitch”](#killswitch) Per-merchant: set `merchants.ai_chat_monthly_cap_cents = 0`. Takes effect on the next request (no cache invalidation needed). Globally: pause API service on Railway. Customers fall back to standard Swft checkout via the plugin’s 402 handler. ## Useful queries [Section titled “Useful queries”](#useful-queries) ```sql -- Top 10 merchants by AI Checkout spend this month select merchant_id, sum(llm_cost_cents)/100.0 as spend_dollars from ai_chat_sessions where created_at >= date_trunc('month', now()) group by merchant_id order by spend_dollars desc limit 10; -- Merchants approaching their cap (>80%) select m.id, m.name, m.ai_chat_monthly_cap_cents as cap_cents, coalesce(sum(s.llm_cost_cents), 0) as spend_cents, round(100.0 * coalesce(sum(s.llm_cost_cents), 0) / nullif(m.ai_chat_monthly_cap_cents, 0)) as pct from merchants m left join ai_chat_sessions s on s.merchant_id = m.id and s.created_at >= date_trunc('month', now()) group by m.id, m.name, m.ai_chat_monthly_cap_cents having coalesce(sum(s.llm_cost_cents), 0) > 0.8 * m.ai_chat_monthly_cap_cents order by pct desc; -- Today's funnel for merchant X select current_state, count(*) from ai_chat_sessions where merchant_id = 'm-xxx' and created_at >= current_date group by current_state order by count(*) desc; ``` # Changelog ## Swft Checkout Plugin [Section titled “Swft Checkout Plugin”](#swft-checkout-plugin) ### 1.0.19 [Section titled “1.0.19”](#1019) * Add: Setup wizard — 3-step onboarding (Connect Store, Connect Stripe, Enable) * Add: Custom domain support with AJAX-based provisioning and status checking * Add: Checkout layout selector (minimal, split, express templates) * Add: Store policies URLs (returns, shipping, privacy) shown in checkout policies bar * Add: `swft_session_extensions` data written to WooCommerce order as `_swft_ext_{key}` meta fields ### 1.0.18 [Section titled “1.0.18”](#1018) * Add: Customer address lookup — returning customers’ addresses pre-filled from WooCommerce via REST API * Add: Order bump support (`/sessions/:id/apply-bumps` endpoint integration) * Fix: PaymentIntent reuse on retry (Stripe best practice — prevents duplicate charges) ### 1.0.17 [Section titled “1.0.17”](#1017) * Add: Stripe webhook handling for `charge.refunded` — marks WooCommerce order as refunded automatically * Add: Order confirmation email sent after successful payment (Resend API) * Fix: Mixed-currency order sync edge case ### 1.0.16 [Section titled “1.0.16”](#1016) * Add: 3DS redirect return handling — customers redirected back from bank auth land on payment step * Fix: Stripe Elements appearance tokens aligned with dark/light theme CSS variables ### 1.0.15 [Section titled “1.0.15”](#1015) * Add: Express checkout (Apple Pay, Google Pay) via Stripe Payment Request Button * Add: Template system (`minimal`, `split`, `express`) — `TemplateWrapper` component ### 1.0.14 [Section titled “1.0.14”](#1014) * Add: `swftcheckout:ready`, `swftcheckout:details-complete`, `swftcheckout:payment-complete` CustomEvents * Add: `window.SwftCheckout` API — `addOrderRow`, `addCustomField`, `getExtension`, `on`, `off` * Add: `SwftOrderRow` and `SwftCustomField` types with full TypeScript definitions ### 1.0.13 [Section titled “1.0.13”](#1013) * Add: Server-side tracking — `AddPaymentInfo` and `Purchase` events for Meta CAPI, GA4, TikTok Events API * Fix: Tracking events fire-and-forget — never blocks payment response ### 1.0.11 [Section titled “1.0.11”](#1011) * Add: Stripe Connect — OAuth flow, 2% platform fee via `application_fee_amount`, connection status in admin ### 1.0.10 [Section titled “1.0.10”](#1010) * Fix: PHP 7.4 compatibility — replaced `str_contains()` with `strpos()` * Fix: `wp_safe_redirect()` with `allowed_redirect_hosts` filter for `checkout.swft.co.uk` * Fix: `$wpdb->prepare()` for deactivation transient cleanup * Fix: `REQUEST_URI` sanitized with `esc_url_raw()` + `parse_url()` in debug output * Fix: API key masked more aggressively in debug logs * Fix: Text domain corrected to `swft-checkout` * Fix: `WC()->cart` existence check before accessing cart methods * Fix: Cron cleanup on deactivation * Fix: Deactivation now clears debug and fallback logs * Add: `uninstall.php` removes all plugin options cleanly * Add: Privacy policy content disclosure for external API usage * Add: Version tracking via `swft_db_version` option * Add: Fieldset/legend structure for tracking pixel fields (accessibility) * Add: Tested up to WordPress 6.7 ### 1.0.8 [Section titled “1.0.8”](#108) * Add: Meta CAPI, GA4, and TikTok server-side tracking settings in admin ### 1.0.7 [Section titled “1.0.7”](#107) * Fix: Settings page moved to **Settings → Swft Checkout** (no WooCommerce JS dependency) * Add: Admin bar indicator — green “Swft Active” / grey “Swft Off” ### 1.0.6 [Section titled “1.0.6”](#106) * Add: WooCommerce REST API credential fields for order sync * Add: Automatic credential sync to Swft API on settings save * Add: WC credentials status display in admin panel ### 1.0.5 [Section titled “1.0.5”](#105) * Fix: Simple product attributes sent as `{}` (JSON object) not `[]` (array) — Zod validation passes ### 1.0.4 [Section titled “1.0.4”](#104) * Fix: Settings page rewritten using WordPress native Settings API — eliminates JS save issues * Add: Admin bar indicator with link to settings * Add: Settings page at **Settings → Swft Checkout** ### 1.0.3 [Section titled “1.0.3”](#103) * Add: Debug logging toggle with live log viewer in admin * Add: Status panel showing enabled state, API key, version * Fix: Increased API timeout to 10 seconds * Fix: `swft_enabled` guard strictly checks for `"yes"` value ### 1.0.2 [Section titled “1.0.2”](#102) * Fix: Use Railway direct URL while `api.swft.co.uk` custom domain is configured ### 1.0.1 [Section titled “1.0.1”](#101) * Add: HPOS (High-Performance Order Storage) compatibility declaration ### 1.0.0 [Section titled “1.0.0”](#100) * Initial release: cart serialisation, session creation, checkout redirect, transient pre-caching *** ## Swft Cart [Section titled “Swft Cart”](#swft-cart) ### 1.0.0 [Section titled “1.0.0”](#100-1) * Initial release: 18 modules, 8 themes, `window.SwftCart` public API, PHP filter system # Dashboard The Swft Dashboard at [app.swft.co.uk](https://app.swft.co.uk) is where you configure everything: branding, payments, fraud, dunning, A/B tests, custom domains, integrations, analytics, billing. Live KPIs, today's conversion funnel, recent orders with Radar scores. Visual editor for theme, typography, accent colour, layout, and custom CSS. Browse every checkout session — status, amount, fraud score, payment method. Conversion funnel, payment-method breakdown, geographic data, form-field friction. Run multivariate tests across templates, accent colours, and typography. Enable / disable Express Checkout, Stripe Link, BNPL, Local Payments, and more. Manage your plan, payment method, monthly fees, and Swft Offload tier. Brand, credentials, integrations, recovery emails, tracking pixels. Pre-launch checklist: domain verification, test payment, Apple Pay setup. ## Other pages [Section titled “Other pages”](#other-pages) * **Offload** — manage archived orders and tier usage. See the [Swft Offload](/modules/swft-offload) plugin docs. * **Dunning** — failed-payment recovery. See [Dunning](/swft-checkout/dunning). * **Fraud & Risk** — Radar thresholds and flagged-order review. See [Fraud Rules](/swft-checkout/fraud-rules). * **Gift Cards** — issue and manage gift cards. See [Gift Cards](/swft-checkout/gift-cards). * **KYC & Compliance** — B2B verification. See [KYC](/swft-checkout/kyc). * **AI Checkout** — LLM-powered chat config. See [AI Checkout](/swft-checkout/ai-chat). * **Ambassadors** — referral programme. See [Ambassadors](/integrations/ambassadors). * **Wallet Manager** — customer wallet balances and refund management. * **Onboarding** — 4-step setup wizard for new merchants (Stripe, branding, domain, go-live). * **Live Monitor** — real-time stream of orders coming in, with KPIs. * **MCP** — Model Context Protocol server config for AI-agent commerce. See [MCP Server](/integrations/mcp). ## How merchants typically use it [Section titled “How merchants typically use it”](#how-merchants-typically-use-it) **First 30 minutes** — Onboarding wizard. Connect Stripe, upload your logo, pick an accent colour, point your domain. **Week 1** — Watch the Overview and Sessions pages. Confirm orders are arriving. Spot-check a few in the Sessions table to make sure data matches your WooCommerce orders. **Week 2-4** — Open Analytics. Look at the conversion funnel — where are shoppers dropping off? Use the form-field friction heatmap to identify problem fields. **Month 2+** — Start A/B testing. Try a different template, accent colour, or typography. Most stores see 5-15% conversion uplift from template tuning alone. **Ongoing** — Watch Fraud and Dunning weekly. Tune thresholds. Review AI Checkout transcripts (if enabled). # A/B Testing Run controlled experiments on your Swft Checkout — template variations, accent colours, typography, copy — and let real conversion data tell you which design wins. ## What you can test [Section titled “What you can test”](#what-you-can-test) * **Template** — minimal vs split vs fullscreen vs centered vs express. * **Accent colour** — your primary action button colour. * **Typography** — font family for body and headings. * **(Coming soon)** — full custom CSS variants. Each test has a **control** (your current design) and one or more **variants** (changes to test against the control). ## Setting up a test [Section titled “Setting up a test”](#setting-up-a-test) Swft Dashboard → **A/B Testing** → **Create test**. 1. Name the test (e.g. “Split vs Minimal for mobile”). 2. Add 2-5 variants. For each: * Pick the template * Pick the accent colour * Pick the font(s) 3. Set the traffic split. Defaults to 50/50; you can weight asymmetrically (e.g. 10% to a risky variant, 90% to control). 4. Click **Start test**. The test runs against incoming sessions — each shopper is assigned a variant via a cookie and stays on it for the rest of their session. Returning shoppers see the same variant they saw before (we cookie for 30 days). ## Reading the results [Section titled “Reading the results”](#reading-the-results) The test page shows: | Metric | Per variant | | ---------------------------- | ------------------------------------------------ | | **Sessions** | Total checkout sessions on this variant | | **Completed orders** | Successful payments on this variant | | **Conversion rate** | Completed ÷ Sessions | | **Revenue** | Total order value on this variant | | **AOV** | Average order value | | **Statistical significance** | p-value vs control (when sample is large enough) | A green badge appears when one variant beats control with p < 0.05 and at least 250 sessions per variant. ## Stopping & rolling out [Section titled “Stopping & rolling out”](#stopping--rolling-out) When you’re satisfied a variant has won: 1. Click **Stop test**. 2. Pick the winning variant. 3. Click **Roll out to 100%**. The winning variant becomes your new default in the Checkout Editor. All future sessions see the winning design. ## Tips for good tests [Section titled “Tips for good tests”](#tips-for-good-tests) * **Test one thing at a time.** Don’t change template + colour + font + copy all in one variant — you won’t know which change moved the needle. * **Be patient.** You need \~250 sessions per variant for results to be statistically meaningful. For most stores that’s a week of traffic. Resist stopping early because variant B is “up 12%” with 30 sessions. * **Variants should be meaningfully different.** Tweaking the accent from `#D4EF3B` to `#D3EF3C` won’t move conversion. Test changes you’d actually consider deploying. * **Watch for seasonality.** A test during Black Friday is not representative of normal traffic. * **Don’t stop at the first significant result.** Wait until the sample size feels solid. Tests can flip-flop with small samples. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Returning shoppers stick to their variant.** This is by design (otherwise they’d see different designs across sessions, which is jarring). But it means switching tests rapidly can leave some shoppers on stale variants for up to 30 days. Wait for cookies to expire if you’re sensitive to that. * **Mobile vs desktop responds differently.** A test that wins overall may lose on mobile. We surface a device breakdown once your sample is big enough. * **You can run only one active test at a time.** Multi-variant testing across two dimensions is on the roadmap. * **Stripe Connect orders only.** The traffic split system attaches the variant cookie before the gateway choice, so all gateways participate — but reporting is most reliable for Stripe orders because we have the richest per-order data. # Analytics Conversion funnel breakdown, payment-method mix, geographic distribution, and form-field friction analysis. Use it to find drop-off points and tune your checkout. ## Time-range picker [Section titled “Time-range picker”](#time-range-picker) Top-right of the page. Pick **24H / 7D / 30D / 90D**. Every chart below updates to that window. ## Conversion funnel [Section titled “Conversion funnel”](#conversion-funnel) A vertical bar chart with four stages: 1. **Sessions started** — checkout page loaded. 2. **Details complete** — email + address submitted. 3. **Payment attempted** — at least one payment attempt fired. 4. **Order completed** — payment succeeded. Each bar shows the absolute count plus the drop-off percentage from the previous stage. Big drop-offs are where you focus your tuning. Typical healthy patterns: * **>80%** sessions started → details complete (lower than this = address autocomplete or form is friction-y) * **>85%** details complete → payment attempted (lower = the shopper hit the payment step and balked) * **>90%** payment attempted → completed (lower = card decline rate is high; check fraud settings or 3DS issues) ## Payment method breakdown [Section titled “Payment method breakdown”](#payment-method-breakdown) Pie chart + table showing the split of completed orders by payment method: Card (Stripe), PayPal, Klyme, NomuPay, Apple Pay, Google Pay, BNPL, etc. Useful for: * Spotting which gateway your customers actually prefer * Deciding whether to enable / disable a slow-converting method * Verifying that a newly-enabled method is actually being used ## Geographic breakdown [Section titled “Geographic breakdown”](#geographic-breakdown) A list of countries with: * Session count * Order count * Conversion rate * Average order value Click a country to filter the rest of the page. Useful for finding markets that are converting unexpectedly well (or badly). ## Form-field friction [Section titled “Form-field friction”](#form-field-friction) A heatmap showing every field on the checkout details step, with: * Time spent on field (seconds, median) * Edit count (median — high = shoppers correcting typos repeatedly) * Abandon rate (shoppers who reached this field but never moved past it) Red-flag patterns: * **Phone number** with high abandon rate → make it optional. * **Postcode** with high edit count → your address autocomplete might be misbehaving. * **Email** with high time-spent → consider whether your error message is unclear. ## Tips [Section titled “Tips”](#tips) * **Drop-off is fractal.** The big-picture funnel hides micro-funnels. Use the form-field heatmap to find the per-field issues. * **Mobile vs desktop is huge.** If you’re seeing a big drop-off and haven’t filtered by device, you’re missing the most likely cause. (Mobile-specific filter coming soon — for now, use UTM source if you can segment your traffic.) * **The funnel reflects what shoppers do, not why.** Combine quantitative data here with qualitative tools — talk to your customers, run a Hotjar-style session replay, A/B test variants. # Billing Manage your Swft subscription, payment method, and invoice history. Track usage-based fees from Swft Offload and any out-of-band gateway fees billed monthly. ## What you see [Section titled “What you see”](#what-you-see) ### Current plan [Section titled “Current plan”](#current-plan) Your active subscription tier and the platform fee percentage that applies to your orders. For most stores this is **2%**; agencies and high-volume merchants can negotiate lower rates. ### Payment method [Section titled “Payment method”](#payment-method) The card Swft charges your monthly invoices to. Stored on your behalf in Stripe Billing (Swft itself never sees the card number). * **Set up card** if no card is on file. * **Replace card** to swap. If your card fails on a monthly charge, Swft retries for 7 days then suspends the account. You’ll get email warnings before that. ### This month’s usage [Section titled “This month’s usage”](#this-months-usage) A summary of: * Total checkout sessions * Successful orders * Total transaction volume * Platform fees accrued (in-flow via Stripe Connect) * Platform fees billed out-of-band (PayPal, Klyme, NomuPay — see [Payment Gateways](/getting-started/payment-gateways)) ### Swft Offload tier [Section titled “Swft Offload tier”](#swft-offload-tier) If you have [Swft Offload](/modules/swft-offload) active, this section shows: * Your current tier (Starter / Growth / Scale / Enterprise) * Archived order count vs the tier limit * Cloud storage used vs limit * **Upgrade tier** if you’re approaching the limit ### Invoice history [Section titled “Invoice history”](#invoice-history) Every monthly Swft invoice with PDF download, status (paid / pending / failed), and a breakdown by line item: * Stripe Connect fee summary (informational — these are deducted in-flow, not invoiced) * Out-of-band gateway fees (one line per gateway) * Swft Offload subscription * Any one-off charges ## How fees work [Section titled “How fees work”](#how-fees-work) See the [Payment Gateways overview](/getting-started/payment-gateways) for the full fee-model explainer. Short version: * **Stripe** — 2% deducted at payment time via Stripe Connect. Doesn’t appear on your Swft invoice. * **PayPal / Klyme / NomuPay** — 2% accumulated into a monthly invoice you pay via the card above. * **Swft Offload** — flat monthly subscription based on tier (£5–£40+). * **Swft Cart, Swft Checkout, all other plugins** — free. The monthly invoice is generated on the 1st and charged on the 5th. If your card declines, you’ll be notified and Swft retries daily for 7 days. ## Changing your plan [Section titled “Changing your plan”](#changing-your-plan) Most plan changes happen via email — contact . Specifically: * **Increasing your Offload tier** — self-serve in the Billing page. * **Negotiating a lower platform fee** — contact support; usually requires £100k+/month volume. * **Switching to annual billing** — contact support. * **Cancelling** — contact support. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Cancelling Swft doesn’t refund the current month.** You get the rest of the month, then it stops. * **A failed card pauses dispatch.** Swft keeps processing checkouts (so you don’t lose orders) but suspends new feature changes until billing is resolved. * **Invoice currency.** Swft invoices in GBP by default. If you’d prefer EUR or USD, ask support — they can switch you. # Checkout Editor A WYSIWYG editor for your checkout’s visual design. Pick a template, set your accent colour and typography, tweak spacing and button radius, and write custom CSS if you need it. Live preview alongside the controls. ## Templates [Section titled “Templates”](#templates) Five base templates: | Template | Vibe | | -------------- | --------------------------------------------------------------------- | | **Minimal** | Clean, single-column, lots of whitespace. The Swft default. | | **Split** | Two-column with the order summary fixed on the right. | | **Fullscreen** | Maximalist — hero image, no order summary, full-width steps. | | **Centered** | Narrow column centred on the page, like a Stripe Checkout. | | **Express** | Strips everything non-essential. Wallet + email + pay, in that order. | Switching templates is reversible — your settings carry over. ## Theme presets [Section titled “Theme presets”](#theme-presets) Five preset colour systems: * **Paper** — warm off-white background, dark ink text, lime accent. Default. * **Technical** — pure white, gridded, monospace headings. Engineering-vibes. * **Warm** — peachy background, deep brown text, terracotta accent. * **Electric** — black background, neon accent. Dark mode. * **Custom** — pick every colour individually. ## Typography [Section titled “Typography”](#typography) Pick from 12 fonts grouped by family: * **Sans** — Inter, IBM Plex Sans, Geist, DM Sans * **Serif** — Playfair Display, Source Serif, Lora * **Display** — Fraunces, Cabinet Grotesk * **Mono** — JetBrains Mono, Geist Mono, IBM Plex Mono Pick one for body, one for headings. Defaults are sensible — start with Inter on both unless you have brand reasons to diverge. ## Spacing & shape [Section titled “Spacing & shape”](#spacing--shape) * **Max width** — how wide the checkout content column goes. 480px (narrow) to 1200px (wide). Default 720px. * **Base font size** — 14 / 15 / 16 / 17 / 18 px. Default 16. * **Border radius** — 0 (square) to 24px (very rounded). Default 8. * **Button radius** — same range, separate from card radius. Default 8. ## Express & enhancement toggles [Section titled “Express & enhancement toggles”](#express--enhancement-toggles) In-line toggles for: * **Express Checkout** — show Apple Pay / Google Pay above the form. * **Social Proof** — show “X people viewing this checkout right now” / recent purchases. * **Inventory warnings** — show “only 3 left in stock” when items are low. * **Referral prompt** — show “Refer a friend, get £5” on the confirmation screen ([Ambassadors](/integrations/ambassadors)). ## Custom CSS [Section titled “Custom CSS”](#custom-css) A textarea where you can paste arbitrary CSS. Loaded onto every checkout page, scoped to the checkout origin. Use the inspector in Chrome to find the right selectors. Custom CSS is unsupported We make best effort to keep stable class names, but if a Swft Checkout update breaks your custom CSS, that’s not a bug — it’s the cost of arbitrary CSS. Use sparingly. ## Live preview [Section titled “Live preview”](#live-preview) The right panel shows the checkout rendered with your current settings, using a sample cart. It updates as you change controls. The preview is approximate — the actual checkout has a real cart, real shipping methods, real Stripe Elements. Some interactions don’t fire in the preview. Always **test the real checkout** before launching. ## Saving [Section titled “Saving”](#saving) Changes save automatically every few seconds. There’s no “publish” button — once saved, every new session uses the new design. If you want to test a design change against control traffic before rolling it to everyone, use [A/B Testing](/dashboard/ab-testing). # Go Live A pre-launch checklist that confirms your Swft Checkout is ready for real customers. Walk through every item before you switch your DNS / activate the WordPress plugin. ## Why a checklist [Section titled “Why a checklist”](#why-a-checklist) It’s easy to mess up the small things — Apple Pay domain not verified, test mode still on, webhook secret not regenerated for production. The Go Live page surfaces every common pitfall and gives you a one-click test for most of them. ## The checks [Section titled “The checks”](#the-checks) ### 1. Stripe connected and live [Section titled “1. Stripe connected and live”](#1-stripe-connected-and-live) * ✅ Stripe Connect OAuth complete. * ✅ The connected Stripe account is **live mode** (not test mode). * ✅ The Stripe account’s “Activate payments” status is **enabled**. Click **Test Stripe** to fire a £0.01 payment intent and immediately void it. ### 2. Domain verified [Section titled “2. Domain verified”](#2-domain-verified) * ✅ Custom domain DNS records propagated (CNAME). * ✅ SSL certificate issued and serving. * ✅ HTTPS reachable from external networks. Click **Verify** to re-check. ### 3. Apple Pay domain verification [Section titled “3. Apple Pay domain verification”](#3-apple-pay-domain-verification) * ✅ `apple-developer-merchantid-domain-association` file served at `https://yourdomain/.well-known/`. * ✅ Domain registered in Stripe Dashboard → Apple Pay Domains. Click **Test Apple Pay** to confirm. If you’re on the Swft default domain (`checkout.swft.co.uk`), this is handled for you. ### 4. Tracking pixels firing [Section titled “4. Tracking pixels firing”](#4-tracking-pixels-firing) * ✅ Meta CAPI test event received. * ✅ GA4 measurement received. * ✅ TikTok pixel received. Click each pixel’s **Send test event** button. You’ll need to confirm receipt in each provider’s dashboard: * **Meta** — Events Manager → Test events. * **GA4** — DebugView (use the test stream). * **TikTok** — Events Manager → Test events. ### 5. Webhook delivery [Section titled “5. Webhook delivery”](#5-webhook-delivery) * ✅ Test event delivered to your endpoint with 200 response. * ✅ Signature verification succeeded. Click **Send test event**. ### 6. Email deliverability [Section titled “6. Email deliverability”](#6-email-deliverability) * ✅ Recovery emails are sending. * ✅ Confirmation emails go to inbox (not spam) — check both Gmail and Outlook tests. The Go Live page lets you send a sample of each email to your own address. ### 7. Plugin compatibility [Section titled “7. Plugin compatibility”](#7-plugin-compatibility) * ✅ WordPress plugin is up to date. * ✅ WooCommerce is up to date. * ✅ No plugin conflict warnings on your WP admin’s Swft Checkout settings page. ### 8. Test order [Section titled “8. Test order”](#8-test-order) Run a complete real-money test order: 1. Go to your store. 2. Add a product to cart. 3. Checkout. 4. Pay with a real card for a small amount. 5. Verify: * You see the confirmation screen. * The order appears in WooCommerce → Orders. * The order appears in your Swft Dashboard’s Sessions list. * Tracking pixels fired in your analytics providers’ real-time views. 6. Refund the order. Skipping this step is the most common cause of “wait, it’s broken in production” support tickets. ### 9. Backup plan [Section titled “9. Backup plan”](#9-backup-plan) * ✅ The Swft WordPress plugin’s **fallback to WooCommerce checkout** toggle is enabled. (If Swft’s API is unreachable, shoppers fall through to standard WC checkout instead of seeing a blank page.) * ✅ You have access to roll back: deactivate the Swft plugin in WP admin to instantly revert to WC checkout. ## Status indicators [Section titled “Status indicators”](#status-indicators) Each check shows one of: * ✅ **Pass** — green tick. * ⚠️ **Warning** — yellow; not blocking but worth fixing (e.g. test mode keys present, harmless but suspicious). * ❌ **Fail** — red; blocking. The Go Live button is disabled until all reds are green. ## The Go Live button [Section titled “The Go Live button”](#the-go-live-button) When every check is green, the big **Go Live** button activates. Clicking it: 1. Flags your account as production-active. 2. Enables live Stripe Radar. 3. Switches the Swft Checkout WP plugin from “test mode” to “live mode” automatically. 4. Records a `merchant.went_live` event for your dashboard timeline. You can roll back by clicking **Pause live mode** — the plugin re-disables Swft and falls back to WC checkout immediately. ## After going live [Section titled “After going live”](#after-going-live) * Watch the Sessions table for the first few hours. * Run another test order with a different payment method (e.g. PayPal if you have it). * Check email deliverability is working with real customer orders. * Tell your team you’re live so they know to look for issues. # Modules A single page to enable, disable, and configure every feature module. Some are built into Swft Checkout (Express Checkout, Local Payments, BNPL); others ship as separate WordPress plugins you install from this page. ## Built-in modules [Section titled “Built-in modules”](#built-in-modules) These toggle on/off from the Modules page directly: | Module | What it does | | -------------------- | ---------------------------------------------------------------------------------------------------------------- | | **Express Checkout** | Apple Pay / Google Pay button above the contact form. See [Express Checkout](/getting-started/express-checkout). | | **Stripe Link** | Stripe’s one-click checkout for returning Link-enabled shoppers. | | **BNPL** | Klarna, Clearpay, Affirm, Zip via Stripe. See [BNPL](/getting-started/bnpl). | | **Local Payments** | iDEAL, Bancontact, BLIK, SOFORT, etc. See [Local Payments](/integrations/local-payments). | | **PayPal** | Adds the PayPal button. See [Connecting PayPal](/getting-started/connecting-paypal). | | **Klyme** | UK Open Banking. See [Connecting Klyme](/getting-started/connecting-klyme). | | **NomuPay** | Total Processing card payments. See [Connecting NomuPay](/getting-started/connecting-nomupay). | | **Social Proof** | Live viewer count + recent purchase notifications. | | **Upsell** | Cross-sell / upsell offers on the cart or payment step. | | **Charity Round-up** | Optional donation of order-total rounding to a charity. | | **B2B mode** | Adds company name / VAT / PO number fields and KYC. | | **Custom Fields** | Add merchant-defined fields to the details step. | | **Gift Cards** | See [Gift Cards](/swft-checkout/gift-cards). | Each module has an **enable** toggle and a “Configure” link that takes you to its settings. ## WordPress plugins [Section titled “WordPress plugins”](#wordpress-plugins) Some features ship as separate plugins. The Modules page shows them with **Install** / **Activate** / **Settings** buttons: | Plugin | Doc | | --------------------------- | ------------------------------------------------- | | **Swft Cart** (cart drawer) | [Swft Cart Overview](/swft-cart/) | | **Swft Bumps** | [Swft Bumps](/modules/swft-bumps) | | **Swft Drops** | [Swft Drops](/modules/swft-drops) | | **Swft Gifts** | [Swft Gifts](/modules/swft-gifts) | | **Swft Subscriptions** | [Swft Subscriptions](/modules/swft-subscriptions) | | **Swft Bundles** | [Swft Bundles](/modules/swft-bundles) | | **Swft Digital** | [Swft Digital](/modules/swft-digital) | | **Swft Donations** | [Swft Donations](/modules/swft-donations) | | **Swft Funnels** | [Swft Funnels](/modules/swft-funnels) | | **Swft License** | [Swft License](/modules/swft-license) | | **Swft Offload** | [Swft Offload](/modules/swft-offload) | | **Swft Reviews** | [Swft Reviews](/modules/swft-reviews) | | **Swft Wallet** | [Swft Wallet](/modules/swft-wallet) | Click **Install** to download the plugin’s zip. Upload to your WordPress site via **Plugins → Add New → Upload Plugin**, then activate. ## Per-module configuration [Section titled “Per-module configuration”](#per-module-configuration) Most modules expose their settings inline (an expandable details panel). Heavier modules link out to their own pages — for example **Gift Cards** opens the dedicated Gift Cards page where you create and manage codes. ## Tips [Section titled “Tips”](#tips) * **Enable conservatively.** Every active module adds visual elements to your checkout. Too many at once = noise; conversion suffers. * **Test after enabling.** Run a real test checkout after toggling any module on, especially payment methods. * **Some modules need creds.** Enabling PayPal / Klyme / NomuPay shows the module but the gateway only appears for shoppers once you also enter API credentials in **Settings → Payments**. # Overview The Overview page is your dashboard home. It shows live KPIs, today’s conversion funnel, and a recent-orders feed. ## What you see [Section titled “What you see”](#what-you-see) ### KPI cards [Section titled “KPI cards”](#kpi-cards) | Card | What it shows | | -------------------------- | ------------------------------------------------------------ | | **Active sessions** | Open checkout sessions right now. Updates every few seconds. | | **Completed orders today** | Successful orders since midnight (your timezone). | | **Revenue today** | Gross revenue of those orders. | | **Conversion rate** | Completed ÷ Started for the current calendar day. | ### Today’s conversion funnel [Section titled “Today’s conversion funnel”](#todays-conversion-funnel) A horizontal bar showing the drop-off at each step: 1. **Sessions started** — total checkout pages loaded. 2. **Details complete** — shoppers who submitted email + address. 3. **Payment attempted** — at least one payment attempt was made. 4. **Completed** — successful orders. Each bar’s width is proportional to the count. Hover for percentage drop-offs. ### Recent orders [Section titled “Recent orders”](#recent-orders) A table of the last 20 orders showing: * Customer email * Country flag * Amount * Payment method * Stripe Radar risk score (colour-coded amber / red for elevated risk — see [Fraud Rules](/swft-checkout/fraud-rules)) * Timestamp (in your configured timezone) Click any row to open the full session detail. ## Tips [Section titled “Tips”](#tips) * **Timezone matters.** Set your timezone in Settings → Localisation. The “today” cards reset at midnight in your timezone, not UTC. * **Real-time isn’t instant.** Sessions are updated as the shopper progresses. There can be a 1-3 second lag between a shopper finishing and them showing up here. * **Active sessions** include shoppers who landed on the page but never started filling in details. Don’t take this as “people about to buy” — most are tab-leavers. # Sessions Every checkout session that ever started — successful, failed, expired — is in the Sessions table. Use it to investigate specific orders, debug payment issues, and confirm tracking is firing. ## The table [Section titled “The table”](#the-table) Columns: | Column | What | | ------------------ | ------------------------------------------------------------------------------------------------------------------ | | **Status** | Coloured badge: complete (green), pending (amber), payment (blue), expired (grey), failed (red), refunded (purple) | | **Customer** | Email; click to see all sessions for that email | | **Country** | Flag emoji based on billing country | | **Amount** | Order total in the cart currency | | **Payment method** | Card / PayPal / Klyme / NomuPay / etc | | **Radar score** | Stripe Radar risk score, colour-coded (see [Fraud](/swft-checkout/fraud-rules)) | | **Started** | Time the session was created | | **TTFB** | Time-to-first-byte for the checkout page (Swft tracks this for perf) | | **Actions** | Copy session ID, view detail, view in WooCommerce | ## Filtering [Section titled “Filtering”](#filtering) * **Status** — filter to one state. * **Search** — by customer email, session ID, or order reference. * **Date range** — last 24h / 7d / 30d / 90d / custom. * **Country** — pick a single country. * **Payment method** — pick a single gateway. * **Radar level** — normal / elevated / highest. Filters are AND-combined. ## Session detail [Section titled “Session detail”](#session-detail) Click any row to open the full session detail: * **Customer** — email, name, phone, addresses. * **Cart** — every line item with quantity and price; subtotal, tax, shipping, discount, total. * **Payment** — gateway, payment intent ID, amount, status, any captured / refunded splits. * **Fraud** — Stripe Radar score, level, rule (if any). * **Custom fields** — values for any merchant-defined custom fields. * **Extensions** — flags from active modules (gift card applied, donation included, KYC verified, etc). * **Timeline** — events in order: created, details submitted, payment started, payment succeeded, WC order created. * **Raw payload** — JSON dump of the full session record, for debugging. The **View in WooCommerce** button deep-links to the resulting WC order (if it was created — failed and expired sessions don’t have one). ## Debugging specific orders [Section titled “Debugging specific orders”](#debugging-specific-orders) **Customer says they paid but you see no order:** 1. Find their session. 2. Check the status. If `payment` and not `complete`, the payment failed at the last step. 3. Check the timeline for the failure reason. 4. Check Radar score — was it blocked for fraud? **Customer says they were charged twice:** 1. Search their email. 2. Look for two `complete` sessions or two `payment` sessions with similar timestamps. 3. If duplicate, refund one via Stripe / PayPal / WC and apologise. **Order amount looks wrong:** 1. Open the session detail. 2. Cross-check cart subtotal, shipping, tax, discount. 3. The most common cause: a coupon code applied that you weren’t expecting, or a gift card applied silently. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Sessions live forever.** We don’t auto-purge. The table can get large after months of trading; use filters. * **Failed payment intents aren’t the same as failed sessions.** A shopper can attempt payment 3 times on one session before giving up — the session is still one row but the payment intent history is in the detail view. * **Expired sessions** are sessions that were created but never completed within the timeout (30 minutes by default). They’re not failures — most expired sessions are just shoppers who got distracted. * **Customer email isn’t unique.** A repeat customer has many sessions. Filter by email to find them all. # Settings The dashboard’s Settings page is the master config surface. Everything from brand colour to webhook URLs to tracking-pixel IDs lives here. Heavy page — 40+ fields grouped into sections. This is distinct from the **WordPress plugin settings** (in your WP admin at WooCommerce → Swft Checkout). See [Plugin Settings](/swft-checkout/settings) for those. ## Sections [Section titled “Sections”](#sections) ### Store identity [Section titled “Store identity”](#store-identity) * **Store name** — shown in the checkout header. * **Store URL** — the URL Swft redirects shoppers to on cancel / back navigation. * **Logo** — uploaded to Swft’s R2 bucket; rendered in the checkout header. SVG preferred, PNG with transparent background fine. * **Brand colour** — your primary accent. Sets button background, link colour, focus ring. * **Support email** — shown in the footer and on error screens. * **Localisation** — currency, timezone, language fallbacks. ### Custom domain [Section titled “Custom domain”](#custom-domain) If you want the checkout on `checkout.yourstore.com` instead of `checkout.swft.co.uk`: 1. Add the domain here. 2. Swft shows the DNS records you need: a CNAME pointing at `checkout.swft.co.uk`. 3. Add the records in your DNS provider. 4. Click **Verify**. Swft issues an SSL cert (free, via Let’s Encrypt) and the domain is live. See [Custom Domains](/swft-checkout/custom-domains) for the deep dive. ### Payments [Section titled “Payments”](#payments) Per-gateway credentials and toggles. See the [Payment Gateways](/getting-started/payment-gateways) section for setup of each. ### Recovery emails [Section titled “Recovery emails”](#recovery-emails) When a shopper abandons their cart, Swft can send a series of recovery emails: * **Email 1** — 1 hour after abandonment (default). * **Email 2** — 24 hours. * **Email 3** — 3 days (final reminder, often with a discount code). Each email has: * Subject line with placeholder support (e.g. `\{\{customer.firstName\}\}`). * Body (rich-text editor with placeholders for cart items, return URL, discount code). * Live preview. You can also configure a single transactional sender domain (must be verified with Resend / SendGrid / whoever you use) — recovery emails go out from `noreply@yourdomain.com` rather than Swft’s generic sender. ### Webhooks [Section titled “Webhooks”](#webhooks) * **Webhook URL** — your endpoint. See [Webhooks Reference](/integrations/webhooks). * **Webhook secret** — used to sign every outgoing request. Click **Regenerate** to rotate. * **Subscriptions** — optional event-specific filters. Leave empty to send every event. * **Test event** — fires a synthetic `order.completed` to your URL. Use to confirm signature verification works. * **Failed deliveries** — list of webhook attempts that didn’t return 2xx after retry. Inspect, resend, or mark resolved. ### Tracking pixels [Section titled “Tracking pixels”](#tracking-pixels) Server-side tracking pixels Swft fires on every order: * **Meta Conversions API (CAPI)** — Facebook / Instagram. Paste your Pixel ID and access token. * **Google Analytics 4** — Measurement ID and API secret. * **TikTok** — Pixel ID and access token. * **Google Ads** — conversion ID and label. * **Reddit Pixel** — advertiser ID. Server-side pixels fire from Swft’s API, not from the browser, which means: * They bypass ad blockers. * They don’t require cookie consent (technically — verify with your DPO). * They aren’t slowed by client-side load. ### Custom fields [Section titled “Custom fields”](#custom-fields) Define merchant-specific fields shown on the details step: * **Field name** — internal name (e.g. `business_size`). * **Label** — what the shopper sees. * **Type** — text / email / phone / select / radio / textarea. * **Options** (for select / radio) — comma-separated. * **Required** — toggle. * **Section** — `details` / `shipping` / `payment` — where the field renders. Custom field values appear on the resulting WC order’s meta and on every webhook payload. ### B2B mode [Section titled “B2B mode”](#b2b-mode) Toggle B2B mode for the entire store. When on: * Company name and VAT number fields appear on the details step. * VIES + HMRC VAT validation is enabled. * PO number and Net payment terms can be requested. * [KYC](/swft-checkout/kyc) settings become relevant. ### Address autocomplete [Section titled “Address autocomplete”](#address-autocomplete) Pick a provider: * **Google Places** — paste your Maps API key. Best coverage globally. * **SmartyStreets** — US-focused; better for US-heavy stores. * **None** — disable address autocomplete; shoppers type addresses manually. ### Tax automation [Section titled “Tax automation”](#tax-automation) Configure tax calculation: * **None** — your WooCommerce tax rules are honoured (default). * **Avalara** — Avalara AvaTax integration; paste your account / licence key. * **TaxJar** — TaxJar API key. ### API keys [Section titled “API keys”](#api-keys) The **Swft API key** that the Swft Checkout WordPress plugin uses to authenticate session creation. Keep this secret; regenerate if compromised. ## Saving [Section titled “Saving”](#saving) Each section has its own **Save** button. Saving is idempotent — re-saving an unchanged section is a no-op. Some changes (logo upload, custom domain) take a few seconds to propagate. Other changes are instant. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Webhook secret rotation is hard.** When you regenerate, in-flight deliveries fail signature verification. Coordinate with your endpoint’s deployment. * **Custom field changes don’t migrate.** Renaming a field doesn’t update past orders’ meta. Plan field schemas upfront. * **Tax automation is opinionated.** Avalara / TaxJar override your WC tax rules entirely. Test thoroughly before relying. * **Logo upload doesn’t optimise.** Upload a sensibly-sized SVG or compressed PNG. A 5MB photo will load 5MB on every checkout. # Unstack × Swft — Pilates booking quickstart End-to-end Next.js + Supabase integration that uses Swft as headless checkout for a multi-tenant Pilates booking platform. Every file in this directory is a stand-alone copy-paste recipe — adapt names but keep the structure. ## What this covers [Section titled “What this covers”](#what-this-covers) 1. **Onboard a studio** as a Swft merchant via the agency API, including Stripe Connect + a `cobalt-pilates.checkouts.unstack.co.uk` subdomain. 2. **Create checkout sessions** with `extensions.booking` carrying the class id, customer id, reformer assignment, and payment type. 3. **Pre-confirmation hook** that verifies class capacity right before Stripe charges. 4. **Webhook handlers** for `order.completed` (single bookings), `subscription.created`/`renewed` (monthly memberships → grant credits), and `subscription.cancelled` (revoke access). 5. **Refund/cancellation flows** that round-trip through Swft. ## File layout [Section titled “File layout”](#file-layout) | File | Purpose | | -------------------------------------- | ------------------------------------------------------------ | | `lib/swft.ts` | Tiny typed client around `/v2/agencies/*` | | `app/api/studios/onboard/route.ts` | POST /api/studios/onboard — calls Swft to mint a merchant | | `app/api/bookings/new/route.ts` | Server action that creates a session with booking extensions | | `app/api/swft/capacity-check/route.ts` | The pre-confirmation hook | | `app/api/swft/webhooks/route.ts` | The unified Standard Webhooks consumer | | `lib/membership-credits.ts` | Grants/refreshes class credits from subscription events | ## Environment [Section titled “Environment”](#environment) ```plaintext SWFT_PARTNER_KEY=swft_ak_live_… SWFT_WEBHOOK_SECRET=whsec_… SWFT_PRECONFIRM_SECRET=whsec_… SWFT_WORKSPACE_ID= SWFT_API_URL=https://api.swft.co.uk ``` # Extensions API The Swft Extensions API allows any WordPress plugin to pass structured data through the checkout session and interact with the checkout frontend. ## How it works [Section titled “How it works”](#how-it-works) Data flows in one direction: **WP plugin → Swft API → Cloudflare KV → checkout frontend → WooCommerce order meta**. 1. A WP plugin hooks into `swft_session_extensions` and adds data to the `$extensions` array 2. When a session is created, the plugin calls `POST api.swft.co.uk/sessions` with the extensions included in the payload 3. The API stores the extensions in the session record and Cloudflare KV 4. The checkout frontend reads extensions from `window.__SWFT_SESSION__.extensions` 5. After payment, extensions are written to the WooCommerce order as `_swft_ext_{key}` meta fields ## Available hooks [Section titled “Available hooks”](#available-hooks) ### PHP filters (WordPress side) [Section titled “PHP filters (WordPress side)”](#php-filters-wordpress-side) For modifying session data before it reaches the Swft API: | Filter | Purpose | | ------------------------------- | ---------------------------------------------------------- | | `swft_session_extensions` | Primary extension hook — add arbitrary data to the session | | `swft_session_line_item` | Modify or exclude a single line item | | `swft_session_line_items` | Modify all line items after the loop | | `swft_session_shipping_methods` | Modify available shipping methods | | `swft_session_customer` | Modify pre-filled customer data | | `swft_session_totals` | Modify calculated totals | | `swft_session_payload` | Last-resort: modify the complete payload | For modifying Swft Cart data: | Filter | Purpose | | ------------------------ | ----------------------------------------------------- | | `swftcart_cart_data` | Modify the full cart object sent to the cart frontend | | `swftcart_cart_item` | Modify a single cart item | | `swftcart_modules` | Override module enable/disable state | | `swftcart_i18n` | Override UI strings | | `swftcart_announcements` | Set announcement bar content | | `swftcart_theme_vars` | Override CSS custom property values | | `swftcart_delivery_data` | Set delivery countdown data | | `swftcart_upsell_ids` | Set product IDs for the upsell module | See [PHP Filters](/extensions/php-filters) for full documentation. ### JS events (checkout frontend) [Section titled “JS events (checkout frontend)”](#js-events-checkout-frontend) Events fired by both Swft Cart and Swft Checkout as `CustomEvent` on `document`: | Event | Description | | ------------------------------- | --------------------------------------------------------- | | `swftcart:opened` | Cart drawer opened | | `swftcart:closed` | Cart drawer closed | | `swftcart:fab-clicked` | FAB icon clicked | | `swftcart:cart-updated` | Cart contents changed | | `swftcart:checkout` | Checkout button clicked (cancelable) | | `swftcart:item-removed` | Item removed from cart | | `swftcart:item-saved-for-later` | Item moved to saved-for-later | | `swftcart:upsell-added` | Upsell product added to cart | | `swftcart:coupon-applied` | Coupon code applied | | `swftcart:coupon-removed` | Coupon code removed | | `swftcart:cart-shared` | Share cart URL copied | | `swftcart:upsell-shown` | Upsell strip rendered | | `swftcheckout:ready` | Checkout app initialised, `window.SwftCheckout` available | | `swftcheckout:details-complete` | Customer submitted the details step | | `swftcheckout:payment-complete` | Payment succeeded | | `swftcheckout:upsell-shown` | Post-payment upsell rendered | See [JS Events](/extensions/js-events) for detail shapes and examples. ### JavaScript API (checkout frontend) [Section titled “JavaScript API (checkout frontend)”](#javascript-api-checkout-frontend) `window.SwftCheckout` is available after `swftcheckout:ready` fires: * `.addOrderRow(row)` — inject a custom row into the order summary * `.addCustomField(field)` — inject a custom form field * `.getExtension(key)` — read data from the session extensions object See [SwftCheckout API](/extensions/swft-checkout-api) for the full reference. ## Tutorial [Section titled “Tutorial”](#tutorial) For a complete walkthrough of building a module using all these hooks together, see [Building a Module](/extensions/building-a-module). # Building a Module This guide walks through building a complete Swft module — a standalone WordPress plugin that extends the checkout using the Extensions API. We will build a simplified version of **SwftGifts**: a gift message and gift wrap option that appears in the cart and checkout. ## What we are building [Section titled “What we are building”](#what-we-are-building) * A settings field in WooCommerce where merchants configure the gift wrap price * Cart-side: gift message textarea and gift wrap toggle in the Swft Cart drawer * Checkout-side: a custom order row showing the gift wrap fee, and the message shown in the order summary * Post-order: gift data written to the WooCommerce order *** ## Step 1: Plugin boilerplate [Section titled “Step 1: Plugin boilerplate”](#step-1-plugin-boilerplate) Create `/wp-content/plugins/my-swft-gifts/my-swft-gifts.php`: ```php session; $message = sanitize_textarea_field( $session->get( 'gift_message', '' ) ); $wrap = (bool) $session->get( 'gift_wrap', false ); $wrap_fee = (int) get_option( 'my_swft_gifts_wrap_price', 299 ); // pence $extensions['gifts'] = [ 'message' => $message, 'wrap' => $wrap, 'wrap_fee' => $wrap, // only charge if wrap is selected 'price' => $wrap ? $wrap_fee : 0, ]; return $extensions; } /** * When WooCommerce creates an order (triggered by Swft's WC sync), * the extension data is already in _swft_ext_gifts. Here we also * set a human-readable order note. */ public static function write_order_meta( WC_Order $order ): void { $ext = $order->get_meta( '_swft_ext_gifts', true ); if ( ! $ext ) { return; } $data = is_string( $ext ) ? json_decode( $ext, true ) : (array) $ext; if ( ! empty( $data['message'] ) ) { $order->add_order_note( 'Gift message: ' . esc_html( $data['message'] ), false // not customer-facing ); } if ( ! empty( $data['wrap'] ) ) { $order->add_order_note( 'Gift wrap requested.', false ); } } } ``` *** ## Step 3: Test that data flows through [Section titled “Step 3: Test that data flows through”](#step-3-test-that-data-flows-through) 1. Activate your plugin 2. Add a product to cart 3. Enable debug logging: **Settings → Swft Checkout → Debug Logging → Yes** 4. Navigate to the WooCommerce checkout page 5. Check the debug log — you should see the session creation call succeed 6. In the Swft dashboard or via `api.swft.co.uk`, inspect the session — `extensions.gifts` should be present You can also test directly via `wp-cli`: ```bash wp eval 'WC()->session->set("gift_message", "Happy birthday!"); WC()->session->set("gift_wrap", true); echo json_encode((new Swft_Cart())->build_payload()["extensions"]);' ``` *** ## Step 4: Listen for swftcheckout:ready on the frontend [Section titled “Step 4: Listen for swftcheckout:ready on the frontend”](#step-4-listen-for-swftcheckoutready-on-the-frontend) Add a JavaScript file that your plugin enqueues on all pages (or just the checkout page via a URL check): my-swft-gifts.js ```js document.addEventListener('swftcheckout:ready', function() { const gifts = window.SwftCheckout.getExtension('gifts') if (!gifts) return // If gift wrap was selected, add a fee row to the order summary if (gifts.wrap && gifts.price > 0) { const formatted = new Intl.NumberFormat('en-GB', { style: 'currency', currency: window.__SWFT_SESSION__.cart.currency, }).format(gifts.price / 100) window.SwftCheckout.addOrderRow({ id: 'gift-wrap-fee', label: 'Gift wrap', value: formatted, pos: 'after-subtotal', }) } // If there is a gift message, show it as a custom field (read-only display) if (gifts.message) { window.SwftCheckout.addOrderRow({ id: 'gift-message-display', label: 'Gift message', value: gifts.message, pos: 'after-subtotal', }) } }) ``` Enqueue it: ```php add_action( 'wp_enqueue_scripts', function(): void { wp_enqueue_script( 'my-swft-gifts-frontend', plugin_dir_url( MY_SWFT_GIFTS_DIR . 'my-swft-gifts.php' ) . 'assets/my-swft-gifts.js', [], MY_SWFT_GIFTS_VERSION, true ); } ); ``` Because the script is enqueued on the WordPress side and the checkout runs on `checkout.swft.co.uk`, this script will not run on the checkout domain. For frontend checkout behaviour, the correct approach is to pass all necessary data via `swft_session_extensions` and use `addOrderRow` / `addCustomField` from a script loaded via the Swft Cart drawer (which runs on the WooCommerce store domain). If you need JavaScript to run on the checkout frontend itself, contact Swft about the hosted script injection feature. *** ## Step 5: Handle cart-side UI [Section titled “Step 5: Handle cart-side UI”](#step-5-handle-cart-side-ui) If you want a gift message field and wrap toggle in the Swft Cart drawer, hook into `swftcart_cart_data` to add the fields as module configuration: ```php add_filter( 'swftcart_cart_data', function( array $cart_data ): array { $cart_data['gift_options'] = [ 'enabled' => true, 'wrap_price' => (int) get_option( 'my_swft_gifts_wrap_price', 299 ), 'currency' => get_woocommerce_currency(), 'message' => WC()->session->get( 'gift_message', '' ) ?? '', 'wrap' => (bool) ( WC()->session->get( 'gift_wrap', false ) ?? false ), ]; return $cart_data; } ); ``` The Swft Cart frontend reads `cart_data.gift_options` and renders the UI if your module is registered with the Swft Cart module system. See [Building a Swft Cart Module](https://swft.co.uk/docs/cart-modules) for the cart module registration API. *** ## Step 6: Handle post-order in woocommerce\_checkout\_order\_created [Section titled “Step 6: Handle post-order in woocommerce\_checkout\_order\_created”](#step-6-handle-post-order-in-woocommerce_checkout_order_created) The `woocommerce_checkout_order_created` hook fires when Swft syncs the order to WooCommerce via the REST API. By this point, all extension data is already written to order meta as `_swft_ext_{key}`. The hook in Step 2 already handles this. In more complex modules, you might: * Send a confirmation email with gift-specific content * Trigger a fulfilment system * Update a loyalty points balance ```php add_action( 'woocommerce_checkout_order_created', function( WC_Order $order ): void { $gifts_raw = $order->get_meta( '_swft_ext_gifts', true ); if ( ! $gifts_raw ) return; $gifts = is_string( $gifts_raw ) ? json_decode( $gifts_raw, true ) : (array) $gifts_raw; if ( ! empty( $gifts['wrap'] ) ) { // Notify packing team wp_mail( 'packing@yourstore.com', 'Gift wrap required — Order #' . $order->get_order_number(), 'Gift message: ' . esc_html( $gifts['message'] ?? '(none)' ) ); } }, 10, 1 ); ``` *** ## Summary [Section titled “Summary”](#summary) | Step | Hook | What it does | | ----------------- | ------------------------------------------------- | ---------------------------------------- | | Session creation | `swft_session_extensions` | Adds `gifts` data to the session payload | | Checkout frontend | `swftcheckout:ready` + `SwftCheckout.addOrderRow` | Renders gift wrap fee in order summary | | Post-payment | `woocommerce_checkout_order_created` | Reads `_swft_ext_gifts` from order meta | | Cart drawer | `swftcart_cart_data` | Passes gift option state to cart UI | The key insight: **all extension data passes through the Swft session as a JSON blob**. The WordPress plugin writes it in, the checkout frontend reads it out, and the WooCommerce order stores it permanently. Your module never needs to store anything separately — the session is the source of truth. # JS Events Swft Cart and Swft Checkout fire `CustomEvent` on `document`. Listen with `document.addEventListener`. All events are prefixed with `swftcart:` (cart) or `swftcheckout:` (checkout). *** ## Swft Cart events [Section titled “Swft Cart events”](#swft-cart-events) ### swftcart:opened [Section titled “swftcart:opened”](#swftcartopened) Fires when the cart drawer opens. ```js document.addEventListener('swftcart:opened', (e) => { console.log(e.detail) // { timestamp: number } }) ``` **Detail:** `{ timestamp: number }` — Unix timestamp in ms. *** ### swftcart:closed [Section titled “swftcart:closed”](#swftcartclosed) Fires when the cart drawer closes. ```js document.addEventListener('swftcart:closed', (e) => { console.log(e.detail) // { timestamp: number } }) ``` **Detail:** `{ timestamp: number }` *** ### swftcart:fab-clicked [Section titled “swftcart:fab-clicked”](#swftcartfab-clicked) Fires when the floating cart icon is clicked, before the drawer opens. ```js document.addEventListener('swftcart:fab-clicked', (e) => { console.log(e.detail) // { timestamp: number } }) ``` **Detail:** `{ timestamp: number }` *** ### swftcart:cart-updated [Section titled “swftcart:cart-updated”](#swftcartcart-updated) Fires when cart contents change (item added, removed, quantity changed, coupon applied/removed). ```js document.addEventListener('swftcart:cart-updated', (e) => { const { itemCount, subtotal, currency } = e.detail document.querySelector('.cart-count').textContent = itemCount }) ``` **Detail:** ```ts { itemCount: number // total quantity across all items subtotal: number // in minor units (pence) currency: string // ISO 4217 (e.g. 'GBP') cartHash: string // WooCommerce cart hash } ``` *** ### swftcart:checkout [Section titled “swftcart:checkout”](#swftcartcheckout) Fires when the checkout button is clicked. **This event is cancelable** — call `e.preventDefault()` to stop the redirect to checkout. ```js document.addEventListener('swftcart:checkout', (e) => { if (!termsAccepted()) { e.preventDefault() SwftCart.toast('Please accept the terms and conditions before continuing.') } }) ``` **Detail:** ```ts { total: number // cart total in minor units (pence) currency: string itemCount: number } ``` **Cancelable:** yes — `e.preventDefault()` blocks the checkout redirect. *** ### swftcart:item-removed [Section titled “swftcart:item-removed”](#swftcartitem-removed) Fires when an item is removed from the cart. ```js document.addEventListener('swftcart:item-removed', (e) => { console.log(`Removed: ${e.detail.name} (qty ${e.detail.quantity})`) }) ``` **Detail:** ```ts { productId: number name: string quantity: number cartItemKey: string } ``` *** ### swftcart:item-saved-for-later [Section titled “swftcart:item-saved-for-later”](#swftcartitem-saved-for-later) Fires when an item is moved to the saved-for-later list. ```js document.addEventListener('swftcart:item-saved-for-later', (e) => { console.log(e.detail.productId) }) ``` **Detail:** ```ts { productId: number name: string cartItemKey: string } ``` *** ### swftcart:upsell-added [Section titled “swftcart:upsell-added”](#swftcartupsell-added) Fires when a customer adds an upsell product to the cart from the cart drawer. ```js document.addEventListener('swftcart:upsell-added', (e) => { console.log(`Upsell added: ${e.detail.name}`) }) ``` **Detail:** ```ts { productId: number name: string price: number // minor units } ``` *** ### swftcart:coupon-applied [Section titled “swftcart:coupon-applied”](#swftcartcoupon-applied) Fires when a coupon code is successfully applied. ```js document.addEventListener('swftcart:coupon-applied', (e) => { console.log(`Coupon ${e.detail.code} saved ${e.detail.discount} pence`) }) ``` **Detail:** ```ts { code: string discount: number // discount amount in minor units currency: string } ``` *** ### swftcart:coupon-removed [Section titled “swftcart:coupon-removed”](#swftcartcoupon-removed) Fires when a coupon code is removed. ```js document.addEventListener('swftcart:coupon-removed', (e) => { console.log(`Removed coupon: ${e.detail.code}`) }) ``` **Detail:** ```ts { code: string } ``` *** ### swftcart:cart-shared [Section titled “swftcart:cart-shared”](#swftcartcart-shared) Fires when the customer copies the share cart URL. ```js document.addEventListener('swftcart:cart-shared', (e) => { console.log(`Shared URL: ${e.detail.url}`) }) ``` **Detail:** ```ts { url: string // the share URL that was copied } ``` *** ### swftcart:upsell-shown [Section titled “swftcart:upsell-shown”](#swftcartupsell-shown) Fires when the upsell strip renders (products became visible). ```js document.addEventListener('swftcart:upsell-shown', (e) => { console.log(`${e.detail.productIds.length} upsells shown`) }) ``` **Detail:** ```ts { productIds: number[] } ``` *** ## Swft Checkout events [Section titled “Swft Checkout events”](#swft-checkout-events) ### swftcheckout:ready [Section titled “swftcheckout:ready”](#swftcheckoutready) Fires when the checkout app has initialised and `window.SwftCheckout` is available. Always listen for this event before using the `SwftCheckout` API. ```js document.addEventListener('swftcheckout:ready', (e) => { const { session } = e.detail console.log('Session loaded:', session.sessionId) // Now safe to use window.SwftCheckout window.SwftCheckout.addOrderRow({ id: 'loyalty-discount', label: 'Loyalty discount', value: '-£5.00', pos: 'after-subtotal', }) }) ``` **Detail:** ```ts { session: SwftSession // the full session object (window.__SWFT_SESSION__) } ``` *** ### swftcheckout:details-complete [Section titled “swftcheckout:details-complete”](#swftcheckoutdetails-complete) Fires when the customer submits the details step (email, address, shipping method) and the payment intent has been created. ```js document.addEventListener('swftcheckout:details-complete', (e) => { const { formData, session } = e.detail // Track InitiateCheckout on your own analytics }) ``` **Detail:** ```ts { formData: { email: string firstName: string lastName: string line1: string line2: string city: string postcode: string country: string shippingMethodId: string } session: SwftSession } ``` *** ### swftcheckout:payment-complete [Section titled “swftcheckout:payment-complete”](#swftcheckoutpayment-complete) Fires when payment succeeds, before the confirmation screen is shown. The session status in `window.__SWFT_SESSION__` is updated to `'complete'` before this fires. ```js document.addEventListener('swftcheckout:payment-complete', (e) => { const { orderRef, session } = e.detail // Fire client-side conversion events gtag('event', 'purchase', { transaction_id: orderRef, value: session.cart.total / 100, currency: session.cart.currency, }) }) ``` **Detail:** ```ts { orderRef: string // WooCommerce order number, e.g. '#1042' session: SwftSession // session with status: 'complete' } ``` *** ### swftcheckout:upsell-shown [Section titled “swftcheckout:upsell-shown”](#swftcheckoutupsell-shown) Fires when a post-payment upsell offer is shown on the confirmation screen. ```js document.addEventListener('swftcheckout:upsell-shown', (e) => { console.log(e.detail.products) }) ``` **Detail:** ```ts { products: UpsellProduct[] } ``` Where `UpsellProduct` is: ```ts { id: number | string name: string price: number // minor units currency: string image?: string url: string tag?: string // e.g. 'Frequently bought together' } ``` # PHP Filters ## Swft Checkout filters [Section titled “Swft Checkout filters”](#swft-checkout-filters) ### swft\_session\_extensions [Section titled “swft\_session\_extensions”](#swft_session_extensions) The primary extension hook. Add arbitrary structured data to the checkout session. Data is stored in the session, available in the checkout frontend as `window.__SWFT_SESSION__.extensions`, and written to the WooCommerce order as `_swft_ext_{key}` meta on payment completion. ```php apply_filters( 'swft_session_extensions', array $extensions, WC_Cart $cart ): array ``` **Parameters:** | Parameter | Type | Description | | ------------- | --------- | --------------------------------------------------------------------------------------------- | | `$extensions` | `array` | Current extensions object. Initially empty `[]`. May already contain keys from other plugins. | | `$cart` | `WC_Cart` | The current WooCommerce cart object. | **Return:** `array` — the modified extensions array. Keys must be strings. Values may be any JSON-serialisable type. **Example:** ```php add_filter( 'swft_session_extensions', function( array $extensions, WC_Cart $cart ): array { $extensions['gift_options'] = [ 'message' => WC()->session->get( 'gift_message' ) ?? '', 'wrap' => (bool) WC()->session->get( 'gift_wrap' ), ]; return $extensions; }, 10, 2 ); ``` **Data flow:** ```plaintext WP plugin → swft_session_extensions filter → payload['extensions']['gift_options'] = { message: '...', wrap: true } → POST api.swft.co.uk/sessions → stored in Supabase + Cloudflare KV → window.__SWFT_SESSION__.extensions.gift_options → WooCommerce order meta: _swft_ext_gift_options = '{"message":"...","wrap":true}' ``` *** ### swft\_session\_line\_item [Section titled “swft\_session\_line\_item”](#swft_session_line_item) Modify or exclude a single line item before it is added to the session payload. Return `null` or `false` to exclude the item (e.g. if your plugin represents it differently in the extensions object). ```php apply_filters( 'swft_session_line_item', array $line_item, array $cart_item, WC_Product $product, string $cart_item_key ): array|null|false ``` **Parameters:** | Parameter | Type | Description | | ---------------- | ------------ | ------------------------------------------------------------- | | `$line_item` | `array` | The built line item. See structure below. | | `$cart_item` | `array` | Raw WooCommerce cart item data from `WC()->cart->get_cart()`. | | `$product` | `WC_Product` | The product object. | | `$cart_item_key` | `string` | The cart item key. | **Line item structure:** ```php [ 'product_id' => int, 'variation_id' => int|null, 'name' => string, 'quantity' => int, 'price' => int, // minor units (pence) 'subtotal' => int, // minor units 'total' => int, // minor units 'tax' => int, // minor units 'image_url' => string|null, 'sku' => string|null, 'attributes' => object, // variation attributes as key-value pairs ] ``` **Example — add custom meta to a line item:** ```php add_filter( 'swft_session_line_item', function( array $item, array $cart_item, WC_Product $product ): array { $item['engraving_text'] = $cart_item['engraving_text'] ?? ''; return $item; }, 10, 3 ); ``` **Example — exclude a virtual line item:** ```php add_filter( 'swft_session_line_item', function( $item, array $cart_item, WC_Product $product ) { if ( $product->is_virtual() && $product->get_sku() === 'GIFT_WRAP_FEE' ) { return null; // exclude from line items — handled via extensions instead } return $item; }, 10, 3 ); ``` *** ### swft\_session\_line\_items [Section titled “swft\_session\_line\_items”](#swft_session_line_items) Modify all line items after the loop, before they are added to the payload. ```php apply_filters( 'swft_session_line_items', array $line_items, WC_Cart $cart ): array ``` **Parameters:** | Parameter | Type | Description | | ------------- | --------- | ------------------------------------ | | `$line_items` | `array` | Array of all built line item arrays. | | `$cart` | `WC_Cart` | The current cart. | **Example — sort line items by name:** ```php add_filter( 'swft_session_line_items', function( array $items ): array { usort( $items, fn( $a, $b ) => strcmp( $a['name'], $b['name'] ) ); return $items; } ); ``` *** ### swft\_session\_shipping\_methods [Section titled “swft\_session\_shipping\_methods”](#swft_session_shipping_methods) Modify the available shipping methods passed to the checkout. ```php apply_filters( 'swft_session_shipping_methods', array $shipping_methods, WC_Cart $cart ): array ``` **Parameters:** | Parameter | Type | Description | | ------------------- | --------- | --------------------------------------------------------------------------------------------- | | `$shipping_methods` | `array` | Array of shipping method arrays. Each has `id`, `name`, `price` (minor units), `description`. | | `$cart` | `WC_Cart` | The current cart. | **Example — add a custom shipping option:** ```php add_filter( 'swft_session_shipping_methods', function( array $methods, WC_Cart $cart ): array { if ( $cart->get_subtotal() >= 50 ) { $methods[] = [ 'id' => 'click_collect', 'name' => 'Click & Collect', 'price' => 0, 'description' => 'Collect from our store in 2 hours', ]; } return $methods; }, 10, 2 ); ``` *** ### swft\_session\_customer [Section titled “swft\_session\_customer”](#swft_session_customer) Modify the pre-filled customer data. Used to pre-fill the details step for logged-in customers. ```php apply_filters( 'swft_session_customer', array $customer, WC_Customer $wc_customer ): array ``` **Parameters:** | Parameter | Type | Description | | -------------- | ------------- | -------------------------------- | | `$customer` | `array` | \`\[‘email’ => string | | `$wc_customer` | `WC_Customer` | The WooCommerce customer object. | **Example:** ```php add_filter( 'swft_session_customer', function( array $customer, WC_Customer $wc_customer ): array { // Add phone number for pre-fill $customer['phone'] = $wc_customer->get_billing_phone() ?: null; return $customer; }, 10, 2 ); ``` *** ### swft\_session\_totals [Section titled “swft\_session\_totals”](#swft_session_totals) Modify the totals sent to the Swft API. All values are integers in minor units (pence/cents). ```php apply_filters( 'swft_session_totals', array $totals, WC_Cart $cart ): array ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ---------------------------------------------------------------------------- | | `$totals` | `array` | `['subtotal' => int, 'tax' => int, 'discount' => int]` — all in minor units. | | `$cart` | `WC_Cart` | The current cart. | *** ### swft\_session\_payload [Section titled “swft\_session\_payload”](#swft_session_payload) Last-resort filter. Modify the complete payload before it is sent to the Swft API. Use specific filters above where possible. ```php apply_filters( 'swft_session_payload', array $payload ): array ``` **Payload structure:** ```php [ 'merchant_api_key' => string, 'cart_hash' => string, 'currency' => string, // ISO 4217 (e.g. 'GBP') 'line_items' => array, 'shipping_methods' => array, 'subtotal' => int, // minor units 'tax' => int, 'discount' => int, 'customer' => array, 'extensions' => array, 'meta' => [ 'store_url' => string, 'needs_shipping' => bool, 'needs_payment' => bool, 'wc_version' => string, 'plugin_version' => string, ], ] ``` *** ## Swft Cart filters [Section titled “Swft Cart filters”](#swft-cart-filters) ### swftcart\_cart\_data [Section titled “swftcart\_cart\_data”](#swftcart_cart_data) Modify the full cart object before it is rendered in the cart drawer. ```php apply_filters( 'swftcart_cart_data', array $cart_data ): array ``` *** ### swftcart\_cart\_item [Section titled “swftcart\_cart\_item”](#swftcart_cart_item) Modify a single cart item before rendering. ```php apply_filters( 'swftcart_cart_item', array $item, string $cart_item_key ): array ``` *** ### swftcart\_modules [Section titled “swftcart\_modules”](#swftcart_modules) Override the enabled/disabled state of modules. ```php apply_filters( 'swftcart_modules', array $modules ): array ``` `$modules` is an associative array of `option_name => bool`. See [Modules](/swft-cart/modules) for all option names. *** ### swftcart\_i18n [Section titled “swftcart\_i18n”](#swftcart_i18n) Override any UI string in the cart drawer. ```php apply_filters( 'swftcart_i18n', array $strings ): array ``` **Example:** ```php add_filter( 'swftcart_i18n', function( array $strings ): array { $strings['checkout_button'] = 'Proceed to secure checkout'; $strings['empty_cart'] = 'Your bag is empty'; return $strings; } ); ``` *** ### swftcart\_announcements [Section titled “swftcart\_announcements”](#swftcart_announcements) Set the content of the announcement bar module. ```php apply_filters( 'swftcart_announcements', array $announcements ): array ``` Each announcement is an array with `text` (string) and optionally `url` (string) and `icon` (string). **Example:** ```php add_filter( 'swftcart_announcements', function(): array { return [ [ 'text' => 'Free delivery on orders over £50', 'icon' => 'truck', ], [ 'text' => '10% off your first order — use WELCOME10', 'url' => '/shop/', ], ]; } ); ``` *** ### swftcart\_theme\_vars [Section titled “swftcart\_theme\_vars”](#swftcart_theme_vars) Override CSS custom property values for the cart drawer theme. ```php apply_filters( 'swftcart_theme_vars', array $vars ): array ``` See [Themes](/swft-cart/themes) for the full variable list. *** ### swftcart\_delivery\_data [Section titled “swftcart\_delivery\_data”](#swftcart_delivery_data) Set the data for the delivery countdown module. ```php apply_filters( 'swftcart_delivery_data', array $data ): array ``` **Structure:** ```php [ 'cutoff_time' => '14:00', // 24h time — order by this time for same-day 'timezone' => 'Europe/London', 'message_before' => 'Order in {time} for same-day dispatch', 'message_after' => 'Order now for next-day dispatch', ] ``` *** ### swftcart\_upsell\_ids [Section titled “swftcart\_upsell\_ids”](#swftcart_upsell_ids) Set the product IDs shown in the upsell module. ```php apply_filters( 'swftcart_upsell_ids', array $ids ): array ``` **Example — return bestsellers dynamically:** ```php add_filter( 'swftcart_upsell_ids', function( array $ids ): array { $bestsellers = wc_get_products( [ 'limit' => 6, 'orderby' => 'popularity', 'order' => 'DESC', 'status' => 'publish', ] ); return array_map( fn( $p ) => $p->get_id(), $bestsellers ); } ); ``` # SwftCheckout API — window.SwftCheckout `window.SwftCheckout` is available after the `swftcheckout:ready` event fires. Always wait for this event before calling any methods. ```js document.addEventListener('swftcheckout:ready', () => { // window.SwftCheckout is now available }) ``` *** ## Methods [Section titled “Methods”](#methods) ### .on(event, fn) [Section titled “.on(event, fn)”](#onevent-fn) Subscribe to a `swftcheckout:*` event. The `event` parameter should be the suffix after `swftcheckout:`. ```ts on(event: string, fn: (detail: unknown) => void): void ``` ```js window.SwftCheckout.on('payment-complete', (detail) => { console.log(detail.orderRef) }) ``` Equivalent to `document.addEventListener('swftcheckout:payment-complete', ...)`. *** ### .off(event, fn) [Section titled “.off(event, fn)”](#offevent-fn) Unsubscribe from a `swftcheckout:*` event. ```ts off(event: string, fn: (detail: unknown) => void): void ``` *** ### .addOrderRow(row) [Section titled “.addOrderRow(row)”](#addorderrowrow) Inject a custom row into the order summary. Rows are rendered between the line items and the total. Use `pos` to control placement. ```ts addOrderRow(row: SwftOrderRow): void ``` **SwftOrderRow:** ```ts interface SwftOrderRow { id: string // unique identifier label: string // label text value: string // formatted value string (e.g. '£5.00' or '-£5.00') pos?: 'before-subtotal' | 'after-subtotal' | 'after-total' // default: 'after-subtotal' } ``` If a row with the same `id` already exists, it is replaced in-place. ```js document.addEventListener('swftcheckout:ready', () => { window.SwftCheckout.addOrderRow({ id: 'gift-wrap', label: 'Gift wrap', value: '£2.99', pos: 'after-subtotal', }) }) ``` *** ### .removeOrderRow(id) [Section titled “.removeOrderRow(id)”](#removeorderrowid) Remove a custom order row by its `id`. ```ts removeOrderRow(id: string): void ``` *** ### .addCustomField(field) [Section titled “.addCustomField(field)”](#addcustomfieldfield) Inject a custom form field into the checkout. Fields appear in the details step or payment step depending on the `section` property. ```ts addCustomField(field: SwftCustomField): void ``` **SwftCustomField:** ```ts interface SwftCustomField { id: string label: string type: 'text' | 'textarea' | 'select' | 'checkbox' placeholder?: string required?: boolean options?: Array<{ value: string; label: string }> // for type: 'select' only section?: 'details' | 'payment' // default: 'details' } ``` If a field with the same `id` already exists, it is replaced in-place. ```js document.addEventListener('swftcheckout:ready', () => { window.SwftCheckout.addCustomField({ id: 'gift-message', label: 'Gift message (optional)', type: 'textarea', placeholder: 'Write something nice...', section: 'details', }) }) ``` Custom field values entered by the customer are submitted alongside the payment intent. They are available in the order meta as `_swft_field_{id}`. *** ### .removeCustomField(id) [Section titled “.removeCustomField(id)”](#removecustomfieldid) Remove a custom field by its `id`. ```ts removeCustomField(id: string): void ``` *** ### .getExtension(key) [Section titled “.getExtension(key)”](#getextensionkey) Read extension data from the session. Data was added by a WP plugin via the `swft_session_extensions` filter. ```ts getExtension(key: string): T | undefined ``` ```js document.addEventListener('swftcheckout:ready', () => { const gifts = window.SwftCheckout.getExtension('gift_options') // { message: '...', wrap: true } if (gifts?.wrap) { window.SwftCheckout.addOrderRow({ id: 'gift-wrap-fee', label: 'Gift wrap', value: '£2.99', }) } }) ``` Returns `undefined` if the key does not exist in the extensions object. *** ## Properties [Section titled “Properties”](#properties) ### .version [Section titled “.version”](#version) The version of the SwftCheckout API. ```ts version: string // e.g. '1.0.0' ``` *** ### .session [Section titled “.session”](#session) The full `SwftSession` object (same as `window.__SWFT_SESSION__`). ```ts session: SwftSession | null ``` **SwftSession shape:** ```ts interface SwftSession { sessionId: string status: 'pending' | 'processing' | 'complete' | 'expired' merchant: Merchant cart: Cart shippingMethods: ShippingMethod[] stripePublishableKey: string mapboxToken: string | null extensions: Record // Set only after payment completes: orderRef?: string deliveryAddress?: DeliveryAddress deliveryCoords?: { lat: number; lng: number } estimatedDelivery?: string } interface Merchant { id: string name: string logoUrl: string | null accentColor: string | null storeUrl: string wcOrderUrl: string | null googleMapsApiKey?: string checkoutTemplate?: 'minimal' | 'split' | 'express' policies?: { returns?: string shipping?: string privacy?: string } } interface Cart { currency: string // ISO 4217 items: LineItem[] subtotal: number // pence tax: number // pence total: number // pence needsShipping?: boolean } ``` *** ## window.**SWFT\_SESSION** [Section titled “window.SWFT\_SESSION”](#windowswft_session) The raw session object injected by the Cloudflare Worker. Identical to `SwftCheckout.session` before payment, updated to `status: 'complete'` with `orderRef` after `swftcheckout:payment-complete` fires. ```js console.log(window.__SWFT_SESSION__.extensions) // { gift_options: { message: '...', wrap: true } } ``` This object is set before the React app boots, making it available immediately without waiting for `swftcheckout:ready`. # Overview Swft is two things: a **WordPress plugin** and a **hosted checkout**. The plugin intercepts the WooCommerce checkout page and redirects customers to `checkout.swft.co.uk`. Sessions are pre-created on cart update so the redirect lands on an already-loaded page. The checkout itself is a React application served from Cloudflare Pages, rendered by a Cloudflare Worker that injects the session as `window.__SWFT_SESSION__`. Your store stays on WordPress. Only checkout moves to the edge. ## Components [Section titled “Components”](#components) | Component | What it does | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Swft Checkout plugin** | Serialises the WooCommerce cart, calls the Swft API to create a session, and redirects to `checkout.swft.co.uk/{sessionId}` | | **Swft API** (`api.swft.co.uk`) | Validates the cart payload, creates the session in Supabase and Cloudflare KV, creates the Stripe PaymentIntent on `/pay`, and handles webhooks | | **Cloudflare Worker** | Serves `checkout.swft.co.uk/{sessionId}` — reads session from KV (fast path) or falls back to API, injects it into the HTML shell | | **Checkout frontend** | React SPA at `checkout.swft.co.uk` — two-step form, Stripe Elements, order summary, Express checkout (Apple/Google Pay) | | **Swft Cart** | Optional free WordPress plugin — a slide-in cart drawer with 18 modules | ## Data flow [Section titled “Data flow”](#data-flow) ```plaintext Customer clicks "Checkout" → WooCommerce checkout page → Swft plugin: check transient cache for pre-created session → HIT: instant redirect to checkout.swft.co.uk/{sessionId} → MISS: POST to api.swft.co.uk/sessions → get sessionUrl → redirect checkout.swft.co.uk/{sessionId} → Cloudflare Worker: read session from KV → KV HIT: inject window.__SWFT_SESSION__ into HTML → KV MISS: GET api.swft.co.uk/sessions/{id} → inject → React app boots, reads window.__SWFT_SESSION__ Customer completes details step → POST api.swft.co.uk/sessions/{id}/pay → Stripe PaymentIntent created → Stripe Elements shown Customer pays → Stripe webhook: payment_intent.succeeded → api.swft.co.uk/webhooks/stripe → WooCommerce order created via REST API (status: processing) → Confirmation screen shown ``` ## Requirements [Section titled “Requirements”](#requirements) | Requirement | Minimum | | -------------- | ------------------------------------------------------ | | WordPress | 6.0 | | WooCommerce | 8.0 | | PHP | 8.0 | | Stripe account | Required for payments | | Swft API key | Required — get one at [swft.co.uk](https://swft.co.uk) | ## What Swft does not change [Section titled “What Swft does not change”](#what-swft-does-not-change) * Product pages, cart page, account pages — untouched * WooCommerce order management — all orders land in WP Admin as normal * Existing payment gateways — Swft replaces checkout only; other gateways remain for orders placed outside Swft * Coupons and discounts — calculated by WooCommerce, passed through in the session payload # Address Autocomplete Swft uses the Google Places Autocomplete API to provide real-time address suggestions as the shopper types their postcode or street address. Selecting a suggestion populates all address fields instantly — street, city, county, postcode, and country. ## How it works [Section titled “How it works”](#how-it-works) The autocomplete field replaces the standard address line 1 input. As the shopper types, suggestions are fetched from the Places API restricted to the countries enabled for the merchant. Selecting a result triggers a Place Details lookup that returns structured address components, which Swft maps to WooCommerce billing and shipping field equivalents. Field mapping: | Places component | Swft field | | ----------------------------- | -------------- | | `street_number` + `route` | Address line 1 | | `subpremise` | Address line 2 | | `postal_town` or `locality` | City | | `administrative_area_level_2` | County / state | | `postal_code` | Postcode | | `country` | Country | ## Configuration [Section titled “Configuration”](#configuration) Address autocomplete is enabled by default for all merchants. To disable it: ```json { "modules": { "addressAutocomplete": false } } ``` To restrict autocomplete to specific countries (recommended — reduces noise for single-market merchants): ```json { "modules": { "addressAutocomplete": { "countries": ["GB", "IE"] } } } ``` ## API key [Section titled “API key”](#api-key) Swft manages the Google Places API key at the platform level. You do not need a Google Cloud account or a Places API key. Usage is included in all Swft plans. Tip If your checkout serves multiple countries, leave `countries` unconfigured. The Places API will suggest addresses globally and Swft will select the appropriate country code. ## Fallback [Section titled “Fallback”](#fallback) If the Places API request fails (network error, quota exceeded), the autocomplete field degrades to a standard text input. All address fields remain editable manually, so no shopper is ever blocked. # B2B Checkout Mode B2B mode adds business-specific fields to the checkout form: company name, VAT number with real-time validation, purchase order (PO) number, and configurable payment terms. All collected data is stored as order meta in WooCommerce. ## Enabling B2B mode [Section titled “Enabling B2B mode”](#enabling-b2b-mode) Pass `b2b: true` when creating a session: ```bash curl -X POST https://api.swft.co.uk/v1/sessions \ -H "Authorization: Bearer $SWFT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "cart": { "b2b": true, "lineItems": [...] } }' ``` You can also enable it globally for a merchant so every session defaults to B2B mode: ```bash curl -X PATCH https://api.swft.co.uk/v1/merchants/{id} \ -H "Authorization: Bearer $SWFT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"defaults": {"b2b": true}}' ``` ## Fields added in B2B mode [Section titled “Fields added in B2B mode”](#fields-added-in-b2b-mode) | Field | Required | Notes | | ------------- | -------- | ---------------------------------------------------- | | Company name | Yes | Stored as `_billing_company` on the order | | VAT number | No | Validated in real time; stored as `_swft_vat_number` | | PO number | No | Free text; stored as `_swft_po_number` | | Payment terms | No | Shown if `paymentTerms` options are configured | ### Company name [Section titled “Company name”](#company-name) Replaces the standard “First name / Last name” row with “Company name” plus a condensed name row beneath it. The contact name is still collected for order fulfilment purposes. ### VAT number [Section titled “VAT number”](#vat-number) The VAT field appears below the company name. As the shopper types, Swft validates the number format client-side using a country-code prefix regex before triggering a live lookup. ## VAT validation [Section titled “VAT validation”](#vat-validation) Swft validates VAT numbers against two authoritative sources depending on the country prefix: **EU member states** — validated via the [VIES SOAP API](https://ec.europa.eu/taxation_customs/vies/). A valid VIES response returns the registered business name, which is shown to the shopper as confirmation. If VIES is temporarily unavailable (it has planned downtime windows), validation is skipped and the number is accepted with a warning flag stored on the order. **GB (post-Brexit)** — validated via the [HMRC VAT number API](https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/vat-registered-businesses-api/1.0). The same confirmation name display applies. **Other countries** — format validation only (no live lookup). The number is stored as entered. When a valid VAT number is confirmed, the checkout automatically removes VAT from the order total if the merchant’s WooCommerce store has the “EU VAT Assistant” or “WooCommerce EU VAT Number” plugin installed and configured for zero-rating B2B sales. This is handled via the `swft_session_tax_override` filter. ```php add_filter( 'swft_session_tax_override', function( $tax, $session ) { if ( $session['b2b']['vatValid'] && $session['b2b']['vatCountry'] !== 'GB' ) { return 0; // zero-rate validated EU business customers } return $tax; }, 10, 2 ); ``` ## PO number [Section titled “PO number”](#po-number) An optional free-text field for the customer’s internal purchase order reference. Stored as `_swft_po_number` on the WooCommerce order and displayed in the order confirmation email if the `swft_order_email_po_number` filter returns `true`. ## Payment terms [Section titled “Payment terms”](#payment-terms) Configure available terms in the merchant config: ```json { "b2b": { "paymentTerms": [ { "label": "Pay now", "value": "immediate", "default": true }, { "label": "Net 14", "value": "net14" }, { "label": "Net 30", "value": "net30" } ] } } ``` When `paymentTerms` is configured, a selector appears in the checkout. Selecting a deferred term (anything other than `immediate`) skips the card step and places the order with the WooCommerce status `on-hold`. Your accounts team or an automation (e.g. WooCommerce Subscriptions, Xero integration) is then responsible for collecting payment. The selected term is stored as `_swft_payment_terms` on the order. ## WooCommerce order meta [Section titled “WooCommerce order meta”](#woocommerce-order-meta) A completed B2B order carries the following additional meta: | Meta key | Value | | --------------------- | ------------------------------------------------- | | `_billing_company` | Company name as entered | | `_swft_vat_number` | VAT number string | | `_swft_vat_valid` | `1` if VIES/HMRC confirmed, `0` otherwise | | `_swft_vat_country` | ISO 3166-1 alpha-2 country code of the VAT number | | `_swft_vat_name` | Registered business name returned by VIES/HMRC | | `_swft_po_number` | Purchase order reference | | `_swft_payment_terms` | Selected payment terms value | Caution VAT validation failures (VIES downtime, invalid format) do not block order placement. The `_swft_vat_valid` meta key lets you handle these cases in post-order automation. # Buy Now Pay Later (BNPL) Swft supports Klarna, Clearpay (Afterpay), Affirm, and other BNPL providers automatically — no configuration required. ## How it works [Section titled “How it works”](#how-it-works) Swft uses Stripe’s `PaymentElement` with `automatic_payment_methods: { enabled: true }`. Stripe automatically shows available payment methods based on: * The customer’s **location** (Klarna available in DE, UK, SE, NL, US etc.) * The **order amount** (some BNPL providers have minimums) * Your Stripe account’s **enabled payment methods** ## Enabling BNPL [Section titled “Enabling BNPL”](#enabling-bnpl) ### 1. Go to your Stripe Dashboard [Section titled “1. Go to your Stripe Dashboard”](#1-go-to-your-stripe-dashboard) → [Payment methods settings](https://dashboard.stripe.com/settings/payment_methods) ### 2. Enable your preferred BNPL providers [Section titled “2. Enable your preferred BNPL providers”](#2-enable-your-preferred-bnpl-providers) | Provider | Markets | Min. order | | ------------------- | ------------------------- | ---------- | | Klarna | UK, EU, US, AU, CA + more | \~£1 | | Afterpay / Clearpay | UK, AU, US, CA, NZ | £1–£2,000 | | Affirm | US, CA | $50 | | Zip | AU, US, UK | Varies | ### 3. That’s it [Section titled “3. That’s it”](#3-thats-it) Swft’s checkout automatically shows the BNPL button when a customer is eligible. No plugin updates, no code changes. ## What the customer sees [Section titled “What the customer sees”](#what-the-customer-sees) When Klarna is enabled and the customer is eligible, the PaymentElement shows a “Pay with Klarna” option alongside the card form. Klarna handles the instalment plan — your WooCommerce order is created for the full amount immediately. ## WooCommerce orders [Section titled “WooCommerce orders”](#woocommerce-orders) BNPL orders create native WooCommerce orders exactly like card orders. You receive the full order amount from Stripe (Klarna/Clearpay pays you immediately). The customer pays Klarna/Clearpay in instalments — that’s between them and the BNPL provider. ## Fees [Section titled “Fees”](#fees) BNPL providers charge merchants a fee (typically 2–6% depending on provider and country). Swft’s 2% platform fee applies on top. For merchants on the ambassador tier (1.8%) or agency tier (1.5%), those rates apply to the full transaction amount. Check each provider’s merchant fees in the Stripe Dashboard. ## Stripe Connect [Section titled “Stripe Connect”](#stripe-connect) If you’re using Swft with Stripe Connect (the default for all Swft merchants), BNPL is routed through your connected account. The application fee is taken on the full amount before the BNPL provider fee. Everything settles to your Stripe account normally. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) **BNPL button not showing?** * Confirm the payment method is enabled in your Stripe Dashboard → Payment methods * Check the customer’s location is supported by that provider * Verify the order amount meets the provider’s minimum **BNPL available in some countries only?** * This is by design. Klarna, for example, is not available in all markets. Stripe shows only what’s available for each customer. **Can I disable BNPL?** * Yes — disable it in your Stripe Dashboard → Payment methods. Swft won’t show options that aren’t enabled in Stripe. # Connecting Klyme (Pay by Bank) Klyme is a UK Open Banking provider. Shoppers pay by authorising a bank transfer in their banking app — no card details, no chargebacks, instant settlement. Swft collects a 2% platform fee, billed monthly. ## When to use Klyme [Section titled “When to use Klyme”](#when-to-use-klyme) Klyme is best for **UK-only shoppers** paying mid-to-high-ticket items. It works for any cart size but its main value is bypassing card-network fees (typically 1.5%+) for larger transactions where 2% Swft + Klyme’s own tariff is still cheaper than card processing. Klyme is not available outside the UK. Swft auto-hides the Pay by Bank button for shoppers in non-UK billing countries. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A Klyme merchant account ([klyme.io](https://klyme.io)) * Your Swft API key already saved in **Settings → Swft Checkout** ## Get your Klyme credentials [Section titled “Get your Klyme credentials”](#get-your-klyme-credentials) Klyme issues three values per merchant: * **Merchant UUID** — your unique identifier in Klyme’s system. * **Username** — HTTP Basic auth username for API calls. * **Password** — HTTP Basic auth password. Find these in your Klyme merchant dashboard under **Settings → API credentials**. If you don’t see them, contact your Klyme account manager — they may need to enable API access for your merchant profile. For testing, Klyme provides a separate sandbox dashboard at `api-test.klyme.io` with its own UUID and credentials. ## Connect Klyme in Swft [Section titled “Connect Klyme in Swft”](#connect-klyme-in-swft) 1. Go to your [Swft Dashboard](https://app.swft.co.uk) → **Settings → Payments**. 2. Find the **Klyme (Pay by Bank)** card. 3. Paste your **Merchant UUID**, **Username**, and **Password**. 4. If you’re testing with sandbox credentials, leave **Live mode** OFF. Switch it ON for production. 5. Tick **Enable Klyme**. 6. Click **Save**. The Pay by Bank button now appears above the card form for UK shoppers. ## How payments work [Section titled “How payments work”](#how-payments-work) When a UK shopper clicks Pay by Bank: 1. Swft’s API requests a payment authorisation from Klyme using your credentials. 2. Klyme returns a checkout URL; Swft redirects the shopper. 3. Shopper picks their bank, authenticates in their banking app, and approves the transfer. 4. Bank confirms to Klyme → Klyme confirms to Swft → Swft marks the session complete and creates a paid WooCommerce order with `payment_method: 'klyme'`. 5. Shopper sees the order confirmation page. Funds settle directly to your nominated bank account under Klyme’s payout schedule (typically same-day for UK Faster Payments). Klyme’s own per-transaction fees apply — refer to your Klyme contract. ## Pending payments [Section titled “Pending payments”](#pending-payments) Some bank transfers are **asynchronous** — the shopper authorises in their app but the funds take seconds to minutes to clear. Swft handles this by marking the session “pending” on return; once Klyme confirms settlement, the session and WC order flip to complete. If a shopper returns to your site before settlement, they see a “We’re confirming your payment with your bank” message and an email receipt arrives once cleared. ## Platform fee [Section titled “Platform fee”](#platform-fee) Swft’s 2% platform fee on Klyme transactions is **billed monthly** via Stripe Billing, against the card you have on file. Open Banking doesn’t support marketplace-style fee skims at the protocol layer. You’ll see a single Swft invoice each month covering accumulated fees from all out-of-band gateways (PayPal, Klyme, NomuPay). ## Refunds [Section titled “Refunds”](#refunds) Open Banking has no native refund mechanism — transactions are one-way bank transfers. To refund a Klyme order: 1. Issue a refund from your **Klyme dashboard** → the order. Klyme initiates a reverse Faster Payment to the customer’s bank. 2. Mark the WC order refunded manually in WooCommerce → Orders. This is by design at the protocol level, not a Swft limitation. ## Test mode [Section titled “Test mode”](#test-mode) To test without real bank transfers: 1. Use **sandbox** Klyme credentials (provided when you sign up; if missing, request from Klyme support). 2. In Swft, paste them with **Live mode** OFF. 3. Run a checkout and pick Pay by Bank. Klyme’s sandbox bank picker shows mock banks you can authorise against without real funds moving. ## Disconnecting Klyme [Section titled “Disconnecting Klyme”](#disconnecting-klyme) In **Settings → Payments**, uncheck **Enable Klyme**. The Pay by Bank button disappears for new shoppers. Existing pending transactions continue to settle. To fully remove credentials, blank out the UUID, username, and password and save. # Connecting NomuPay (Total Processing) NomuPay is the payment processor behind Total Processing’s OPPWA platform. It’s widely used by UK competition, lottery, and prize-draw merchants. Swft renders a custom card form using OPPWA’s hosted fields (so card data never touches your server — you stay in PCI SAQ-A scope) and also surfaces Apple Pay and Google Pay through the same gateway. Swft collects a 2% platform fee, billed monthly. ## When to use NomuPay [Section titled “When to use NomuPay”](#when-to-use-nomupay) Use NomuPay if you already have an account with **NomuPay / Total Processing** — typically because you run a competition, lottery, or other high-risk vertical that processors like Stripe won’t underwrite. For most general retail, Stripe is simpler. NomuPay shines specifically when: * Your existing PSP relationship is with Total Processing * You need card payments for verticals that other PSPs decline * You want Apple Pay / Google Pay through NomuPay rather than Stripe You can run NomuPay alongside Stripe — both render in the checkout and the shopper picks one. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A NomuPay / Total Processing merchant account * The **WooCommerce NomuPay plugin** installed and configured on your WordPress site (required for refunds — see below) * Your Swft API key already saved in **Settings → Swft Checkout** ## Get your OPPWA credentials [Section titled “Get your OPPWA credentials”](#get-your-oppwa-credentials) NomuPay issues credentials per environment (sandbox + production): * **EntityID** — a 32-character identifier per payment brand / channel. * **Access Token** — Bearer token used to authenticate API calls. * **Webhook Secret** — a 64-character hex string used to verify incoming webhooks. Sandbox credentials are not self-serve — request them from NomuPay support at . Production credentials come with your merchant onboarding pack. If you already use the WooCommerce NomuPay plugin, you’ll have these in your NomuPay merchant dashboard under **API access**. ## Connect NomuPay in Swft [Section titled “Connect NomuPay in Swft”](#connect-nomupay-in-swft) 1. Go to your [Swft Dashboard](https://app.swft.co.uk) → **Settings → Payments**. 2. Find the **NomuPay** card. 3. Fill in: * **Test EntityID** + **Test Access Token** (sandbox) * **Live EntityID** + **Live Access Token** (production) * **Webhook Secret** (64-char hex) 4. Pick the **Payment brands** you want to accept: VISA, MASTER, AMEX, plus optionally APPLEPAY and GOOGLEPAY. 5. If you’re testing, leave **Live mode** OFF. Switch ON for production. 6. Tick **Enable saved cards** if you want returning shoppers to re-use stored card tokens. 7. Tick **Enable NomuPay**. 8. Click **Save**. The NomuPay card form now appears in your checkout’s payment section. ## Set up the OPPWA webhook [Section titled “Set up the OPPWA webhook”](#set-up-the-oppwa-webhook) NomuPay confirms successful payments via a server-to-server push webhook (this is the canonical settlement source — the browser redirect is best-effort). 1. In the **NomuPay merchant portal**, go to **Webhooks** (sometimes under **Server-to-Server notifications**). 2. Add a new webhook: * **URL**: `https://api.swft.co.uk/webhooks/nomupay` * **Events**: `PAYMENT` (at minimum); `REGISTRATION` and `RISK` if you want them for telemetry. * **Secret**: paste the same 64-char hex you entered in Swft. 3. Save. Swft verifies every webhook payload using AES-256-GCM (OPPWA’s documented scheme). Tampered payloads are rejected with a 400; replays are idempotent. ## Set up Apple Pay (optional) [Section titled “Set up Apple Pay (optional)”](#set-up-apple-pay-optional) If you enabled `APPLEPAY` in your payment brands: 1. In the NomuPay portal, add the domain `checkout.swft.co.uk` to your Apple Pay domains list. 2. NomuPay gives you a verification string. Send it to Swft support and we’ll deploy it to `https://checkout.swft.co.uk/.well-known/apple-developer-merchantid-domain-association` (Apple requires this file to live on the same origin that hosts the checkout). 3. Once verified, the Apple Pay button appears automatically inside the NomuPay card form on supported devices (Safari + Touch ID / Face ID). If you also use a [Swft custom domain](/swft-checkout/custom-domains), repeat the domain registration for that origin too. Google Pay works out of the box once you’ve enabled the `GOOGLEPAY` brand — no domain verification step required. ## How payments work [Section titled “How payments work”](#how-payments-work) When a shopper enters their card in the NomuPay form: 1. OPPWA’s `paymentWidgets.js` tokenises the card inside per-field iframes — the raw PAN never reaches your server. 2. OPPWA processes the payment, including 3D Secure 2 challenges where required. 3. The shopper is redirected to `https://checkout.swft.co.uk/return/nomupay?session=...&resourcePath=...`. 4. The Swft return page calls our API, which fetches the canonical result from OPPWA and: * Marks the session complete on success. * Records the platform fee event. * Creates a paid WooCommerce order tagged for the WC NomuPay plugin to handle refunds. 5. Shopper sees the order confirmation page. If the OPPWA result is asynchronous (rare), the return page shows a “confirming with your bank” message and the OPPWA webhook completes the session out-of-band — the shopper gets an email receipt once settled. ## Saved cards [Section titled “Saved cards”](#saved-cards) If **Enable saved cards** is ticked, OPPWA stores a tokenised reference (an OPPWA “registration”) after the shopper’s first successful payment. On returning visits with the same email, the saved card option appears alongside the new-card form. Stored cards are scoped per merchant: a card stored for merchant A is never visible to merchant B. Tokens are revocable — shoppers can remove saved cards from the dashboard (or by emailing your support). ## Platform fee [Section titled “Platform fee”](#platform-fee) Swft’s 2% platform fee on NomuPay transactions is **billed monthly** via Stripe Billing, against the card you have on file. OPPWA doesn’t support marketplace-style fee skims at the gateway layer. You’ll see a single Swft invoice each month covering accumulated fees from all out-of-band gateways (PayPal, Klyme, NomuPay). ## Refunds [Section titled “Refunds”](#refunds) Refunds happen in WooCommerce, **not** in Swft. This is by design — we don’t get involved in money movement. For this to work, you need both plugins installed and active: 1. **Swft Checkout** plugin (handles checkout itself) 2. [**NomuPay Payment Gateway for WooCommerce**](https://wordpress.org/plugins/totalprocessing-card-payments/) (handles refunds via NomuPay’s API) Swft-created NomuPay orders are tagged with `_payment_method = wc_tp_cardsv3` plus the meta (`_transaction_id`, `platformBase`, `paymentType`) that the WC NomuPay plugin’s refund handler expects. The standard **Refund** button in WC → Orders works without further setup. Configure both plugins with the **same** NomuPay credentials. If they diverge, the Swft side may create an order against one OPPWA account while the WC plugin tries to refund against another — and the refund will fail. ## Test mode [Section titled “Test mode”](#test-mode) To test without real money: 1. Request sandbox credentials from . 2. Paste them as **Test EntityID** + **Test Access Token** in Swft, with **Live mode** OFF. 3. Run a checkout. Use OPPWA’s test card numbers (NomuPay support provides the current list; commonly `4200 0000 0000 0000` for Visa with any future expiry and CVV). Sandbox payments appear in your NomuPay test dashboard, not the live one. ## Disconnecting NomuPay [Section titled “Disconnecting NomuPay”](#disconnecting-nomupay) In **Settings → Payments**, uncheck **Enable NomuPay**. The NomuPay form disappears from the checkout immediately. Existing pending payments continue to settle via the webhook. To fully remove credentials, blank out the EntityID, Access Token, and Webhook Secret fields and save again. Also delete the webhook in the NomuPay merchant portal. # Connecting PayPal Swft adds a native PayPal button to the checkout payment section. The button uses PayPal’s official JS SDK, so shoppers stay on your checkout — they’re not redirected to PayPal.com — and your PayPal account receives funds directly. Swft collects a 2% platform fee, billed monthly. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A PayPal Business account (free at [paypal.com/business](https://www.paypal.com/business)) * Your Swft API key already saved in **Settings → Swft Checkout** ## Get your API credentials [Section titled “Get your API credentials”](#get-your-api-credentials) PayPal exposes per-app credentials. You need a **Client ID** and **Client Secret** for one of your PayPal apps. ### 1. Open the PayPal developer dashboard [Section titled “1. Open the PayPal developer dashboard”](#1-open-the-paypal-developer-dashboard) → [developer.paypal.com/dashboard/applications](https://developer.paypal.com/dashboard/applications) ### 2. Create (or pick) an app [Section titled “2. Create (or pick) an app”](#2-create-or-pick-an-app) * **Live mode**: use an app under **Live → My Apps & Credentials**. * **Sandbox mode**: use an app under **Sandbox → My Apps & Credentials**. Sandbox apps come with a free test PayPal buyer account so you can test end-to-end without real money. If you don’t have an app yet, click **Create App**, choose **Merchant**, give it a name (e.g. “Swft Checkout”), and select the sandbox or live business account to attach. ### 3. Copy the Client ID and Secret [Section titled “3. Copy the Client ID and Secret”](#3-copy-the-client-id-and-secret) On the app detail page, click **Show** next to the Secret. Copy both values somewhere safe. ## Connect PayPal in Swft [Section titled “Connect PayPal in Swft”](#connect-paypal-in-swft) 1. Go to your [Swft Dashboard](https://app.swft.co.uk) → **Settings → Payments**. 2. Find the **PayPal** card. 3. Paste your **Client ID** and **Client Secret**. 4. If you’re testing with PayPal Sandbox credentials, leave **Live mode** OFF. If you’re using your real PayPal Business app, switch it ON. 5. Tick **Enable PayPal**. 6. Click **Save**. The PayPal button now appears in your checkout’s payment section between Card and BNPL. It only appears for shoppers in countries where PayPal supports buyer accounts — Swft hides it automatically elsewhere. ## How payments work [Section titled “How payments work”](#how-payments-work) When a shopper clicks the PayPal button: 1. The PayPal SDK opens an in-page PayPal login (no full-page redirect). 2. Shopper authorises the payment against their PayPal balance or linked card. 3. Swft’s API calls PayPal’s `orders/{id}/capture` endpoint server-side, using your credentials. 4. On capture success, Swft marks the session complete and creates a paid WooCommerce order with `payment_method: 'paypal'`. 5. The shopper sees the order confirmation page. Funds settle into your PayPal Business balance under your standard PayPal payout schedule. PayPal’s own per-transaction fees apply (refer to your PayPal Business pricing). ## Platform fee [Section titled “Platform fee”](#platform-fee) Swft’s 2% platform fee on PayPal transactions is **billed monthly** via Stripe Billing, against the card you have on file. This is because PayPal doesn’t expose a marketplace-style fee skim at the gateway layer. You’ll see a single Swft invoice each month covering the accumulated fees from all out-of-band gateways (PayPal, Klyme, NomuPay). The invoice itemises the gateway, the order count, and the total fee — no surprise line items. ## Refunds [Section titled “Refunds”](#refunds) Refunds happen in WooCommerce, not Swft. If you use the [WooCommerce PayPal Payments plugin](https://wordpress.org/plugins/woocommerce-paypal-payments/) (recommended), the refund button in WC → Orders calls PayPal directly and works out of the box. Otherwise, issue refunds from your PayPal Business dashboard and mark the WC order refunded manually. ## Test mode [Section titled “Test mode”](#test-mode) To test without real money: 1. Use **Sandbox** credentials from `developer.paypal.com → Sandbox → My Apps & Credentials`. 2. In Swft, paste the sandbox Client ID/Secret and leave **Live mode** OFF. 3. Create a [sandbox personal buyer account](https://developer.paypal.com/dashboard/accounts) if you don’t have one. 4. Run a checkout, click PayPal, log in with the sandbox buyer credentials, and complete the test payment. Test payments appear in your sandbox PayPal account, not your live account. ## Disconnecting PayPal [Section titled “Disconnecting PayPal”](#disconnecting-paypal) In **Settings → Payments**, uncheck **Enable PayPal**. The PayPal button disappears from the checkout immediately. Your stored credentials are kept (so you can re-enable later without re-entering them); to fully remove, blank out the Client ID and Secret fields and save again. Disabling PayPal in Swft does not affect any pending PayPal transactions — those continue to capture and settle normally. # Connecting Stripe Swft uses **Stripe Connect** to route payments directly to your Stripe account. Swft collects a 2% platform fee automatically; you receive the remainder in your Stripe balance. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A Stripe account (free to create at [stripe.com](https://stripe.com)) * Your Swft API key already saved in **Settings → Swft Checkout** ## OAuth flow [Section titled “OAuth flow”](#oauth-flow) 1. Go to **Settings → Swft Checkout** 2. In the **Stripe Connection** section, click **Connect with Stripe** 3. You are redirected to Stripe’s OAuth authorisation page 4. Log in to Stripe (or create an account) and click **Connect** 5. Stripe redirects back to your WordPress admin with `?stripe_connected=1` 6. The settings page shows **Connected** with your Stripe account ID If you are in the setup wizard, step 2 automatically advances to step 3 after a successful connection. ## Connection status [Section titled “Connection status”](#connection-status) The **Stripe Connection** field in Settings shows one of: | Status | Meaning | | ------------- | ---------------------------------------------- | | Connected | OAuth complete; payments route to your account | | Not connected | No OAuth token — payments cannot be processed | ## Disconnecting Stripe [Section titled “Disconnecting Stripe”](#disconnecting-stripe) In **Settings → Swft Checkout**, click **Disconnect** in the Stripe Connection field. This clears the stored Stripe account ID. Swft cannot process payments until you reconnect. Disconnecting from the Swft settings page does not revoke the connection on the Stripe side. To fully revoke, also go to **Stripe Dashboard → Settings → Connected apps** and remove Swft. ## Platform fee [Section titled “Platform fee”](#platform-fee) Swft collects a **2% platform fee** on every transaction using Stripe Connect’s `application_fee_amount`. This is deducted from each payment before funds are transferred to your account. The fee appears on your Stripe dashboard as a transfer deduction. ## Test mode [Section titled “Test mode”](#test-mode) To test without real charges: 1. Connect a Stripe account that has test mode enabled 2. Use Stripe’s test card numbers (e.g. `4242 4242 4242 4242`, any future expiry, any CVC) 3. Check your Stripe test dashboard for payment intents and transfers Swft does not have a separate test mode toggle — it follows the mode of the connected Stripe account. # Express Checkout Apple Pay and Google Pay appear automatically above the contact form when the shopper’s device and browser support them. No configuration is required — Swft detects wallet availability via the [Stripe Payment Request API](https://stripe.com/docs/stripe-js/elements/payment-request-button). ## How it works [Section titled “How it works”](#how-it-works) When a shopper reaches the checkout, Swft calls `stripe.paymentRequest()` with the cart total, currency, and line items from the pre-created session. The browser then calls `canMakePayment()` to determine whether the device has a configured wallet. ```js const paymentRequest = stripe.paymentRequest({ country: 'GB', currency: 'gbp', total: { label: 'Order total', amount: session.totalPence, }, requestPayerName: true, requestPayerEmail: true, requestShipping: true, shippingOptions: session.shippingOptions, }) const canPay = await paymentRequest.canMakePayment() ``` If `canMakePayment()` returns `null` — which happens when the device has no configured cards, Apple Pay domain verification has failed, or the browser does not support the API — the express button row is hidden entirely. The standard form is always rendered regardless, so no shopper ever sees a broken checkout. ### Button row behaviour [Section titled “Button row behaviour”](#button-row-behaviour) The express button row sits above the email field, separated from the main form by an “or” divider. Up to two buttons render side by side: Apple Pay on Safari/WebKit, Google Pay on Chrome/Edge. On Android devices, Google Pay renders full-width. After the shopper authenticates with Face ID, Touch ID, or their Google account, Swft receives a `paymentmethod.created` event from Stripe, confirms the PaymentIntent on the edge, and redirects to the order confirmation page — the same flow as a standard form submission. Shipping address selection happens within the native wallet sheet. Swft listens to `shippingaddresschange` events and recalculates available shipping options in real time by calling the session API. ## Requirements [Section titled “Requirements”](#requirements) * **HTTPS** — required by browsers for the Payment Request API. `localhost` is allowed for development. * **Stripe Apple Pay domain verification** — Swft automatically serves the `.well-known/apple-developer-merchantid-domain-association` file for `app.swft.co.uk`. If you use a [custom domain](/swft-checkout/custom-domains), you must complete domain verification in the Stripe dashboard for that domain. * **Stripe account country** — Apple Pay requires the Stripe account to be registered in a supported country. Tip Google Pay works without any domain verification step. It is available in all markets where Stripe supports card payments. ## Disabling per merchant [Section titled “Disabling per merchant”](#disabling-per-merchant) Set `modules.expressCheckout: false` in your merchant config via the dashboard or the API: ```json { "modules": { "expressCheckout": false } } ``` Or via the REST API: ```bash curl -X PATCH https://api.swft.co.uk/v1/merchants/{id} \ -H "Authorization: Bearer $SWFT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"modules": {"expressCheckout": false}}' ``` This hides the express button row entirely, even on supported devices. The setting takes effect on the next session creation — existing pre-created sessions use the config that was current at session creation time. ## Styling [Section titled “Styling”](#styling) The Payment Request button renders inside an iframe controlled by Stripe and cannot be styled with CSS. You can control the button theme (`dark`, `light`, `light-outline`) and button type (`default`, `buy`, `donate`, `book`) from the merchant config: ```json { "modules": { "expressCheckout": { "theme": "dark", "type": "buy" } } } ``` The surrounding container, the “or” divider, and the spacing around the button row are all part of Swft’s UI and follow your active template’s token system. # First Checkout ## Walk-through [Section titled “Walk-through”](#walk-through) ### 1. Add a product to cart [Section titled “1. Add a product to cart”](#1-add-a-product-to-cart) Add any WooCommerce product to your cart. Swft only activates when the cart has at least one item. ### 2. Pre-created session [Section titled “2. Pre-created session”](#2-pre-created-session) As soon as you add an item, WooCommerce fires `woocommerce_cart_updated`. The Swft plugin hooks into this, builds the cart payload, and calls `api.swft.co.uk/sessions` to pre-create a session. The session URL is cached in a WordPress transient keyed by the cart hash (`swft_session_{cart_hash}`). This happens in the background — the customer never waits for it. ### 3. Click “Proceed to checkout” [Section titled “3. Click “Proceed to checkout””](#3-click-proceed-to-checkout) The customer navigates to the WooCommerce checkout page. The plugin intercepts via `template_redirect` (priority 1): 1. Checks `swft_enabled` option is `yes` 2. Checks API key is set 3. Checks cart is not empty 4. Looks up the transient — finds the pre-created session URL 5. Calls `wp_safe_redirect()` to `checkout.swft.co.uk/{sessionId}` The redirect is instant because the session already exists. ### 4. Checkout loads [Section titled “4. Checkout loads”](#4-checkout-loads) The Cloudflare Worker at `checkout.swft.co.uk` reads the session from Cloudflare KV and injects it as `window.__SWFT_SESSION__` into the HTML response. The React app boots and reads the session — no loading state in most cases. ### 5. Details step [Section titled “5. Details step”](#5-details-step) The customer sees: * Apple Pay / Google Pay buttons (if available in their browser) * Email address field * Shipping address fields (first name, last name, address line 1 and 2, city, postcode, country) * Shipping method selection (radio cards with prices from WooCommerce) If the customer has ordered before and WooCommerce REST API credentials are set, their address is pre-filled via a customer lookup call. ### 6. Payment step [Section titled “6. Payment step”](#6-payment-step) After clicking **Continue to payment**, the frontend calls `POST api.swft.co.uk/sessions/{id}/pay` with the selected shipping method and customer details. The API creates a Stripe PaymentIntent and returns the `clientSecret`. Stripe Elements renders the card input. The pay button shows the exact total: `Pay £[amount]`. The customer enters card details and clicks **Pay**. ### 7. Confirmation [Section titled “7. Confirmation”](#7-confirmation) Stripe confirms the payment. The checkout shows a confirmation screen with: * Order reference number (WooCommerce order number) * Delivery address * Estimated delivery window (if available) * Interactive map (if Google Maps API key is set) Simultaneously, in the background: * Stripe fires `payment_intent.succeeded` to the Swft webhook * The API creates a WooCommerce order via REST API (`status: processing`) * A confirmation email is sent to the customer * Server-side tracking events fire (Purchase) to Meta CAPI, GA4, and TikTok ### 8. WooCommerce order [Section titled “8. WooCommerce order”](#8-woocommerce-order) Check **WooCommerce → Orders**. A new order appears with: * Status: `Processing` * Payment method: `Card (Swft)` * All line items, shipping line, and billing/shipping address * Custom meta: `_swft_session_id`, `_swft_payment_intent` * Extension data as `_swft_ext_{key}` meta fields ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) **Swft is not intercepting checkout:** Enable debug logging in **Settings → Swft Checkout → Debug Logging**, then visit the checkout page. The debug log shows every decision point. Common causes: * `swft_enabled` is not `yes` — check the Enable checkbox is saved * API key is empty * Cart is empty at time of redirect **Session creation failed:** The plugin falls through to native WooCommerce checkout if no session URL is returned. Check: * API key is valid * `api.swft.co.uk` is reachable from your server * No firewall blocking outbound requests to `api.swft.co.uk` **Orders not appearing in WooCommerce:** The WooCommerce REST API credentials may be missing or incorrect. Verify them in **Settings → Swft Checkout → WooCommerce API Credentials**. The status field shows whether the credentials have been synced. # Headless Embed The Swft headless embed lets you use the Swft checkout UI on any site — React, Vue, plain HTML, Shopify headless, custom Laravel storefronts — without a WooCommerce backend. You provide the cart data; Swft handles the checkout UI, payment processing, and order confirmation. ## CDN script tag [Section titled “CDN script tag”](#cdn-script-tag) Add the Swft embed script to your page ``: ```html ``` The script is \~14kb gzipped and loads asynchronously. It exposes a global `SwftEmbed` object once the DOM is ready. ## `SwftEmbed.init()` [Section titled “SwftEmbed.init()”](#swftembedinit) Call `init()` once with your configuration: ```js SwftEmbed.init({ merchantKey: 'pk_swft_live_xxxxxxxxxxxxxxxx', trigger: '#checkout-btn', mode: 'modal', // 'modal' | 'redirect' | 'inline' locale: 'en-GB', currency: 'GBP', theme: 'minimal', // matches your Swft template onSuccess: (order) => { console.log('Order placed:', order.id) window.location.href = `/thank-you?order=${order.id}` }, onError: (err) => { console.error('Checkout error:', err) }, }) ``` ### Config options [Section titled “Config options”](#config-options) | Option | Type | Default | Description | | ------------- | ----------------- | ---------------- | -------------------------------------------------------- | | `merchantKey` | `string` | required | Your public merchant key from the dashboard | | `trigger` | `string\|Element` | required | CSS selector or DOM element that opens the checkout | | `mode` | `string` | `'modal'` | How the checkout renders (`modal`, `redirect`, `inline`) | | `locale` | `string` | `'en-GB'` | BCP 47 locale for the checkout UI | | `currency` | `string` | merchant default | ISO 4217 currency code | | `theme` | `string` | merchant default | Template name to render | | `cart` | `object` | `null` | Pre-set cart data (see below) | | `onSuccess` | `function` | `null` | Called with the completed order object | | `onError` | `function` | `null` | Called with an error object | | `onClose` | `function` | `null` | Called when the modal is dismissed without completing | ## WooCommerce Store API cart detection [Section titled “WooCommerce Store API cart detection”](#woocommerce-store-api-cart-detection) If your site has the WooCommerce Store API available (standard on WooCommerce 6.9+), Swft detects it automatically and reads the cart from `/wp-json/wc/store/v1/cart` on trigger. No extra configuration needed — `SwftEmbed.init()` without a `cart` option will use the Store API cart. ## `SwftEmbed.setCart()` [Section titled “SwftEmbed.setCart()”](#swftembedsetcart) For non-WordPress sites, pass your cart data before the checkout opens: ```js SwftEmbed.setCart({ lineItems: [ { id: 'prod_abc123', name: 'Organic Cotton T-Shirt', price: 2999, // pence / cents quantity: 2, image: 'https://example.com/img/shirt.jpg', meta: [ { label: 'Size', value: 'M' }, { label: 'Colour', value: 'Forest Green' }, ], }, ], subtotal: 5998, shipping: 299, tax: 1259, total: 7557, currency: 'GBP', }) ``` Call `setCart()` whenever your cart changes (item added, quantity updated, coupon applied). Swft will use the latest cart state the next time the checkout opens. ## Inline mode [Section titled “Inline mode”](#inline-mode) For a fully embedded checkout (no modal), use `mode: 'inline'` and provide a container element: ```html
``` The checkout renders directly into the container. Useful for dedicated checkout pages. ## Example: non-WordPress storefront [Section titled “Example: non-WordPress storefront”](#example-non-wordpress-storefront) A typical integration for a custom PHP or Node storefront: ```html ``` ## Webhook fulfilment for non-WP stores [Section titled “Webhook fulfilment for non-WP stores”](#webhook-fulfilment-for-non-wp-stores) When an order completes on a non-WooCommerce site, Swft fires a `order.completed` webhook to your configured endpoint: ```json { "event": "order.completed", "orderId": "swft_ord_xxxxxxxx", "merchant": "mer_xxxxxxxx", "customer": { "email": "shopper@example.com", "name": "Jane Smith", "address": { ... } }, "lineItems": [ ... ], "total": 7557, "currency": "GBP", "payment": { "method": "card", "stripeChargeId": "ch_xxxxxxxx" } } ``` Configure your webhook URL in the dashboard under **Settings > Webhooks**. # Installation ## 1. Get an API key [Section titled “1. Get an API key”](#1-get-an-api-key) Go to [swft.co.uk](https://swft.co.uk) and register your store. You will receive an API key of the form `swft_live_...`. ## 2. Download the plugin [Section titled “2. Download the plugin”](#2-download-the-plugin) Download `swft-checkout.zip` from [swft.co.uk/plugin](https://swft.co.uk/plugin) or from the email sent after registration. ## 3. Install in WordPress [Section titled “3. Install in WordPress”](#3-install-in-wordpress) 1. Go to **Plugins → Add New → Upload Plugin** 2. Upload `swft-checkout.zip` 3. Click **Install Now**, then **Activate** Alternatively, extract the zip and upload the `swft-checkout` folder to `/wp-content/plugins/` via SFTP, then activate from **Plugins**. ## 4. Enter your API key [Section titled “4. Enter your API key”](#4-enter-your-api-key) 1. Go to **Settings → Swft Checkout** 2. Paste your API key into the **API Key** field 3. Click **Save Changes** The setup wizard will guide you through the remaining steps. ## 5. Connect Stripe [Section titled “5. Connect Stripe”](#5-connect-stripe) See [Connecting Stripe](/getting-started/connecting-stripe) for the OAuth flow. Stripe must be connected before you enable Swft — payments cannot be processed without it. ## 6. Enable Swft Checkout [Section titled “6. Enable Swft Checkout”](#6-enable-swft-checkout) Once your API key is saved and Stripe is connected: 1. Check the **Enable Swft Checkout** checkbox 2. Click **Save Changes** The admin bar will show a green indicator confirming Swft is active. ## Auto-updates [Section titled “Auto-updates”](#auto-updates) Swft Checkout ships with a built-in update system. When a new version is available it appears in **Dashboard → Updates** alongside your other plugins. No separate updater plugin is needed. ## WooCommerce API credentials (optional) [Section titled “WooCommerce API credentials (optional)”](#woocommerce-api-credentials-optional) If you want orders to sync back to WooCommerce as native orders, you need to provide WooCommerce REST API credentials: 1. Go to **WooCommerce → Settings → Advanced → REST API** 2. Click **Add key**, set permissions to **Read/Write**, and generate 3. Copy the **Consumer key** and **Consumer secret** 4. Paste both into **Settings → Swft Checkout → WooCommerce API Credentials** 5. Click **Save Changes** — the credentials are synced to the Swft API immediately Without these credentials, Swft will still process payments, but orders will not appear in WooCommerce Admin. ## Uninstalling [Section titled “Uninstalling”](#uninstalling) Deactivating the plugin: * Clears all `swft_session_*` transients * Clears debug and fallback logs * Cancels the WP HealthKit heartbeat cron Running **Delete** from the Plugins screen removes all options (`swft_enabled`, `swft_api_key`, `swft_debug`, tracking pixel settings, etc.) cleanly via `uninstall.php`. # Medusa.js ```bash npm install @swft-checkout/medusa @swft-checkout/js ``` ## Setup [Section titled “Setup”](#setup) medusa-config.ts ```typescript import { defineConfig } from '@medusajs/medusa' export default defineConfig({ plugins: [{ resolve: '@swft-checkout/medusa', options: { merchantApiKey: process.env.SWFT_MERCHANT_API_KEY, webhookSecret: process.env.SWFT_WEBHOOK_SECRET, }, }], }) ``` ## Create checkout session [Section titled “Create checkout session”](#create-checkout-session) ```typescript import { SwftSessionService } from '@swft-checkout/medusa' const swft = new SwftSessionService({ merchantApiKey: process.env.SWFT_MERCHANT_API_KEY, }) // In your storefront API route: const session = await swft.createFromCart(cart) return { sessionUrl: session.sessionUrl } ``` ## Webhook [Section titled “Webhook”](#webhook) The plugin registers `POST /webhooks/swft` automatically. Set your webhook URL in the Swft dashboard to `https://your-medusa-instance.com/webhooks/swft`. ## Environment variables [Section titled “Environment variables”](#environment-variables) | Variable | Description | | ----------------------- | --------------------------------------------------------- | | `SWFT_MERCHANT_API_KEY` | From [swft.co.uk/dashboard](https://swft.co.uk/dashboard) | | `SWFT_WEBHOOK_SECRET` | From Swft dashboard → Settings → Webhook Secret | # Next.js ```bash npm install @swft-checkout/nextjs @swft-checkout/js ``` ## App Router (Server Action) [Section titled “App Router (Server Action)”](#app-router-server-action) app/checkout/actions.ts ```typescript 'use server' import { swftCheckout } from '@swft-checkout/nextjs' export async function checkout(cartItems: CartItem[]) { await swftCheckout({ merchantApiKey: process.env.SWFT_MERCHANT_API_KEY!, currency: 'GBP', lineItems: cartItems.map(item => ({ name: item.name, quantity: item.quantity, price: item.price, product_id: item.id, })), shippingMethods: [ { id: 'standard', label: 'Standard Shipping (3-5 days)', cost: 499 }, { id: 'express', label: 'Express Shipping (next day)', cost: 999 }, ], subtotal: cartItems.reduce((s, i) => s + i.price * i.quantity, 0), }) // swftCheckout calls redirect() — never returns } ``` app/cart/page.tsx ```tsx import { checkout } from './actions' export default function CartPage({ items }: { items: CartItem[] }) { return (
) } ``` ## Webhook handler (App Router) [Section titled “Webhook handler (App Router)”](#webhook-handler-app-router) app/api/webhooks/swft/route.ts ```typescript import { createSwftWebhookHandler } from '@swft-checkout/nextjs' export const POST = createSwftWebhookHandler( process.env.SWFT_WEBHOOK_SECRET!, async (payload) => { if (payload.event === 'payment_succeeded') { await createOrder({ ...payload }) } } ) ``` # Using Swft Without WordPress Swft is platform-agnostic. The WooCommerce plugin automates everything for WP merchants — but any platform can use the API directly. ## How it works [Section titled “How it works”](#how-it-works) 1. Your backend creates a checkout session via the Swft API 2. You redirect the customer to `checkout.swft.co.uk/{sessionId}` 3. Customer pays — Swft handles 3DS, Apple Pay, Google Pay 4. Swft calls your webhook with `payment_succeeded` 5. You fulfil the order in your platform ## Quick start [Section titled “Quick start”](#quick-start) ```bash npm install @swft-checkout/js ``` ```typescript import { createSession, redirectToCheckout } from '@swft-checkout/js' // 1. Create session (run server-side — never expose your API key client-side) const session = await createSession({ merchantApiKey: process.env.SWFT_MERCHANT_API_KEY, currency: 'GBP', lineItems: [ { name: 'Midnight Runner Pro', quantity: 1, price: 12999 }, ], shippingMethods: [ { id: 'standard', label: 'Standard Shipping', cost: 499 }, { id: 'express', label: 'Express Shipping', cost: 999 }, ], subtotal: 12999, }) // 2. Redirect customer redirectToCheckout(session.sessionUrl) ``` ## Receiving webhooks [Section titled “Receiving webhooks”](#receiving-webhooks) Configure your webhook URL in the [Swft dashboard](https://swft.co.uk/dashboard) → Settings → Webhook URL. ```typescript import { verifySwftWebhook } from '@swft-checkout/js' // Express example app.post('/webhooks/swft', express.raw({ type: 'application/json' }), async (req, res) => { const payload = await verifySwftWebhook( req.body, req.headers['x-swft-signature'], process.env.SWFT_WEBHOOK_SECRET ) if (payload.event === 'payment_succeeded') { // Create order in your system await createOrder({ customer: payload.customer, items: payload.lineItems, address: payload.shippingAddress, amount: payload.amount, currency: payload.currency, referenceId: payload.sessionId, }) } res.json({ received: true }) }) ``` ## Platform guides [Section titled “Platform guides”](#platform-guides) * [Next.js](/getting-started/nextjs) — App Router + Pages Router * [Medusa.js](/getting-started/medusa) — v2 plugin * [Vanilla JS](/getting-started/vanilla) — any platform * [WooCommerce](/getting-started/installation) — WordPress plugin (automatic) ## Passing extension data [Section titled “Passing extension data”](#passing-extension-data) Swft modules (order bumps, gift options, funnels, etc.) are configured via the `extensions` field: ```typescript const session = await createSession({ // ... extensions: { order_bumps: [{ product_id: 456, headline: 'Add matching socks!', price: 699, original_price: 999, discount_label: '30% OFF', }], gift_options: { message: 'Happy Birthday!', wrap: true, wrap_price: 399, }, }, }) ``` ## API reference [Section titled “API reference”](#api-reference) Full API reference: [api.swft.co.uk/docs](https://api.swft.co.uk/docs) # Payment Gateways Swft Checkout supports multiple payment gateways side-by-side. Configure as many as you need; shoppers see only the methods you’ve enabled for their region and cart. Cards globally via Stripe Connect. Swft's 2% fee is deducted in-flow. Native in-page PayPal button. Funds settle to your PayPal Business balance. UK Open Banking. Lower fees, instant settlement, no chargebacks. Cards plus Apple Pay and Google Pay for high-risk verticals like competitions and lotteries. Automatic above the contact form via Stripe's Express Checkout Element. Klarna, Clearpay, Affirm, Zip — toggled in your Stripe Dashboard. iDEAL, Bancontact, BLIK, SOFORT, EPS, MobilePay, Swish, Vipps, Bizum, MB WAY, Alma. ## Fee model [Section titled “Fee model”](#fee-model) Swft charges a flat **2% per successful transaction**, regardless of gateway. How that fee is collected differs: * **In-flow (Stripe)** — Stripe Connect’s `application_fee_amount` deducts the 2% before funds settle to your account. The fee shows up as a transfer deduction on your Stripe dashboard. No separate invoice. * **Out-of-band (everyone else)** — PayPal, Klyme, and NomuPay don’t support marketplace-style fee skims at the gateway layer. Swft records each successful transaction in a platform-fee ledger and bills the accumulated fees once a month to the card you have on file in Stripe Billing. Both models snapshot the fee rate at the time of the transaction, so a future rate change doesn’t retroactively re-bill historical orders. ## Choosing what to enable [Section titled “Choosing what to enable”](#choosing-what-to-enable) Most merchants enable **Stripe** plus one or two regional methods: * **UK consumer storefront** — Stripe + Klyme (Open Banking is fast and cheap for shoppers) + BNPL. * **EU storefront** — Stripe + Local Payments (iDEAL, Bancontact etc. surface automatically by billing country) + BNPL. * **Competition / lottery site on Total Processing** — NomuPay alongside or instead of Stripe. PayPal as a secondary option. * **Global SaaS / digital goods** — Stripe + PayPal + BNPL is usually enough. You can run any number of gateways at once. The checkout’s payment section orders them by best-fit-for-the-shopper: 1. Express wallets first (Apple Pay / Google Pay) 2. Open Banking in the UK 3. Local methods in the EU 4. Card form 5. PayPal / NomuPay (where configured) 6. BNPL Empty / unconfigured gateways are hidden — shoppers never see a broken button. ## Test mode [Section titled “Test mode”](#test-mode) Each gateway has its own concept of “test mode”: | Gateway | Test mode toggle | Test credentials | | ------- | ---------------------------------- | ------------------------------------------ | | Stripe | Live/test on the connected account | Stripe test cards (`4242 4242 4242 4242`) | | PayPal | `paypal_live_mode` in Swft | PayPal sandbox app + sandbox buyer account | | Klyme | `klyme_live_mode` in Swft | Klyme sandbox credentials | | NomuPay | `nomupay_live_mode` in Swft | OPPWA sandbox via | If you mix live and test credentials across gateways, the checkout shows a banner warning the shopper that an order may be a test charge. This prevents real customers from being billed against a sandbox account. ## Refunds [Section titled “Refunds”](#refunds) Refunds happen in WooCommerce, not Swft. Each WC payment gateway plugin handles its own refund flow: Swft is not in the refund path for any gateway You retain full control of money movement. Refund buttons in WooCommerce → Orders call the merchant’s gateway plugin directly. * **Stripe** — refund button in WC → Orders, hits Stripe via the WooCommerce Stripe Gateway plugin. * **PayPal** — same pattern, via the WooCommerce PayPal Payments plugin. * **NomuPay** — same pattern, via the [NomuPay Payment Gateway for WooCommerce](https://wordpress.org/plugins/totalprocessing-card-payments/) plugin. Swft tags NomuPay orders with the meta that plugin’s refund handler expects, so refunds work out of the box. * **Klyme** — Open Banking refunds go through the Klyme dashboard; WooCommerce will mark the order refunded but the actual fund transfer happens in Klyme. ## What’s next [Section titled “What’s next”](#whats-next) The most common starting point. Two-click OAuth, supports cards globally. Already on Stripe? Add PayPal in under five minutes. Connect your existing NomuPay account for cards, Apple Pay, and Google Pay. # Vanilla JavaScript / Any Platform Works in any Node.js backend, Deno, Bun, Cloudflare Workers, or browser (session creation must be server-side). ```bash npm install @swft-checkout/js ``` ## Express [Section titled “Express”](#express) ```typescript import express from 'express' import { createSession, verifySwftWebhookNode } from '@swft-checkout/js' const app = express() // Checkout endpoint — redirects to Swft app.post('/checkout', async (req, res) => { const session = await createSession({ merchantApiKey: process.env.SWFT_MERCHANT_API_KEY, currency: 'GBP', lineItems: req.body.items, shippingMethods: [{ id: 'standard', label: 'Standard', cost: 499 }], subtotal: req.body.subtotal, }) res.redirect(session.sessionUrl) }) // Webhook app.post('/webhooks/swft', express.raw({ type: 'application/json' }), (req, res) => { const payload = verifySwftWebhookNode( req.body, req.headers['x-swft-signature'] as string, process.env.SWFT_WEBHOOK_SECRET ) // handle payload.event === 'payment_succeeded' res.json({ received: true }) }) ``` ## Cloudflare Workers / Deno / Bun [Section titled “Cloudflare Workers / Deno / Bun”](#cloudflare-workers--deno--bun) Use `verifySwftWebhook` (async, Web Crypto API) instead of `verifySwftWebhookNode`. # Agency Portal The Swft agency portal lets digital agencies manage multiple merchant accounts from a single login. Agencies can provision merchants, monitor checkout performance across their client portfolio, apply consistent configurations, and access consolidated billing. ## What the agency portal provides [Section titled “What the agency portal provides”](#what-the-agency-portal-provides) * A single dashboard at `app.swft.co.uk/agency` covering all linked merchants * Per-merchant analytics: conversion rate, revenue, TTFB, top drop-off steps * Bulk config actions: push a template or module setting to multiple merchants at once * Client-facing reports (PDF export or shareable link) * Consolidated invoicing for agencies on the Agency or White-Label plan ## Registering an agency account [Section titled “Registering an agency account”](#registering-an-agency-account) Send a `POST` to `/agencies/register` with your agency details: ```bash curl -X POST https://api.swft.co.uk/v1/agencies/register \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Agency Ltd", "email": "hello@acmeagency.com", "website": "https://acmeagency.com", "plan": "agency" }' ``` **Response:** ```json { "agencyId": "agc_xxxxxxxx", "apiKey": "sk_swft_agency_xxxxxxxxxxxxxxxx", "dashboardUrl": "https://app.swft.co.uk/agency/agc_xxxxxxxx" } ``` Store the `apiKey` securely — it has elevated permissions and can act on behalf of any linked merchant. ## Linking merchants [Section titled “Linking merchants”](#linking-merchants) After registering, link existing merchant accounts to your agency: ```bash curl -X POST https://api.swft.co.uk/v1/agencies/{agencyId}/merchants \ -H "Authorization: Bearer $SWFT_AGENCY_KEY" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "mer_xxxxxxxx" }' ``` The merchant receives an email asking them to approve the link. Once approved, their account appears in your agency dashboard. Merchants retain full access to their own account — linking an agency does not transfer ownership. You can also create a new merchant account directly from the agency (no approval step needed): ```bash curl -X POST https://api.swft.co.uk/v1/agencies/{agencyId}/merchants/create \ -H "Authorization: Bearer $SWFT_AGENCY_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Client Store Name", "email": "owner@clientstore.com", "websiteUrl": "https://clientstore.com", "plan": "grow" }' ``` ## Agency API key scopes [Section titled “Agency API key scopes”](#agency-api-key-scopes) The agency API key (`sk_swft_agency_*`) can perform any action a merchant key can, across all linked merchants. Specify the target merchant in the `X-Swft-Merchant` header: ```bash curl https://api.swft.co.uk/v1/sessions \ -H "Authorization: Bearer $SWFT_AGENCY_KEY" \ -H "X-Swft-Merchant: mer_xxxxxxxx" ``` Without the `X-Swft-Merchant` header, the request targets the agency account itself (e.g. for aggregated analytics). ## Agency dashboard [Section titled “Agency dashboard”](#agency-dashboard) Log in at `app.swft.co.uk/agency` to access: **Portfolio overview** — all merchants in a table: name, plan, 30-day revenue, conversion rate, last checkout activity, and a quick-link to each merchant’s settings. **Performance comparison** — bar chart ranking merchants by conversion rate, revenue, or TTFB. Useful for identifying underperforming clients. **Bulk actions** — select multiple merchants and push a config change (e.g. enable address autocomplete for all clients at once). **Reports** — generate a branded PDF performance report for any merchant covering the selected date range. Reports include funnel metrics, device split, top products, and recovery stats. ## White-label plans [Section titled “White-label plans”](#white-label-plans) On the White-Label plan, the Swft checkout UI removes all Swft branding: * “Powered by Swft” footer link removed * Email templates use your agency’s branding * The checkout domain can be `checkout.youragency.com` (custom domain per merchant, managed by your agency) * Client-facing dashboards at `app.youragency.com` (requires DNS delegation) To enquire about the White-Label plan, email or open a conversation via the agency dashboard. Tip Agency API keys should never be embedded in client-side code or WordPress plugins. Use merchant-scoped keys (`pk_swft_live_*`) for frontend operations. # Ambassadors & Referrals Set up an affiliate / ambassador programme to recruit creators, customers, and partners who promote your store. Swft integrates with Partnero (the partner platform we recommend) and surfaces a referral prompt on the checkout confirmation screen. ## What it does [Section titled “What it does”](#what-it-does) Two pieces: 1. **Ambassador applications** — interested partners visit `yourstore.com/ambassadors/apply` (a small form Swft hosts), submit their details, and get a referral link they can promote. 2. **Confirmation referral prompt** — after a shopper completes a purchase, Swft Checkout shows a “Refer a friend and get £X” prompt with a copy-link button. The shopper becomes a casual referrer without needing to apply formally. Successful referrals are tracked via Partnero, which handles commission calculation and payout. ## Setup [Section titled “Setup”](#setup) ### 1. Create a Partnero programme [Section titled “1. Create a Partnero programme”](#1-create-a-partnero-programme) If you don’t have one already: 1. Sign up at [partnero.com](https://partnero.com). 2. Create a programme. Set commission rate (a percentage of GMV) and tracking parameters. 3. Copy your **API key** and **Program ID** from the Partnero dashboard. ### 2. Configure Swft [Section titled “2. Configure Swft”](#2-configure-swft) Add these env vars to your Swft API deployment (or paste them in your Swft Dashboard’s Settings → Integrations): | Var | What | | --------------------- | -------------------------------- | | `PARTNERO_API_KEY` | Your Partnero API key. | | `PARTNERO_PROGRAM_ID` | The Partnero program identifier. | Once these are set, ambassador applications are auto-approved and instantly issued referral links. ### 3. Configure the confirmation prompt [Section titled “3. Configure the confirmation prompt”](#3-configure-the-confirmation-prompt) Swft Dashboard → **Checkout Editor** → **Modules** → enable **Referral prompt**. Set the reward amount (the message shown to the shopper, e.g. “Refer a friend, you both get £5”). The actual reward fulfilment is handled by Partnero — Swft is just the surface. ## Customer experience [Section titled “Customer experience”](#customer-experience) ### Ambassadors [Section titled “Ambassadors”](#ambassadors) A partner visits `yourstore.com/ambassadors/apply`. They submit: * Name * Email * Optional promotion notes (“I run a TikTok with 100k followers in the running niche…”) If Partnero is configured: they immediately receive their referral link by email. If Partnero is not configured: their application is stored as pending. You can review and approve in the **Ambassadors** page of your dashboard (manual handoff to Partnero). ### Referrers (informal) [Section titled “Referrers (informal)”](#referrers-informal) A shopper completes a purchase. The confirmation screen shows: > Refer a friend and get £5 \[Copy referral link] The link is unique per shopper (encoded in the URL as a referral code). When a friend clicks through and purchases, the system reports the transaction to Partnero with the referrer’s code; Partnero credits the referrer per your programme rules. ## Dashboard [Section titled “Dashboard”](#dashboard) Swft Dashboard → **Ambassadors**: * **Applications** — pending and approved ambassador applications. * **Stats** — clicks, conversions, GMV per ambassador (proxied from Partnero). * **Top performers** — highest-converting ambassadors this month. ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) `POST /api/ambassadors/apply` accepts the application. If `PARTNERO_API_KEY` and `PARTNERO_PROGRAM_ID` are set, the application is forwarded to Partnero’s `POST /v1/partners` endpoint. Partnero returns a `referral_url`; Swft stores the application as `approved` and emails the link. If Partnero is not configured, the application is stored in `ambassador_applications` (status: `pending`) with no auto-issued link. You can later run a manual upload to Partnero. On every order completion, the Swft webhook posts to Partnero’s `POST /v1/transactions` with: the partner key (referral code from the order’s UTM / cookie), customer key, sale amount, currency. Partnero handles commission tracking from there. For the informal confirmation-screen referrer: Swft generates a per-shopper referral code on the fly when the prompt renders, encodes it in the share URL, and reports any resulting orders back to Partnero. The `GET /api/ambassadors/stats?ref=CODE` endpoint proxies stats from Partnero (clicks, conversions, GMV) so you can show partners their own stats from your dashboard. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Partnero is optional but recommended.** Without it, ambassador applications pile up as pending and you have to handle referral tracking manually. * **Commission payouts are Partnero’s job.** Swft doesn’t move money — Partnero does. Configure your Partnero programme’s payout schedule and method directly there. * **Referral attribution is cookie-based.** If a referee clears cookies between clicking the link and purchasing, the referral is lost. Cookies last 30 days by default (configurable in Partnero). * **One referrer per order.** If a shopper clicks links from multiple ambassadors, only the most recent one gets credit. * **The confirmation-screen prompt is opt-in only.** It’s only shown if you enable it in the Checkout Editor. Turn it off if your verticals (B2B, regulated) shouldn’t have casual referral programmes. # Local Payment Methods Swft automatically surfaces local payment methods based on the shopper’s billing country. These are rendered as native UI elements — not Stripe’s generic payment element — giving shoppers a familiar, trusted experience. ## Supported methods by country [Section titled “Supported methods by country”](#supported-methods-by-country) | Country | Method | Notes | | ---------------- | ----------------------- | ----------------------------------------- | | Netherlands (NL) | iDEAL | Bank selector presented inline | | Belgium (BE) | Bancontact | Card-style input, redirects to Bancontact | | Poland (PL) | BLIK | 6-digit code input (see below) | | Germany (DE) | SOFORT / Klarna Pay Now | Redirect-based | | Sweden (SE) | Swish | QR code on desktop, deep link on mobile | | Denmark (DK) | MobilePay | Redirect-based | | Norway (NO) | Vipps | Redirect-based | | Finland (FI) | MobilePay | Redirect-based | | Austria (AT) | EPS | Bank selector, redirect | | Portugal (PT) | MB WAY | Phone number input, push notification | | Spain (ES) | Bizum | Phone number input | | France (FR) | Alma | BNPL instalment option (3x, 4x) | Country detection uses the billing country selected by the shopper, not IP geolocation. This ensures accurate method presentation when billing and shipping addresses differ. ## How methods are shown [Section titled “How methods are shown”](#how-methods-are-shown) When a shopper selects a supported billing country, the relevant local payment methods appear as tab options in the payment section — alongside Card, Apple Pay / Google Pay (where supported), and any configured BNPL options. Swft does not show multiple local methods at once in the same tab row (which creates decision paralysis). Only the primary method for the selected country is promoted. Secondary methods are accessible via a “More payment options” expander. ## iDEAL (Netherlands) [Section titled “iDEAL (Netherlands)”](#ideal-netherlands) iDEAL transactions present an inline bank selector with logos for all major Dutch banks. Selecting a bank redirects to the bank’s own authentication page and returns to the Swft confirmation screen on success. Stripe handles the redirect flow. Swft pre-creates the PaymentIntent with `payment_method_types: ['ideal']` for NL sessions. ## Bancontact (Belgium) [Section titled “Bancontact (Belgium)”](#bancontact-belgium) Bancontact renders a card-number-style input for the 16-digit card number or redirects to the Bancontact app on mobile. The payment is confirmed synchronously on the Swft edge worker before redirecting to the order confirmation page. ## BLIK (Poland) [Section titled “BLIK (Poland)”](#blik-poland) BLIK is the most-used payment method in Poland. The flow is: 1. Shopper opens their banking app and generates a 6-digit BLIK code (valid for 2 minutes). 2. Swft presents a 6-digit code input field. 3. The shopper enters the code and clicks Pay. 4. Swft submits the code to Stripe, which sends a push notification to the banking app. 5. The shopper approves in their banking app. 6. Stripe confirms the payment; Swft redirects to order confirmation. The BLIK input has a 2-minute countdown timer with a “Get new code” prompt that refreshes the PaymentIntent without losing the order. ## Disabling local payments [Section titled “Disabling local payments”](#disabling-local-payments) To disable all local payment methods for a merchant: ```json { "modules": { "localPayments": false } } ``` To disable a specific method only: ```json { "modules": { "localPayments": { "exclude": ["blik", "ideal"] } } } ``` Valid method identifiers for `exclude`: `ideal`, `bancontact`, `blik`, `sofort`, `swish`, `mobilepay`, `vipps`, `eps`, `mbway`, `bizum`, `alma`. ## Currency handling [Section titled “Currency handling”](#currency-handling) Local methods are only shown when the cart currency matches the payment method’s native currency. iDEAL requires EUR; BLIK requires PLN; Swish requires SEK. If your store charges in GBP only, iDEAL and BLIK will not appear even for Dutch or Polish shoppers. To support local currencies, configure multi-currency in the merchant settings and ensure your Stripe account has the relevant settlement currency enabled. ## Stripe requirements [Section titled “Stripe requirements”](#stripe-requirements) All local payment methods are processed via Stripe. Your Stripe account must have each payment method enabled in the [Stripe Dashboard](https://dashboard.stripe.com/settings/payment_methods). Swft will not render a payment method that is not enabled on your connected Stripe account, even if the shopper’s country matches. Caution iDEAL, Bancontact, and SOFORT payments can take a few seconds to confirm. Do not mark orders as processing before the Stripe webhook `payment_intent.succeeded` event is received. # Make (Integromat) Integration Receive Swft order events in Make and route them through any visual automation scenario. ## Setup — Webhooks module [Section titled “Setup — Webhooks module”](#setup--webhooks-module) 1. In Make, create a new scenario 2. Add a **Webhooks → Custom webhook** trigger module 3. Click **Add** to create a new webhook and copy the URL — it looks like: ```plaintext https://hook.eu1.make.com/abc123xyz ``` 4. Register this URL as a Swft subscription by calling the API (you can do this from Make using an HTTP module, or from any API client): ```plaintext POST https://api.swft.co.uk/merchants/zapier/subscribe x-swft-api-key: YOUR_API_KEY Content-Type: application/json { "target_url": "https://hook.eu1.make.com/abc123xyz", "event": "order.completed", "provider": "make" } ``` Available events: `order.completed`, `order.failed`, `cart.abandoned`, `refund.issued` 5. Click **Run once** in Make, then complete a test order in Swft to send a sample payload 6. Make will detect the payload structure and map all fields automatically *** ## Determining the payload structure [Section titled “Determining the payload structure”](#determining-the-payload-structure) If you need Make to learn the structure before a real event fires, use the sample endpoint: ```plaintext GET https://api.swft.co.uk/merchants/zapier/sample/order.completed x-swft-api-key: YOUR_API_KEY ``` This returns an array containing one sample payload with all fields populated. *** ## Unregistering [Section titled “Unregistering”](#unregistering) When you deactivate a scenario, remove the subscription: ```plaintext DELETE https://api.swft.co.uk/merchants/zapier/unsubscribe x-swft-api-key: YOUR_API_KEY Content-Type: application/json { "target_url": "https://hook.eu1.make.com/abc123xyz" } ``` *** ## Sample scenario — Order completed → Google Sheets row [Section titled “Sample scenario — Order completed → Google Sheets row”](#sample-scenario--order-completed--google-sheets-row) ::: v-pre | Step | Module | Config | | ---- | ------------------------- | ---------------------------------------------- | | 1 | Webhooks → Custom webhook | Listens for `order.completed` | | 2 | Tools → Set variable | `order_total` = `{{1.data.total_pence / 100}}` | | 3 | Google Sheets → Add a row | Map fields from step 1 | Useful field mappings: | Google Sheets column | Make expression | | -------------------- | -------------------------------------------------------------- | | Order ID | `{{1.data.order_id}}` | | Customer name | `{{1.data.customer.first_name}} {{1.data.customer.last_name}}` | | Email | `{{1.data.customer.email}}` | | Total | `{{1.data.total_pence / 100}}` | | Currency | `{{1.data.currency}}` | | Payment method | `{{1.data.payment_method}}` | | Country | `{{1.data.shipping_address.country}}` | | Created | `{{1.created_at}}` | | ::: | | *** ## HTTP module (alternative — polling or one-off) [Section titled “HTTP module (alternative — polling or one-off)”](#http-module-alternative--polling-or-one-off) For workflows that need to pull data on a schedule rather than receive events: | Field | Value | | --------------- | ----------------------------------------------------- | | URL | `https://api.swft.co.uk/merchants/analytics?range=7d` | | Method | GET | | Headers → Name | `x-swft-api-key` | | Headers → Value | Your merchant API key | | Parse response | Yes | *** ## Payload reference [Section titled “Payload reference”](#payload-reference) See the [Zapier integration page](/integrations/zapier) for the full payload field reference — the payload shape is identical across all integrations. # MCP Server (AI Agents) The `@swft-checkout/mcp` package exposes a [Model Context Protocol](https://modelcontextprotocol.io) server that lets AI agents — Claude, GPT-4o, Gemini, or any MCP-compatible runtime — interact with Swft directly. Agents can browse products, inspect analytics, check cart recovery stats, and place orders on behalf of customers. ## Installation [Section titled “Installation”](#installation) ```bash npm install -g @swft-checkout/mcp ``` Or run without installing: ```bash npx @swft-checkout/mcp ``` ## Configuration [Section titled “Configuration”](#configuration) The MCP server reads your API key from the environment: ```bash export SWFT_API_KEY=sk_swft_live_xxxxxxxxxxxxxxxx ``` ## Claude Desktop config [Section titled “Claude Desktop config”](#claude-desktop-config) Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): ```json { "mcpServers": { "swft": { "command": "npx", "args": ["-y", "@swft-checkout/mcp"], "env": { "SWFT_API_KEY": "sk_swft_live_xxxxxxxxxxxxxxxx" } } } } ``` Restart Claude Desktop after saving. You should see “swft” listed in the MCP tools panel. ## Available tools [Section titled “Available tools”](#available-tools) ### `swft_analytics` [Section titled “swft\_analytics”](#swft_analytics) Returns checkout funnel metrics for the merchant over a given period. **Parameters:** | Name | Type | Description | | ----------- | -------- | --------------------------------------------------------------- | | `period` | `string` | `7d`, `30d`, `90d`, or ISO date range (`2026-01-01/2026-03-31`) | | `breakdown` | `string` | Optional. `template`, `device`, `country`, `source` | **Example output:** ```json { "sessions": 4821, "completions": 2341, "conversionRate": 0.486, "revenue": 142850.00, "avgOrderValue": 61.02, "topDropOffStep": "payment" } ``` *** ### `swft_sessions` [Section titled “swft\_sessions”](#swft_sessions) Lists recent checkout sessions with status, cart value, and completion state. Useful for diagnosing drop-off. **Parameters:** | Name | Type | Description | | -------- | -------- | --------------------------------------------- | | `status` | `string` | `all`, `completed`, `abandoned`, `recovering` | | `limit` | `number` | Max results (default 20, max 100) | | `cursor` | `string` | Pagination cursor from previous response | *** ### `swft_merchant_info` [Section titled “swft\_merchant\_info”](#swft_merchant_info) Returns the full merchant config: enabled modules, active template, connected Stripe account, and plan details. **Parameters:** none. *** ### `swft_recovery_stats` [Section titled “swft\_recovery\_stats”](#swft_recovery_stats) Returns cart recovery email performance: send rate, open rate, click rate, and recovered revenue. **Parameters:** | Name | Type | Description | | -------- | -------- | ------------------ | | `period` | `string` | `7d`, `30d`, `90d` | *** ### `swft_get_products` [Section titled “swft\_get\_products”](#swft_get_products) Searches the merchant’s WooCommerce product catalogue. Returns product name, SKU, price, stock status, and image URL. **Parameters:** | Name | Type | Description | | ---------- | --------- | ------------------------- | | `query` | `string` | Search term | | `limit` | `number` | Max results (default 10) | | `inStock` | `boolean` | Filter to in-stock only | | `category` | `string` | WooCommerce category slug | *** ### `swft_create_checkout` [Section titled “swft\_create\_checkout”](#swft_create_checkout) Creates a Swft checkout session and returns a URL the customer can be directed to. This is the tool an AI agent uses to place an order on behalf of a customer. **Parameters:** | Name | Type | Description | | ----------- | --------- | ------------------------------------------ | | `lineItems` | `array` | Product ID / variation ID + quantity pairs | | `customer` | `object` | Pre-fill: email, name, billing address | | `coupon` | `string` | Optional coupon code to apply | | `b2b` | `boolean` | Enable B2B mode for this session | | `note` | `string` | Order note added by the agent | **Example:** ```json { "lineItems": [ { "productId": 1842, "quantity": 1 } ], "customer": { "email": "jane@example.com", "firstName": "Jane", "lastName": "Smith" }, "note": "Placed via AI agent on behalf of customer" } ``` **Returns:** ```json { "sessionId": "sess_xxxxxxxx", "checkoutUrl": "https://checkout.swft.co.uk/s/sess_xxxxxxxx", "expiresAt": "2026-04-26T14:30:00Z" } ``` ## Example: agent placing an order [Section titled “Example: agent placing an order”](#example-agent-placing-an-order) Here is an example conversation where an AI agent uses the MCP tools to complete a purchase: > **User:** Can you order me two tins of the Columbian Dark Roast and apply the WELCOME10 discount? > **Agent (internal):** Calls `swft_get_products` with `query: "Columbian Dark Roast"` → gets product ID 2091, price £8.50, in stock. > > Calls `swft_create_checkout` with `lineItems: [{productId: 2091, quantity: 2}]`, `coupon: "WELCOME10"`, `customer: {email: "user@example.com"}`. > > Returns checkout URL. > **Agent:** I’ve set up your order for 2 × Columbian Dark Roast (£15.30 after 10% discount). Complete payment here: \[checkout link]. The link expires in 60 minutes. Tip The agent never handles payment card data. The checkout URL directs the customer to complete payment themselves via the standard Swft UI. # n8n Integration Receive Swft order events in n8n using the Webhook node, then chain any action — Airtable, Notion, Slack, email, or your own API. ## Option 1 — Webhook trigger node (recommended) [Section titled “Option 1 — Webhook trigger node (recommended)”](#option-1--webhook-trigger-node-recommended) Use this when you want n8n to receive events in real time as they happen. ### Steps [Section titled “Steps”](#steps) 1. Add a **Webhook** node to your workflow 2. Set **HTTP Method** to `POST` 3. Copy the webhook URL n8n gives you — it looks like: ```plaintext https://your-n8n-instance.com/webhook/abc123 ``` 4. In the Swft dashboard **Settings → Integrations**, paste this URL into the inbound webhook field and copy it 5. Register the subscription by calling the Swft API directly (or via an n8n HTTP Request node on workflow activation): ```plaintext POST https://api.swft.co.uk/merchants/zapier/subscribe x-swft-api-key: YOUR_API_KEY Content-Type: application/json { "target_url": "https://your-n8n-instance.com/webhook/abc123", "event": "order.completed", "provider": "n8n" } ``` Available events: `order.completed`, `order.failed`, `cart.abandoned`, `refund.issued` 6. Activate your workflow — Swft will now POST to n8n every time the event fires ### Unregistering [Section titled “Unregistering”](#unregistering) When you deactivate your workflow, call: ```plaintext DELETE https://api.swft.co.uk/merchants/zapier/unsubscribe x-swft-api-key: YOUR_API_KEY Content-Type: application/json { "target_url": "https://your-n8n-instance.com/webhook/abc123" } ``` *** ## Option 2 — HTTP Request node (polling / one-off) [Section titled “Option 2 — HTTP Request node (polling / one-off)”](#option-2--http-request-node-polling--one-off) Use an **HTTP Request** node to fetch data from Swft on demand (e.g. in a scheduled workflow). | Field | Value | | -------------- | -------------------------------------------- | | Method | GET / POST | | URL | `https://api.swft.co.uk/merchants/analytics` | | Authentication | Header Auth | | Header name | `x-swft-api-key` | | Header value | Your merchant API key | *** ## Sample workflow — Order completed → Airtable row [Section titled “Sample workflow — Order completed → Airtable row”](#sample-workflow--order-completed--airtable-row) Import this JSON into n8n via **Workflows → Import from clipboard**: ```json { "name": "Swft — Order to Airtable", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "swft-order", "responseMode": "onReceived" }, "name": "Swft Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 1, "position": [250, 300] }, { "parameters": { "operation": "create", "application": "YOUR_AIRTABLE_BASE_ID", "table": "Orders", "additionalFields": {}, "fields": { "Order ID": "={{ $json.data.order_id }}", "Customer": "={{ $json.data.customer.first_name }} {{ $json.data.customer.last_name }}", "Email": "={{ $json.data.customer.email }}", "Total (£)": "={{ ($json.data.total_pence / 100).toFixed(2) }}", "Currency": "={{ $json.data.currency }}", "Event": "={{ $json.event }}", "Created At": "={{ $json.created_at }}" } }, "name": "Create Airtable Row", "type": "n8n-nodes-base.airtable", "typeVersion": 1, "position": [500, 300] } ], "connections": { "Swft Webhook": { "main": [[{ "node": "Create Airtable Row", "type": "main", "index": 0 }]] } } } ``` Replace `YOUR_AIRTABLE_BASE_ID` and configure Airtable credentials in n8n. The expression `$json.data.total_pence / 100` converts pence to pounds. *** ## Payload reference [Section titled “Payload reference”](#payload-reference) See the [Zapier integration page](/integrations/zapier) for the full payload field reference — the payload shape is identical across all integrations. # Webhooks Reference Swft sends signed webhooks to a URL you specify whenever notable events happen in a checkout — orders complete, payments fail, refunds issue. This page documents the event names, payload schema, and signature verification. ## Quick start [Section titled “Quick start”](#quick-start) 1. In your Swft Dashboard, go to **Settings** → **Webhooks** and paste your endpoint URL (e.g. `https://yourapp.com/swft-webhook`). 2. Click **Generate secret**. Copy the secret — you’ll use it to verify incoming requests. 3. Optionally subscribe to specific events via the **Subscriptions** section; if you leave this empty, Swft delivers every event to your URL. Swft retries delivery on transient failures with exponential backoff. Your endpoint should be **idempotent** — use the `data.session_id` as your deduplication key. ## Signature verification [Section titled “Signature verification”](#signature-verification) Every webhook is signed with an HMAC-SHA256 of the raw body using your webhook secret. The signature is in the `X-Swft-Signature` header, prefixed with `sha256=`: ```plaintext X-Swft-Signature: sha256=4f5e6d... ``` Verify in your handler: ```js import crypto from 'crypto' function verify(rawBody, signatureHeader, secret) { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex') return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected)) } ``` Reject any request where the signature doesn’t match. Use a timing-safe comparison (`timingSafeEqual` in Node, `hmac.compare_digest` in Python). ## Payload structure [Section titled “Payload structure”](#payload-structure) Every payload follows the same envelope: ```json { "event": "order.completed", "created_at": "2026-05-13T22:30:00.000Z", "swft_version": "1.0", "merchant_id": "11111111-2222-3333-4444-555555555555", "data": { "order_id": "wc-1234", "session_id": "cs_abc123...", "customer": { "email": "shopper@example.com", "first_name": "Jamie", "last_name": "Doe", "phone": "+44 7700 900123" }, "billing_address": { "line1": "...", "city": "...", "postcode": "...", "country": "GB" }, "shipping_address": { "line1": "...", "city": "...", "postcode": "...", "country": "GB" }, "items": [ { "product_id": "42", "name": "Tote Bag", "quantity": 1, "price_pence": 1500, "total_pence": 1500 } ], "subtotal_pence": 1500, "tax_pence": 300, "shipping_pence": 500, "discount_pence": 0, "total_pence": 2300, "currency": "GBP", "payment_method": "card", "coupon_code": "WELCOME10", "b2b": { "company_name": "Acme Ltd", "vat_number": "GB123456789", "po_number": "PO-2024-042" } } } ``` Fields marked optional (`phone`, `coupon_code`, `b2b`) are omitted when not present. ## Events [Section titled “Events”](#events) ### `order.completed` [Section titled “order.completed”](#ordercompleted) The shopper has paid and the WooCommerce order has been created. **When it fires:** On `payment_intent.succeeded` (Stripe), `paypal_capture` (PayPal), `klyme_complete` (Klyme), or `nomupay-result` returning success (NomuPay). **Payload:** Full envelope above. This is the event you almost certainly want — fulfilment, accounting sync, CRM updates, email-marketing identification all hook here. ### `order.failed` [Section titled “order.failed”](#orderfailed) A payment attempt failed for a final reason (declined card, fraud block, 3DS challenge rejected). **When it fires:** On `payment_intent.payment_failed` (Stripe) or equivalent failure callbacks from other gateways. **Payload:** Same envelope, with `data.total_pence` reflecting the attempted amount. The `payment_method` field is the method that failed. Useful for routing to your support team or triggering a recovery email from outside the standard Dunning flow. ### `refund.issued` [Section titled “refund.issued”](#refundissued) A merchant has refunded all or part of an order. **When it fires:** On `charge.refunded` from Stripe. (For PayPal/Klyme/NomuPay, refunds happen in the merchant’s WooCommerce admin — the WC plugin handles them; Swft is not in the refund path.) **Payload:** Same envelope, with `data.total_pence` set to the **refunded amount** (not the original order amount). `data.order_id` ties back to the original order. ### `cart.abandoned` [Section titled “cart.abandoned”](#cartabandoned) A checkout session expired with items in the cart but no payment completed. **Status:** Defined in the event enum, **not yet implemented**. If you build for it, expect it to ship in a future release. ## Subscribing to specific events [Section titled “Subscribing to specific events”](#subscribing-to-specific-events) By default, Swft sends every event to your `webhook_url`. If you only want a subset: 1. In **Settings → Webhooks**, add **Subscriptions**. 2. For each subscription, choose an event name and a delivery URL. Subscriptions are stored in `merchant_webhook_subscriptions`. One merchant can have many subscriptions; they’re fanned out in parallel. ## Delivery & retries [Section titled “Delivery & retries”](#delivery--retries) * Each delivery times out after **5 seconds**. Make sure your endpoint responds quickly — defer heavy work to a job queue. * A `2xx` response is success. Anything else (including `3xx` redirects) is treated as failure. * Failed deliveries are retried with exponential backoff: 30s, 2min, 10min, 30min, 2h, 6h (max 6 retries). * After 6 failed retries, the delivery is marked **abandoned** and won’t be retried again. You’ll see it in the **Webhooks** → **Failed deliveries** table in your dashboard. ## SSRF protection [Section titled “SSRF protection”](#ssrf-protection) Your `webhook_url` is checked at save time and at delivery time against a deny-list of internal / loopback / link-local addresses. URLs pointing at `localhost`, `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, or `fc00::/7` are rejected. If you need to deliver to an internal address for testing, use a service like ngrok or webhook.site. ## Testing [Section titled “Testing”](#testing) The dashboard’s **Webhooks** page has a **Send test event** button that fires a synthetic `order.completed` to your URL. Use this to confirm your signature verification works before relying on live traffic. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Don’t trust `data.merchant_id`.** Always verify the HMAC signature first; the body is only authentic after that. * **Idempotency is your responsibility.** Swft retries on failure; if your handler completes work but fails to return `2xx` (network blip, gateway timeout), you’ll get the same event again. Dedupe on `data.session_id`. * **One delivery URL doesn’t have to mean one event type.** Send everything to one endpoint and dispatch internally on `event`, or split into multiple subscriptions per event — your call. * **Versioning.** The `swft_version` field is currently `"1.0"`. We’ll add new fields without bumping the version; breaking changes will bump to `"2.0"` and we’ll dual-write during a transition window. # Zapier Integration Connect Swft to 7,000+ apps automatically when orders complete, payments fail, or carts are abandoned. ## Events available [Section titled “Events available”](#events-available) | Event | When it fires | | ----------------- | --------------------------------------------- | | `order.completed` | A payment succeeds and the order is confirmed | | `order.failed` | A payment attempt fails | | `cart.abandoned` | A checkout session expires without payment | | `refund.issued` | A refund is processed via Stripe | ## Setup [Section titled “Setup”](#setup) 1. Go to [zapier.com/apps/swft](https://zapier.com/apps/swft) 2. Click **Connect Swft** 3. Enter your merchant API key from the dashboard Settings page 4. Choose your trigger event 5. Map the fields to your action (Mailchimp, Slack, Airtable, HubSpot, etc.) ## Payload fields [Section titled “Payload fields”](#payload-fields) Every event delivers a payload in this shape: | Field | Type | Description | | -------------------------------- | ------------------- | --------------------------------------------------------------------------- | | `event` | string | One of `order.completed`, `order.failed`, `cart.abandoned`, `refund.issued` | | `created_at` | string | ISO 8601 timestamp | | `swft_version` | string | Always `1.0` | | `merchant_id` | string | Your merchant UUID | | `data.order_id` | string | WooCommerce order number (empty for `order.failed`) | | `data.session_id` | string | Swft checkout session ID | | `data.customer.email` | string | Customer email address | | `data.customer.first_name` | string | | | `data.customer.last_name` | string | | | `data.customer.phone` | string \| undefined | | | `data.billing_address.line1` | string | | | `data.billing_address.city` | string | | | `data.billing_address.postcode` | string | | | `data.billing_address.country` | string | ISO 3166-1 alpha-2 | | `data.shipping_address.line1` | string | | | `data.shipping_address.city` | string | | | `data.shipping_address.postcode` | string | | | `data.shipping_address.country` | string | | | `data.items` | array | Line items (see below) | | `data.subtotal_pence` | number | Cart subtotal in minor units | | `data.tax_pence` | number | Tax in minor units | | `data.shipping_pence` | number | Shipping cost in minor units | | `data.discount_pence` | number | Discount applied in minor units | | `data.total_pence` | number | Order total in minor units | | `data.currency` | string | ISO 4217 — e.g. `GBP` | | `data.payment_method` | string | `card`, `klarna`, `clearpay`, `ideal`, etc. | | `data.coupon_code` | string \| undefined | First applied coupon code | | `data.b2b` | object \| undefined | Present for B2B orders | ### Line item fields [Section titled “Line item fields”](#line-item-fields) | Field | Type | | ------------- | ------ | | `product_id` | string | | `name` | string | | `quantity` | number | | `price_pence` | number | | `total_pence` | number | ### B2B fields (when present) [Section titled “B2B fields (when present)”](#b2b-fields-when-present) | Field | Type | | --------------- | ------------------- | | `company_name` | string | | `vat_number` | string \| undefined | | `po_number` | string \| undefined | | `payment_terms` | string \| undefined | ## REST Hooks API [Section titled “REST Hooks API”](#rest-hooks-api) Zapier uses REST Hooks — it registers a callback URL when a Zap is activated, and unregisters it when turned off. The Swft API endpoints are: ```plaintext POST /merchants/zapier/subscribe — register a subscription DELETE /merchants/zapier/unsubscribe — remove a subscription GET /merchants/zapier/sample/:event — fetch a sample payload for testing ``` All endpoints authenticate via `x-swft-api-key` header or `api_key` query param. ## Webhook signature verification [Section titled “Webhook signature verification”](#webhook-signature-verification) Every delivery includes an `X-Swft-Signature` header: ```plaintext X-Swft-Signature: sha256= ``` For Zapier-managed subscriptions the HMAC is unsigned (no shared secret per-subscription). For your primary webhook URL configured in Settings, the HMAC uses your **Webhook Secret** shown in the Settings page. # SwftBumps Order bumps for Swft Checkout. Offer additional products at a discounted price on the payment step. Accepted bumps are added to the Stripe PaymentIntent amount and included in the WooCommerce order. ## What it does [Section titled “What it does”](#what-it-does) * Define bump offers: a product, a headline, a description, and an optional discount * Bumps appear on the payment step of the Swft Checkout, below the review strip * Customer accepts or declines with a single click * Accepted bumps update the Stripe PaymentIntent amount via `POST /sessions/:id/apply-bumps` * Accepted bumps are added to the WooCommerce order as line items ## Installation [Section titled “Installation”](#installation) 1. Download `swft-bumps.zip` from [swft.co.uk/modules](https://swft.co.uk/modules) 2. Install and activate 3. Go to **WooCommerce → Bumps → Add New** to create bump offers ## Bump configuration [Section titled “Bump configuration”](#bump-configuration) Each bump is a custom post type (`swft_bump`): | Field | Meta key | Description | | ---------------- | ---------------------------- | -------------------------------------------------------- | | Product | `_bump_product_id` | WooCommerce product ID to add to order | | Headline | `_bump_headline` | Short headline shown prominently | | Description | `_bump_description` | Supporting text (optional) | | Discount label | `_bump_discount_label` | e.g. “Save 30% today only” | | Trigger | `_bump_trigger_product_ids` | Show bump only when these product IDs are in cart | | Trigger category | `_bump_trigger_category_ids` | Show bump when cart contains product in these categories | ## Extension data [Section titled “Extension data”](#extension-data) SwftBumps adds the following to `extensions.order_bumps` in the session: ```json [ { "product_id": 789, "name": "Premium Packaging", "headline": "Add premium packaging for just £3", "description": "Black gift box with ribbon. Limited to one per order.", "price": 300, "original_price": 500, "discount_label": "40% off today", "image": "https://yourstore.com/wp-content/uploads/box.jpg", "sku": "PREM-BOX-01" } ] ``` The checkout frontend reads this and renders the bump UI on the payment step. ## How payment works [Section titled “How payment works”](#how-payment-works) When the customer accepts a bump: 1. The checkout calls `POST api.swft.co.uk/sessions/{id}/apply-bumps` with `accepted_bump_ids: [789]` 2. The API calculates the additional amount and calls `stripe.paymentIntents.update()` with the new total 3. The accepted bumps are stored in `session_data.extensions.accepted_bumps` 4. The WooCommerce order sync adds accepted bump products as line items ## Order meta [Section titled “Order meta”](#order-meta) | Meta key | Value | | -------------------------- | ------------------------ | | `_swft_ext_order_bumps` | Full bumps array as JSON | | `_swft_ext_accepted_bumps` | Accepted bumps as JSON | ## JS events [Section titled “JS events”](#js-events) ```js document.addEventListener('swftcheckout:ready', () => { // Bumps are shown automatically if extensions.order_bumps is non-empty. // Listen for this event if you need to react when bumps render. }) document.addEventListener('swftcheckout:upsell-shown', (e) => { // Fires when the bump UI renders console.log(e.detail.products) }) ``` ## PHP hooks [Section titled “PHP hooks”](#php-hooks) ```php // Modify which bumps are shown for a given cart add_filter( 'swft_bumps_for_cart', function( array $bumps, WC_Cart $cart ): array { // Remove a bump if a condition is met return array_filter( $bumps, fn( $b ) => $b['product_id'] !== 999 ); }, 10, 2 ); // Add custom data to each bump object in the extensions array add_filter( 'swft_bump_extension_data', function( array $bump, int $product_id ): array { $bump['stock_remaining'] = get_post_meta( $product_id, '_stock', true ); return $bump; }, 10, 2 ); // Fired after bumps are applied to the order add_action( 'swft_bumps_applied', function( WC_Order $order, array $accepted_bumps ): void { // notify fulfilment system }, 10, 2 ); ``` # Swft Bundles Create fixed product bundles (“buy A + B + C for £49”) and quantity bundles (“buy 3 from this category, get 25% off”). Bundle savings appear automatically in the Swft Cart drawer and the Swft Checkout order summary — no coupon codes required. Swft Bundles is free. ## What it does [Section titled “What it does”](#what-it-does) Two bundle types: * **Fixed bundles** — pick specific products that combine into a fixed bundle price. Example: a “Starter Kit” with three specific SKUs sold together for £49 instead of £63. * **Quantity bundles** — discount when the shopper hits a quantity threshold within a category. Example: “Buy any 3 t-shirts, get 25% off the total.” Bundles surface in two places: * In the Swft Cart drawer as an emerald-green savings badge. * In the Swft Checkout order summary as a single discount line. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * WordPress 6.0+ * WooCommerce 8.0+ * PHP 7.4+ * Swft Checkout active ## Installation [Section titled “Installation”](#installation) 1. Install the **Swft Bundles** plugin from your Swft dashboard’s downloads section. 2. Activate it. 3. Go to **Swft → Bundles** in the WP admin sidebar. ## Creating a fixed bundle [Section titled “Creating a fixed bundle”](#creating-a-fixed-bundle) 1. Click **Add fixed bundle**. 2. Pick the products that must all be in the cart for the bundle to apply. 3. Set the bundle price (what the shopper pays) and the original price (for the savings display). 4. Optionally add a name, description, and badge text (“Best value”, “Save £14”, etc). 5. Save. The bundle activates automatically the next time a shopper has all required products in their cart. ## Creating a quantity bundle [Section titled “Creating a quantity bundle”](#creating-a-quantity-bundle) 1. Click **Add quantity bundle**. 2. Pick a category slug (e.g. `t-shirts`). 3. Set the minimum quantity threshold. 4. Set the discount — either a percentage or a fixed amount off the total. 5. Save. ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) On every cart view, the plugin scans your configured bundles against the cart contents. Fixed bundles require every listed product to be present; quantity bundles sum items in the configured category against the threshold. Matching bundles are exposed to Swft Checkout via the `swft_session_extensions` filter and to the Swft Cart drawer via the `swftcart_cart_data` filter. The frontend savings badge is rendered by `assets/bundle-widget.js`. Bundle configuration is stored as JSON in the `swft_bundles_config` WordPress option. ## Gotchas [Section titled “Gotchas”](#gotchas) * Fixed bundles require every product to be in the cart. Partial matches don’t qualify. * Quantity bundles use product **category slugs**. Make sure each product is in the right category, or the bundle won’t trigger. * Bundles are **not visible** in the default WooCommerce cart page — only in Swft Cart and Swft Checkout. If you still use WooCommerce’s stock cart anywhere, shoppers there will see the standard prices. # Swft Digital Detects downloadable and virtual products in the cart, skips the shipping step for all-digital orders, and shows download links on the Swft Checkout confirmation screen the moment payment completes. Swft Digital is free. There is no configuration — it works the second you activate it. ## What it does [Section titled “What it does”](#what-it-does) For carts containing only **virtual** products (no shipping required), Swft Digital removes the shipping step entirely. The shopper goes details → payment → confirmation with one fewer screen. For carts containing **downloadable** products, the confirmation screen surfaces download links immediately. Links also appear in WooCommerce’s order confirmation email per the normal WC behaviour. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * WordPress 6.0+ * WooCommerce 8.0+ * PHP 7.4+ * Swft Checkout active * Products marked as **Downloadable** or **Virtual** in the WooCommerce product editor ## Installation [Section titled “Installation”](#installation) 1. Install the **Swft Digital** plugin from your Swft dashboard’s downloads section. 2. Activate it. 3. That’s it. Mark your products as **Downloadable** and/or **Virtual** in the product editor. There’s a Swft → Swft Digital admin page that shows how many downloadable products the plugin has detected, but no configuration is needed. ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) On every checkout session creation, the plugin hooks `swft_session_extensions` to scan the cart. If every product is virtual, `extensions.digital.all_virtual` is set to `true` — Swft Checkout uses this to skip the shipping step. If any product is downloadable, `extensions.digital.has_downloads` is set, and the checkout knows to expect download URLs on the confirmation screen. When payment completes and the WooCommerce order is created, the plugin hooks `woocommerce_payment_complete`, asks WooCommerce for the download URLs for each item in the order, and stores them in the order’s `_swft_digital_downloads` meta. The Swft API reads that meta when serving the confirmation screen. ## Behaviour [Section titled “Behaviour”](#behaviour) | Cart contains | Shipping step | Download links | | ----------------------------------------------------------------- | ---------------------------------------------- | --------------------- | | Only virtual products | Skipped | N/A | | Only downloadable products (some are virtual, some need shipping) | Shown — shopper still needs to give an address | Shown on confirmation | | Mix of physical + virtual | Shown | Shown on confirmation | | Only physical products | Shown | None | ## Gotchas [Section titled “Gotchas”](#gotchas) * Products must be marked **Downloadable** in the WooCommerce product editor. The plugin doesn’t auto-detect digital products by SKU or category. * Download link expiry and access limits are controlled by **WooCommerce’s** product download settings — Swft Digital just surfaces the links. * Virtual products that aren’t downloadable (e.g. service bookings) still skip shipping but won’t show download links — they don’t have any. # Swft Donations Add a donation widget to Swft Checkout with optional HMRC-compliant Gift Aid declarations. Donations are collected at checkout, stored in a dedicated database table, and exportable to Charities Online for HMRC submission. Swft Donations is free. ## What it does [Section titled “What it does”](#what-it-does) A donation widget appears on the Swft Checkout payment step. Shoppers can: * Pick a preset donation amount (you configure the presets — e.g. £1 / £5 / £10). * Enter a custom amount. * Opt in to Gift Aid by ticking a box and confirming their name and postcode (UK only). Donations are added to the order total and processed by the same payment method as the rest of the cart. Gift Aid declarations are logged separately for HMRC reporting. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * WordPress 6.0+ * WooCommerce 8.0+ * PHP 7.4+ * Swft Checkout active * For Gift Aid: a registered UK charity with an HMRC charity reference number ## Installation [Section titled “Installation”](#installation) 1. Install the **Swft Donations** plugin from your Swft dashboard’s downloads section. 2. Activate it. 3. Go to **WooCommerce → Swft Donations**. ## Settings [Section titled “Settings”](#settings) | Setting | What it does | | ------------------------------ | ------------------------------------------------------------------------- | | **Enable donations** | Master toggle. | | **Charity name** | Displayed in the widget header (e.g. “Support Macmillan Cancer Support”). | | **Preset amounts** | Comma-separated list of suggested amounts (e.g. `1,5,10,25`). | | **Enable Gift Aid** | Shows the Gift Aid opt-in checkbox + name/postcode fields. | | **HMRC reference** | Your charity’s HMRC reference number. Required for Gift Aid claims. | | **Widget title / description** | Custom copy shown above the donation amount picker. | ## Customer experience [Section titled “Customer experience”](#customer-experience) The widget shows: 1. The charity name and description. 2. Preset donation buttons + a “Custom amount” input. 3. A Gift Aid checkbox (if enabled). 4. If Gift Aid is ticked, name and postcode inputs appear inline. When the shopper completes payment, the donation amount is added as an order line and the Gift Aid declaration (if applicable) is logged. ## Managing declarations [Section titled “Managing declarations”](#managing-declarations) In **WooCommerce → Swft Donations → Declarations** you can: * View every Gift Aid declaration: donor name, postcode, donation amount, order ID, timestamp. * Export to CSV for upload to [Charities Online](https://www.gov.uk/guidance/claim-tax-back-on-donations-using-charities-online). * Mark periods as **Claimed** once you’ve submitted them to HMRC — this acts as a manual audit trail and prevents accidental re-submission. ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) On activation, the plugin creates a `swft_donations` table with columns: `declaration_id`, `order_id`, `donor_name`, `donor_postcode`, `amount`, `claimed`, `created_at`. The widget is injected into Swft Checkout via the `swft_session_extensions` filter. When the shopper submits, the declaration is written to the table; the donation amount is included as a line item on the resulting WooCommerce order with `_swft_donation_amount` meta. The admin page reads the table to render the declarations list and CSV export. ## Gotchas [Section titled “Gotchas”](#gotchas) * Gift Aid is **UK only**. Don’t enable it if you’re a non-UK charity — the declaration format is HMRC-specific. * HMRC has strict eligibility rules. Your donor must be a UK taxpayer who’s paid enough Income Tax / Capital Gains Tax in the year to cover the Gift Aid. Make sure your widget copy reflects this. * Submissions to Charities Online are **manual** — Swft Donations doesn’t push to HMRC for you. You export CSV, you upload. * The donation appears as an order line, so it counts toward your WooCommerce gross revenue. Reconcile separately in your accounting. # SwftDrops Timed product drops for WooCommerce. Schedule a product release with a countdown, control access via waitlist or exclusive link, and route buyers straight to Swft Checkout when the drop goes live. ## What it does [Section titled “What it does”](#what-it-does) * Schedule a **drop date and time** per product * Show a **countdown timer** on the product page before the drop * Optional **waitlist** — customers register their email, receive a notification when the drop goes live * Optional **exclusive link** — only customers with the link can add to cart * Integrates with Swft Checkout: the checkout redirect happens immediately so buyers land on a pre-loaded checkout page, not a loading spinner ## Installation [Section titled “Installation”](#installation) 1. Download `swft-drops.zip` from [swft.co.uk/modules](https://swft.co.uk/modules) 2. Install and activate 3. Go to **WooCommerce → Settings → SwftDrops** to configure global settings ## Configuration [Section titled “Configuration”](#configuration) ### Global settings [Section titled “Global settings”](#global-settings) | Setting | Option | Default | | ---------------------- | ----------------------------------- | --------------------------- | | Countdown style | `swft_drops_countdown_style` | `clock` (`clock` or `text`) | | Waitlist enabled | `swft_drops_waitlist_enabled` | `yes` | | Waitlist email subject | `swft_drops_waitlist_email_subject` | `Your drop is live!` | ### Per-product settings [Section titled “Per-product settings”](#per-product-settings) Edit any product and find the **SwftDrops** panel in the product data tabs: | Field | Key | Description | | -------------------- | ---------------------------- | ------------------------------------------------------- | | Drop date/time | `_swft_drop_datetime` | UTC datetime string | | Exclusive link token | `_swft_drop_token` | Random token appended to product URL for access control | | Notify waitlist | `_swft_drop_notify_waitlist` | Whether to email the waitlist when the drop opens | ## Extension data [Section titled “Extension data”](#extension-data) SwftDrops adds the following to `extensions.drops` in the session when the cart contains a drop product: ```json { "drop_id": 1042, "product_id": 456, "drop_name": "Summer Drop 01", "dropped_at": "2026-06-01T12:00:00Z" } ``` Available in the checkout frontend via: ```js window.SwftCheckout.getExtension('drops') ``` ## Order meta [Section titled “Order meta”](#order-meta) | Meta key | Value | | ----------------- | ------------------------- | | `_swft_ext_drops` | Full drop data as JSON | | `_swft_drop_id` | Drop ID for easy querying | ## Events fired [Section titled “Events fired”](#events-fired) SwftDrops fires one additional event on the product page (not in the checkout): ```js document.addEventListener('swftdrops:live', (e) => { console.log(`Drop ${e.detail.dropId} is now live`) // e.detail: { dropId: number, productId: number } }) ``` This fires when the countdown reaches zero and the product becomes purchasable. ## PHP hooks [Section titled “PHP hooks”](#php-hooks) ```php // Modify the extension data add_filter( 'swft_drops_extension_data', function( array $data, int $product_id ): array { return $data; }, 10, 2 ); // Fire after a drop goes live (useful for sending waitlist notifications) add_action( 'swft_drops_went_live', function( int $drop_id, int $product_id ): void { // e.g. send push notifications }, 10, 2 ); // Filter the waitlist notification email add_filter( 'swft_drops_waitlist_email', function( array $email, int $drop_id ): array { return $email; }, 10, 2 ); ``` # Swft Funnels Add post-payment upsell sequences to Swft Checkout. After the shopper completes payment, they see one or more targeted offers — each accepted with a single click against the payment method they just used — before landing on the confirmation screen. Swft Funnels is free. ## What it does [Section titled “What it does”](#what-it-does) Each funnel step is a single product offer with: * A headline and supporting copy. * A product (or product variation). * An optional discount (percentage or fixed amount off the standard price). Shoppers see a sequence of these screens after payment. Each is a yes/no choice: accept the offer and one-click charge to the same payment method, or skip to the next step. The last screen is the standard order confirmation. Funnels are ideal for warranty add-ons, related products, subscription upsells, or simple “would you like to add X for £Y?” upsells. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * WordPress 6.0+ * WooCommerce 8.0+ * PHP 7.4+ * Swft Checkout active ## Installation [Section titled “Installation”](#installation) 1. Install the **Swft Funnels** plugin from your Swft dashboard’s downloads section. 2. Activate it. 3. Go to **WooCommerce → Swft → Funnels**. ## Configuring a funnel [Section titled “Configuring a funnel”](#configuring-a-funnel) 1. Click **Add funnel step**. 2. Choose the product to offer (the search box uses an AJAX product search across all WooCommerce products). 3. Write the headline (e.g. “Add a 2-year warranty for £19”). 4. Write supporting copy explaining the offer. 5. Set the discount: a percentage off, a fixed amount off, or leave blank for the standard price. 6. Save. Funnels are evaluated in the order you create them. Reorder by drag-and-drop. ## Customer experience [Section titled “Customer experience”](#customer-experience) After the shopper pays for their main order: 1. **Funnel step 1** appears. They click **Add to order** or **No thanks**. 2. If they accept, the offer product is added to their order using their stored payment method (no re-entry of card details). 3. **Funnel step 2** appears. Repeat. 4. After the last step (or after a “skip” on the first step), the standard Swft confirmation screen renders. The original order is updated with any accepted offers; the shopper sees a single combined order in their email confirmation. ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) The plugin hooks `swft_session_extensions` to attach the funnel configuration to each session. After the main payment succeeds, the Swft API recognises the `funnels` extension and routes the shopper through `/upsell` screens before `/confirmation`. Accepted offers are charged against the stored payment method via Stripe (Stripe-only for now; PayPal and NomuPay funnels are not yet supported). The charge re-uses the existing PaymentIntent’s payment method. Funnel configuration is stored as JSON in the `swft_funnels_config` WordPress option. ## Gotchas [Section titled “Gotchas”](#gotchas) * Funnels only work with **Stripe** today. PayPal and NomuPay payments skip the funnel and go straight to confirmation. * Offered products must be **purchasable and in stock** at the moment the funnel renders. * Discounts apply to the offered product only — they don’t stack with cart-level discounts. * If a one-click upsell charge fails (declined card, expired auth), the funnel skips that step and continues. The shopper isn’t shown the failure. * Don’t put more than 2-3 funnel steps in a row. Conversion data shows fatigue past that point. # SwftGifts Gift message and gift wrap for Swft Checkout. Customers add a personalised message and optionally request gift wrapping. Both are preserved through the checkout and written to the WooCommerce order. ## What it does [Section titled “What it does”](#what-it-does) * Adds a **Gift Message** textarea to the Swft Cart drawer * Adds a **Gift Wrap** toggle with configurable price * Passes both through the checkout session as `extensions.gifts` * Adds a gift wrap fee row to the checkout order summary * Writes gift data to the WooCommerce order as `_swft_ext_gifts` * Adds an order note for the packing team ## Installation [Section titled “Installation”](#installation) 1. Download `swft-gifts.zip` from [swft.co.uk/modules](https://swft.co.uk/modules) 2. Install via **Plugins → Add New → Upload Plugin** 3. Activate ## Configuration [Section titled “Configuration”](#configuration) Go to **WooCommerce → Settings → SwftGifts**: | Setting | Option | Default | | ------------------- | ---------------------------- | ------------- | | Enable gift message | `swft_gifts_message_enabled` | `yes` | | Enable gift wrap | `swft_gifts_wrap_enabled` | `yes` | | Gift wrap price | `swft_gifts_wrap_price` | `299` (pence) | | Wrap product SKU | `swft_gifts_wrap_sku` | “ | | Notification email | `swft_gifts_notify_email` | “ | If **Wrap product SKU** is set, SwftGifts adds that product to the WooCommerce order line items when wrap is selected, so it appears in fulfilment and inventory. ## Extension data [Section titled “Extension data”](#extension-data) SwftGifts adds the following to `extensions.gifts` in the session: ```json { "message": "Happy birthday!", "wrap": true, "price": 299 } ``` Available in the checkout frontend via: ```js window.SwftCheckout.getExtension('gifts') // { message: 'Happy birthday!', wrap: true, price: 299 } ``` ## Order meta [Section titled “Order meta”](#order-meta) After payment, the WooCommerce order receives: | Meta key | Value | | ----------------- | ------------------------------------------------------- | | `_swft_ext_gifts` | `{"message":"Happy birthday!","wrap":true,"price":299}` | ## Events fired [Section titled “Events fired”](#events-fired) SwftGifts does not fire custom events beyond the standard Swft Cart events. Listen for `swftcart:cart-updated` to detect when gift options change. ## PHP filter [Section titled “PHP filter”](#php-filter) SwftGifts exposes one filter to modify the extension data before it is sent: ```php add_filter( 'swft_gifts_extension_data', function( array $data ): array { // e.g. override price for premium members if ( wc_memberships_is_user_active_member( get_current_user_id(), 'premium' ) ) { $data['price'] = 0; } return $data; } ); ``` # Swft License Delivers software license keys to customers on the Swft Checkout confirmation screen, immediately after payment. Supports custom key pools you paste in, License Manager for WooCommerce, WC Software Add-On, and arbitrary order meta fields. Swft License is free. ## What it does [Section titled “What it does”](#what-it-does) When a shopper buys a product that has a license key configured, the key appears on the confirmation screen alongside their order details — and in the WooCommerce confirmation email. No follow-up email required, no portal login. Each product on your store can use a different license source: a stored pool of pre-generated keys, an external license manager plugin, or a custom field that holds the key. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * WordPress 6.0+ * WooCommerce 8.0+ * PHP 7.4+ * Swft Checkout active * One of (optional, depending on source): * [License Manager for WooCommerce](https://wordpress.org/plugins/license-manager-for-woocommerce/) — for managed key pools with activation tracking * [WC Software Add-On](https://woocommerce.com/products/woocommerce-software-add-on/) — for WooCommerce-native license management ## Installation [Section titled “Installation”](#installation) 1. Install the **Swft License** plugin from your Swft dashboard’s downloads section. 2. Activate it. 3. Edit any product. You’ll see a new **Swft License** metabox. ## Configuring a product [Section titled “Configuring a product”](#configuring-a-product) Pick one of four sources from the **License source** dropdown in the product metabox: | Source | When to use | Where keys come from | | ----------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | **Custom pool** | You have a flat list of pre-generated keys. | Paste keys (one per line) into the metabox text area. Each order pulls the next unused key. | | **License Manager for WooCommerce** | You’re already using LMFWC. | The plugin asks LMFWC for a key from the product’s configured pool on each order. | | **WC Software Add-On** | You’re using WooCommerce’s official license add-on. | The plugin reads from WCSA’s standard meta. | | **Order meta field** | You manually populate keys per-order from another system. | Specify the meta key name (e.g. `_my_custom_license`). The plugin reads that meta when surfacing the key. | Save the product. ## Customer experience [Section titled “Customer experience”](#customer-experience) When payment completes: 1. The plugin assigns a license key to each licensed line item. 2. The keys are stored in the order’s `_swft_license_keys` meta. 3. The Swft Checkout confirmation screen reads that meta and displays each key with a “Copy” button. 4. The standard WooCommerce confirmation email also includes the keys at the bottom of the order detail. ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) On `woocommerce_payment_complete`, the plugin iterates the order’s line items. For each item, it looks up the configured license source on the product, fetches a key, and stores it in the order meta keyed by line item ID. Custom pool fetches use a transient-based lock to prevent two simultaneous orders from picking up the same key. The Swft API reads `_swft_license_keys` when serving the confirmation screen. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Source must be set per product.** Products with no source assigned won’t receive a key, even if a pool exists for another product. * Custom pool keys are **consumed**. If you run out, new orders won’t get keys until you paste more in. Monitor stock. * License Manager and WC Software Add-On must be **active** when the order completes. If you deactivate them, the plugin can’t fetch keys. * Refunded or cancelled orders don’t automatically release their consumed keys back to the pool. Manual recovery is required if you re-issue. * Custom pool keys are stored in plain text in the database. If your keys are sensitive, prefer License Manager which encrypts at rest. # Swft Offload Archives old WooCommerce orders out of your database into Swft Cloud (Cloudflare R2). Your admin stays fast as order counts grow, customers still see their full order history in My Account, and any archived order can be restored on demand. Swft Offload is a paid add-on starting at **£5/month**. The plugin itself is free; you pay for cloud storage and retrieval. ## What it does [Section titled “What it does”](#what-it-does) WooCommerce keeps every order in your database forever. Past a few thousand orders, the admin slows down: order screens take seconds to load, reports time out, and any plugin that does a `JOIN` on `wp_posts` or HPOS tables drags. Offload pulls orders older than a threshold off the database and stores them in Swft Cloud, replaying them back to your site whenever a customer (or you) needs them. The plugin is HPOS-aware and works with both the new order tables and the legacy custom-post-type layout. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * WordPress 6.0+ * WooCommerce 8.0+ * PHP 7.4+ * Swft account with the Offload add-on enabled * WP-Cron enabled (Offload runs a daily archive job) ## Installation [Section titled “Installation”](#installation) 1. Install the **Swft Offload** plugin from your Swft dashboard’s downloads section. 2. Activate it in **Plugins** → **Swft Offload**. 3. Go to **WooCommerce → Settings → Offload**. 4. Paste your Swft API key (the same one you use for Swft Checkout). 5. Configure the archive threshold (default: 180 days), batch size, and which order statuses to include. ## Settings [Section titled “Settings”](#settings) | Setting | What it does | | ---------------------------- | ------------------------------------------------------------------------------------------- | | **Archive threshold** | Orders older than this many days are eligible for archival. Default 180. | | **Batch size** | How many orders to archive per cron run. Default 100. Increase if you have a large backlog. | | **Statuses to archive** | Which order statuses qualify. Default: `completed`, `refunded`, `cancelled`, `failed`. | | **Purge cloud on uninstall** | Opt-in destructive cleanup. Off by default. | ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) A daily cron selects qualifying orders, uploads them to Swft Cloud in batches, and writes a row to the local `swft_offload_orders` index table (one row per archived order ID, with customer email and storage key). The plugin hooks into `woocommerce_get_orders` so that whenever WooCommerce or My Account asks for an archived order, the plugin transparently fetches it from Swft Cloud. Restoration via `POST /wp-json/swft-offload/v1/restore` replays the full order back into WooCommerce — line items, fees, refunds, customer notes, meta, original dates — idempotently. The endpoint is rate-limited and IP-checked. ## Pricing tiers [Section titled “Pricing tiers”](#pricing-tiers) | Tier | Object limit | Storage | Monthly | | ---------- | ---------------------- | ------- | ---------- | | Starter | 10,000 archived orders | 1 GB | £5 | | Growth | 100,000 | 10 GB | £15 | | Scale | 1,000,000 | 100 GB | £40 | | Enterprise | Custom | Custom | Contact us | Usage is tracked in your Swft dashboard under **Offload**. ## Gotchas [Section titled “Gotchas”](#gotchas) * Archived orders contain personally identifiable data. Swft Cloud encrypts at rest, but the data lives off your server — make sure your privacy policy reflects this. * WP-Cron must run reliably. If your site has very low traffic, set up a real cron job hitting `wp-cron.php` rather than relying on visitor-triggered cron. * Reports and analytics plugins that query the WC orders table directly will see fewer rows. Use your Swft dashboard for full-history reporting. ## Where to find more [Section titled “Where to find more”](#where-to-find-more) If you need help, contact or check your dashboard at [Offload](https://app.swft.co.uk/offload). # Swft Reviews Automated post-purchase review request emails, plus an optional review prompt on the Swft Checkout confirmation screen. Supports Google, Trustpilot, Yelp, Facebook, and any custom review URL. Swft Reviews is free. ## What it does [Section titled “What it does”](#what-it-does) Two surfaces: 1. **Confirmation-screen prompt** — a “Loved your purchase? Leave a review” panel that appears on the Swft Checkout confirmation screen with a link to your chosen review platform. 2. **Post-purchase email** — a scheduled email sent X days after order completion, asking the customer to leave a review. You pick the platform (or a custom URL) and the delay. The plugin handles scheduling and sending. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * WordPress 6.0+ * WooCommerce 8.0+ * PHP 7.4+ * WP-Cron enabled (the review email is scheduled via WP-Cron) * A working WP mail setup (SPF + DKIM strongly recommended) ## Installation [Section titled “Installation”](#installation) 1. Install the **Swft Reviews** plugin from your Swft dashboard’s downloads section. 2. Activate it. 3. Go to **WooCommerce → Swft → Reviews**. ## Settings [Section titled “Settings”](#settings) | Setting | What it does | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **Enable reviews** | Master toggle. | | **Platform** | Google, Trustpilot, Yelp, Facebook, or Custom URL. | | **Review URL** | The full URL the customer is sent to. For Google, this is your “Write a review” link from Google Business Profile. | | **Email delay (days)** | How long after order completion the email fires. Default 7. | | **Email subject** | The subject line of the review request email. Defaults to “How did we do?”. | | **Show prompt on confirmation screen** | Toggle the confirmation-screen panel on/off. | ## Customer experience [Section titled “Customer experience”](#customer-experience) **Confirmation screen** — immediately after payment, the panel appears: > Loved your purchase? Leave a review on Google → \[Open review] The link opens in a new tab, so the shopper doesn’t lose their confirmation context. **Email** — N days after `woocommerce_order_status_completed`, the customer receives an email with: * Your order subject line. * A short message thanking them. * A button linking to the review platform. ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) The confirmation-screen prompt is hooked into Swft Checkout via the `swft_session_extensions` filter when **Show prompt on confirmation screen** is on. The email is scheduled on `woocommerce_order_status_completed`. The plugin uses `wp_schedule_single_event('swft_send_review_request', time() + DELAY, [order_id])`. When the event fires, it sends the email via `wp_mail()` using WooCommerce’s email template system (so it inherits your store branding). ## Gotchas [Section titled “Gotchas”](#gotchas) * **WP-Cron must run reliably.** Low-traffic sites should set a real system cron hitting `wp-cron.php` rather than depending on visitor-triggered cron. * **Deliverability is your responsibility.** Configure SPF and DKIM for your domain or review request emails will land in spam. The plugin uses `wp_mail()` — set up an SMTP plugin or transactional service if your default mail isn’t reliable. * **Custom URLs aren’t validated.** If you paste a broken URL, the plugin will happily send customers to a 404. * One email per order. The plugin doesn’t repeat-ask; if the customer ignores the first email, that’s it. * If the order is refunded before the email fires, the email still sends. Consider deactivating the plugin if you have a lot of refund volume. # SwftSubscriptions Subscription product support for Swft Checkout. Enables WooCommerce Subscriptions (or WooCommerce Simple Subscriptions) products to go through the Swft checkout flow, with recurring billing handled via Stripe’s subscription APIs. ## What it does [Section titled “What it does”](#what-it-does) * Detects subscription products in the cart * Passes subscription billing interval, period, and trial data through the session * Creates a Stripe `SetupIntent` (for future billing) instead of a `PaymentIntent` when the cart is subscription-only * Creates a `PaymentIntent` + `SetupIntent` for mixed carts (subscription + regular products) * After payment, creates a Stripe Customer and Subscription * Syncs subscription state to WooCommerce Subscriptions ## Compatibility [Section titled “Compatibility”](#compatibility) | Plugin | Status | | -------------------------------- | ------------------------- | | WooCommerce Subscriptions | Supported | | WooCommerce Simple Subscriptions | Supported | | YITH WooCommerce Subscriptions | Partial (contact support) | ## Installation [Section titled “Installation”](#installation) 1. Install and activate WooCommerce Subscriptions (or compatible plugin) 2. Download `swft-subscriptions.zip` from [swft.co.uk/modules](https://swft.co.uk/modules) 3. Install and activate SwftSubscriptions ## Configuration [Section titled “Configuration”](#configuration) Go to **WooCommerce → Settings → SwftSubscriptions**: | Setting | Option | Default | | --------------------------- | -------------------------------- | --------------------------------------------- | | Enable trial period display | `swft_subs_show_trial` | `yes` | | Trial label | `swft_subs_trial_label` | `Free for {days} days, then {price}/{period}` | | Cancel at period end | `swft_subs_cancel_at_period_end` | `no` | ## Extension data [Section titled “Extension data”](#extension-data) SwftSubscriptions adds the following to `extensions.subscription` in the session when the cart contains a subscription product: ```json { "product_id": 101, "type": "subscription", "billing_period": "month", "billing_interval": 1, "trial_length": 14, "trial_period": "day", "initial_amount": 0, "recurring_amount": 2999, "currency": "GBP", "sign_up_fee": 0 } ``` The checkout frontend reads this to: * Replace the pay button label: `Start free trial` / `Subscribe — £29.99/mo` * Show a trial period notice below the pay button * Use a `SetupIntent` flow instead of `PaymentIntent` for zero-amount initial charges ## Payment flow [Section titled “Payment flow”](#payment-flow) ### Regular subscription (no trial) [Section titled “Regular subscription (no trial)”](#regular-subscription-no-trial) ```plaintext SetupIntent created → customer enters card → Stripe charges initial amount → Stripe Customer created → Stripe Subscription created → WooCommerce Subscription activated ``` ### Trial subscription (initial amount = 0) [Section titled “Trial subscription (initial amount = 0)”](#trial-subscription-initial-amount--0) ```plaintext SetupIntent created → customer enters card → card saved, no charge → Stripe Customer created → Stripe Subscription created with trial_end date → Trial billing fires automatically at trial end → WooCommerce Subscription activated ``` ### Mixed cart (subscription + regular products) [Section titled “Mixed cart (subscription + regular products)”](#mixed-cart-subscription--regular-products) ```plaintext PaymentIntent created for regular items → SetupIntent for subscription card → Both confirmed in one Stripe Elements flow → Regular order + Subscription created in WooCommerce ``` ## Order meta [Section titled “Order meta”](#order-meta) | Meta key | Value | | ------------------------------ | ------------------------------ | | `_swft_ext_subscription` | Full subscription data as JSON | | `_swft_stripe_customer_id` | Stripe Customer ID | | `_swft_stripe_subscription_id` | Stripe Subscription ID | ## PHP hooks [Section titled “PHP hooks”](#php-hooks) ```php // Modify subscription extension data before it is sent add_filter( 'swft_subscription_extension_data', function( array $data, WC_Product $product ): array { return $data; }, 10, 2 ); // Fired after the Stripe Subscription is created add_action( 'swft_subscription_created', function( string $stripe_sub_id, int $wc_order_id ): void { // sync to your CRM }, 10, 2 ); // Fired when a subscription renewal payment succeeds (Stripe webhook) add_action( 'swft_subscription_renewed', function( string $stripe_sub_id, int $wc_order_id ): void { // update member access }, 10, 2 ); // Fired when a subscription is cancelled add_action( 'swft_subscription_cancelled', function( string $stripe_sub_id, int $wc_subscription_id ): void { // revoke access }, 10, 2 ); ``` # Swft Wallet A store-credit and cashback system for WooCommerce. Customers earn wallet balance through cashback, signup bonuses, and referrals; they spend it at checkout against any order. Includes peer-to-peer transfers and bank withdrawals. Swft Wallet is free forever — there is no Pro tier. ## What it does [Section titled “What it does”](#what-it-does) Every registered customer gets a wallet. Balance comes from: * **Signup bonus** — a one-time credit when a customer registers. * **Cashback** — a percentage of every completed order is added back to the wallet. * **Referral bonus** — both the referrer and referee can get a credit when a referral converts. * **Manual credit** — admins can credit or debit any wallet from the admin panel. * **Transfers** — customers can transfer balance to each other (subject to a configurable fee). * **Withdrawals** — customers can request a bank withdrawal; admin approves and processes manually. Balance is spent at checkout as a payment method (`Swft Wallet`) — either covering the whole order or partial against the rest. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * WordPress 6.0+ * WooCommerce 8.0+ * PHP 7.4+ * Database user with `CREATE TABLE` privileges (the plugin creates a custom transactions table on activation) ## Installation [Section titled “Installation”](#installation) 1. Install the **Swft Wallet** plugin from your Swft dashboard’s downloads section. 2. Activate it. 3. Go to **WooCommerce → Swft Wallet → Settings**. ## Settings [Section titled “Settings”](#settings) | Setting | What it does | | -------------------------- | ---------------------------------------------------------------------------------------------- | | **Signup bonus** | One-time credit when a new customer registers (e.g. £5). 0 disables. | | **Referral bonus** | Credit to the **referrer** when their referee makes their first purchase. | | **Referee bonus** | Credit to the **referee** when they make their first purchase via a referral link. | | **Cashback rate** | Percentage of every completed order added back to the customer’s wallet (e.g. 2%). 0 disables. | | **Cashback expiry (days)** | How long cashback stays valid before it’s clawed back. 0 = never expires. | | **Transfer fee** | Flat fee deducted from the sender’s wallet on each P2P transfer. | | **Withdrawal minimum** | Minimum balance a customer must accumulate before they can request a withdrawal. | | **API key** | Used by Swft Checkout to verify wallet balance during the checkout flow. | ## Customer experience [Section titled “Customer experience”](#customer-experience) **Account page** — customers see their wallet balance, transaction history, transfer form, and withdrawal request form. **Checkout** — when the wallet has a positive balance, **Swft Wallet** appears as a payment method. Customers can: * Apply their full balance and pay the difference with another method. * Apply a partial amount of their choice. * Pay the full order using only wallet balance. **Earning** — cashback is awarded automatically when an order moves to **Completed**. Referral bonuses are awarded when the referee places their first order. ## Admin pages [Section titled “Admin pages”](#admin-pages) `WooCommerce → Swft Wallet` has five tabs: | Tab | Purpose | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | **Customers** | List of all wallets with balances. Search by email; click through to credit/debit. | | **Transactions** | Full ledger of every credit and debit. Filterable by customer and type. | | **Cashback Rules** | Per-product or per-category cashback overrides. Override the global rate for specific items (e.g. higher cashback on a clearance category). | | **Withdrawals** | Pending and processed withdrawal requests. Approve, mark paid, or reject. | | **Settings** | The settings table above. | ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) On activation, the plugin creates `swft_wallet_transactions` — one row per credit or debit, with type, amount, balance-after, and reference (order ID, transfer ID, withdrawal ID). The plugin registers a WooCommerce payment gateway (`swft_wallet`) that supports both full and partial payment. Cashback is awarded via `woocommerce_order_status_completed`. Referral bonuses are awarded via `user_register` (on a referee using a referral link cookie) and the referee’s first `woocommerce_order_status_completed`. Three REST endpoints power the Swft Checkout integration: * `GET /swft-wallet/v1/balance` — current balance for the logged-in user. * `POST /swft-wallet/v1/debit` — deduct from the wallet (called by Swft Checkout on order completion). * `POST /swft-wallet/v1/credit` — add to the wallet (used for refunds and manual credits). Withdrawals are stored in a `swft_wallet_withdrawals` table with `status` (pending / approved / paid / rejected). Processing is manual — there’s no automatic bank-rail integration. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Cashback only fires on Completed.** Orders stuck in Processing won’t award cashback until they move forward. * **Withdrawal processing is manual.** The admin must approve each request and arrange the bank transfer separately. The plugin doesn’t connect to a payment rail. * **Cashback expiry is hard.** When the configured days elapse, expired cashback is clawed back from the wallet. If the customer has spent it, this can leave them negative — set a sensible expiry or 0. * **Transfer fee is deducted from the sender.** The receiver gets the full amount. * **Wallet balance is real money on your books.** Reconcile carefully with your accounting; outstanding wallet balance is a liability. # Authentication The Swft Partner API uses opaque bearer tokens. Provision one via the workspace dashboard (Workspace → Settings → Partner API Keys) or by calling `POST /v2/workspaces/{workspaceId}/partner-keys` with your Supabase JWT. ```http Authorization: Bearer swft_ak_live_01HZ9X... X-Swft-Merchant: 8a1d52e0-... (only on merchant-scoped routes) Idempotency-Key: 4ea3... (required on all writes) Swft-Version: 2026-06-01 (optional; pinned per key) ``` ## Key prefixes [Section titled “Key prefixes”](#key-prefixes) | Prefix | Mode | Purpose | | ---------------- | ---- | ----------------------------------------- | | `swft_ak_live_*` | live | Production traffic. Bills real cards. | | `swft_ak_test_*` | test | Sandbox traffic against Stripe test mode. | The prefix is shown next to the key in the dashboard and emitted in audit logs. The plaintext value is shown exactly once at creation — save it then. ## Scopes [Section titled “Scopes”](#scopes) Keys carry an explicit `scopes` array. Defaults: `merchants:read`, `merchants:write`, `webhooks:read`, `webhooks:write`, `domains:write`, `logs:read`. A request hitting an endpoint outside its scopes gets a 403 `insufficient_scope`. ## Versioning [Section titled “Versioning”](#versioning) Wire-format changes ship behind a new dated version. Keys are **pinned** to the latest version at mint time. To opt into a newer date, send `Swft-Version: 2026-XX-XX` on the request. Existing keys stay pinned forever unless they explicitly opt-in. ## Idempotency [Section titled “Idempotency”](#idempotency) Every POST/PUT/PATCH on `/v2/agencies/*` and `/v2/webhooks/*` requires an `Idempotency-Key` header (recommendation: a v4 UUID per request). * Same key + same body within 24h → identical response replayed. * Same key + different body → `409 idempotency_key_reused`. Stripe-style. Safe to retry network failures. ## Request IDs [Section titled “Request IDs”](#request-ids) Every response carries `swft-request-id` and `traceparent` headers. Capture the request id in your support tickets — it pinpoints the exact log row in `GET /v2/logs/requests`. # Errors Every Swft error response uses the same envelope — RFC 9457 *Problem Details* compatible AND Stripe-shape, so any SDK that already groks Stripe-style envelopes can consume it unchanged. ```json { "error": { "type": "https://docs.swft.co.uk/errors/idempotency_key_reused", "code": "idempotency_key_reused", "message": "Idempotency-Key was reused with a different request body.", "doc_url": "https://docs.swft.co.uk/errors/idempotency_key_reused", "request_id": "req_01HZ9X...", "param": "Idempotency-Key" } } ``` `request_id` always echoes the value of the response `swft-request-id` header — paste it into support tickets. ## Codes [Section titled “Codes”](#codes) | HTTP | Code | Meaning | | ---- | ------------------------------ | ------------------------------------------------------------------------------------------------------------ | | 400 | `bad_request` | Malformed input. | | 400 | `missing_parameter` | A required parameter is missing. `param` names it. | | 400 | `invalid_parameter` | A parameter failed validation. `param` names it. | | 400 | `missing_idempotency_key` | Endpoint requires `Idempotency-Key`. | | 400 | `missing_swft_merchant_header` | Endpoint requires `X-Swft-Merchant`. | | 400 | `unsupported_api_version` | Swft-Version is not recognised. `meta.supported` lists valid versions. | | 401 | `missing_authorization` | No `Authorization: Bearer` header. | | 401 | `invalid_api_key` | Key not found or revoked. | | 403 | `forbidden` | Authenticated but not authorised. | | 403 | `insufficient_scope` | Key lacks a required scope. `meta.required` / `meta.missing` list them. | | 403 | `merchant_not_in_workspace` | `X-Swft-Merchant` does not belong to the key’s workspace. | | 404 | `merchant_not_found` | Merchant id unknown. | | 404 | `workspace_not_found` | Workspace id unknown. | | 404 | `resource_not_found` | Generic not-found. | | 404 | `webhook_delivery_not_found` | Delivery id not in this workspace. | | 409 | `merchant_already_exists` | `store_url` already registered. | | 409 | `idempotency_key_reused` | Key reused with different body, or request still in-flight. | | 409 | `pre_confirmation_rejected` | Partner pre-confirmation hook returned `available:false`. `meta.reason` repeats the partner-supplied reason. | | 409 | `custom_domain_already_set` | Merchant already has a custom domain assigned. | | 429 | `rate_limited` | Slow down. `meta.retry_after` is seconds. | | 503 | `pre_confirmation_unavailable` | Partner endpoint timed out / 5xx’d and fail\_mode=‘closed’. | ## Verification [Section titled “Verification”](#verification) The `type` URL is stable. Add it to your SDK’s switch statement without parsing the human-readable `message`. Each URL also serves as the documentation deeplink — link to it from your own error UI. # Merchant onboarding Onboard a new merchant under your workspace and get a Stripe Connect URL in one round trip. ```ts import { v4 as uuid } from 'uuid' const res = await fetch(`https://api.swft.co.uk/v2/agencies/${workspaceId}/merchants`, { method: 'POST', headers: { 'Authorization': `Bearer ${SWFT_PARTNER_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': uuid(), }, body: JSON.stringify({ name: 'Cobalt Pilates', store_url: 'https://cobalt-pilates.com', email: 'studio@cobalt-pilates.com', return_url: 'https://unstack.co.uk/studio/cobalt-pilates/stripe-return', refresh_url: 'https://unstack.co.uk/studio/cobalt-pilates/stripe-refresh', }), }) const { merchant, stripe } = await res.json() // Save merchant.id + merchant.api_key on YOUR side; redirect to stripe.onboarding_url redirectStudio(stripe.onboarding_url) ``` ## What happens [Section titled “What happens”](#what-happens) 1. Swft creates a merchant row with `workspace_id = yourWorkspaceId`, `created_via = 'agency'`, and fresh `api_key` / `api_secret` values. 2. Swft opens a Stripe Standard connected account with `metadata.swft_merchant_id` set, and stores the `stripe_account_id` on the merchant. 3. Swft creates a one-time Stripe AccountLink and returns the URL. 4. The studio completes Stripe’s hosted onboarding; Stripe redirects to your `return_url` when they’re done. ## Polling completion [Section titled “Polling completion”](#polling-completion) The merchant row’s `stripe_onboarded` flag flips to `true` when Stripe reports `details_submitted=true`. You can either: * Poll `GET /v2/agencies/me` with `X-Swft-Merchant: ` and inspect the returned scopes (the legacy `stripe_onboarded` field is being added to the introspection response in a future date version), OR * Subscribe to the `merchant.stripe.ready` webhook event (Phase 2) on the merchant’s webhook URL. ## Bundling custom domain [Section titled “Bundling custom domain”](#bundling-custom-domain) After creation, immediately assign a checkout subdomain in the same wizard step: ```ts await fetch( `https://api.swft.co.uk/v2/agencies/${workspaceId}/merchants/${merchant.id}/custom-domain`, { method: 'POST', headers: { 'Authorization': `Bearer ${SWFT_PARTNER_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': uuid(), }, body: JSON.stringify({ subdomain: 'cobalt-pilates' }), }, ) // → checkout served from cobalt-pilates.checkouts.unstack.co.uk ``` `subdomain` only works once your workspace has a delegated zone — see the DNS delegation guide. # Session extensions (booking metadata) Pass arbitrary partner-defined JSON through the entire checkout lifecycle by setting `extensions` on the cart payload when you create a session. Swft treats the contents as opaque and surfaces them verbatim in every webhook that fires off the session. ```ts import { Swft } from '@swft-checkout/js' const swft = new Swft({ apiKey: merchant.api_key }) const session = await swft.sessions.create({ cart: { items: [{ id: 'class:reformer-100', name: '60-min Reformer', quantity: 1, price: 2500 }], extensions: { booking: { class_schedule_id: 'cs_8f...', user_id: 'usr_31...', studio_id: 'stu_4d...', reformer_id: null, payment_type: 'single', }, }, }, }) window.location.href = session.sessionUrl ``` ## Where it shows up [Section titled “Where it shows up”](#where-it-shows-up) `extensions` ends up in two places: 1. **The `order.completed` (and `order.failed`, `cart.abandoned`, `refund.issued`) webhook** as a top-level `extensions` field alongside `data`. Use this to identify the booking on confirmation and create the booking row server-side. 2. **The pre-confirmation hook request body** as `extensions`. Use this to look up the class/inventory and decide `{ available: true | false }`. If the session creates a Stripe Subscription (memberships), Swft copies the extensions into `stripe.subscription.metadata.swft_extensions` (JSON-stringified) so every renewal/cancellation event also carries the same booking context. ## Schema [Section titled “Schema”](#schema) There is no schema on Swft’s side — `extensions` is `Record`. Define yours on the partner side; we recommend versioning your extension keys (`booking_v1`, `booking_v2`, …) so you can evolve the shape without ambiguity in the webhook handler. # Pre-confirmation hook A capacity check the partner controls — fires after the customer commits but BEFORE Stripe is charged. Use it for inventory-tight commerce: bookings, ticketing, classes, time-slotted appointments. ## Configure it once [Section titled “Configure it once”](#configure-it-once) ```ts await fetch( `https://api.swft.co.uk/v2/agencies/${ws}/merchants/${mid}/pre-confirmation`, { method: 'PUT', headers: { 'Authorization': `Bearer ${SWFT_PARTNER_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': uuid(), }, body: JSON.stringify({ url: 'https://api.unstack.co.uk/swft/capacity-check', fail_mode: 'closed', // default — 503 to client on partner timeout }), }, ) // → returns { url, secret, fail_mode } — SAVE the secret, never recoverable. ``` ## Handle the call [Section titled “Handle the call”](#handle-the-call) Swft POSTs to your `url` with Standard Webhooks-signed headers: ```http POST /swft/capacity-check webhook-id: msg_01HZ... webhook-timestamp: 1717000000 webhook-signature: v1, content-type: application/json { "session_id": "sess_...", "merchant_id": "mer_...", "extensions": { "booking": { ... } }, "cart": { "items": [...], "total_pence": 2500, "currency": "GBP" } } ``` Verify the signature, look up the class, and answer: ```ts // Available return Response.json({ available: true }) // Not available — pass a `reason` string; Swft echoes it in // `order.failed.data.reason` and to the API caller as // `error.meta.reason`. return Response.json({ available: false, reason: 'class_full' }) ``` ## Reference implementation (Next.js + Supabase) [Section titled “Reference implementation (Next.js + Supabase)”](#reference-implementation-nextjs--supabase) app/api/swft/capacity-check/route.ts ```ts import { verifyStandardWebhook } from '@swft-checkout/js/webhooks' export async function POST(req: Request) { const id = req.headers.get('webhook-id') ?? '' const timestamp = req.headers.get('webhook-timestamp') ?? '' const sig = req.headers.get('webhook-signature') ?? '' const body = await req.text() if (!verifyStandardWebhook({ id, timestamp, body, signatureHeader: sig, candidateSecrets: [process.env.SWFT_PRECONFIRM_SECRET!] })) { return new Response('bad signature', { status: 401 }) } const { session_id, extensions } = JSON.parse(body) const classId = extensions.booking?.class_schedule_id const { data: cls } = await supabase .from('class_schedules') .select('booked_count, capacity') .eq('id', classId) .single() if (!cls || cls.booked_count >= cls.capacity) { return Response.json({ available: false, reason: 'class_full' }) } return Response.json({ available: true }) } ``` ## Behaviour [Section titled “Behaviour”](#behaviour) | Partner response | API behaviour | | ------------------------------------------ | ----------------------------------------------------------------------------------------- | | `{ available: true }` | Stripe PI is created; checkout proceeds. | | `{ available: false, reason }` | `409 pre_confirmation_rejected`; session marked failed; `order.failed` fires with reason. | | Timeout (>5s) / 5xx + `fail_mode='closed'` | `503 pre_confirmation_unavailable`. | | Timeout / 5xx + `fail_mode='open'` | Proceed with payment; warning logged. | ## Why before the PaymentIntent? [Section titled “Why before the PaymentIntent?”](#why-before-the-paymentintent) Doing the check before Stripe is charged means we never have to refund. Stripe idempotency holds the partner’s commitment until the page either confirms or fails — no double-charge risk on retry. # Webhooks Swft signs every outbound webhook with the [Standard Webhooks](https://standardwebhooks.com) spec — the same headers OpenAI, Anthropic, and Google use in 2026. If your stack already verifies Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), drop Swft into it unchanged. ## Events [Section titled “Events”](#events) | Event | Fires when | | ----------------------------- | ---------------------------------------------------------------- | | `order.completed` | Stripe `payment_intent.succeeded` for a session-based order. | | `order.failed` | Payment failed OR pre-confirmation rejected. | | `cart.abandoned` | Recovery emails consider the cart abandoned. | | `refund.issued` | Stripe `charge.refunded`. | | `draw.winner_selected` | Lottery draw winners committed. | | `subscription.created` | Stripe `customer.subscription.created` for a session-driven sub. | | `subscription.renewed` | Recurring invoice paid (`billing_reason='subscription_cycle'`). | | `subscription.payment_failed` | Recurring invoice failed. | | `subscription.cancelled` | Stripe `customer.subscription.deleted`. | ## Payload shape [Section titled “Payload shape”](#payload-shape) ```json { "event": "order.completed", "created_at": "2026-06-01T12:00:00Z", "swft_version": "2.0", "merchant_id": "mer_8a1d...", "extensions": { "booking": { ... } }, "data": { "order_id": "ord_...", "session_id": "sess_...", "customer": { ... }, "billing_address": { ... }, "shipping_address": { ... }, "items": [ ... ], "subtotal_pence": 2500, "tax_pence": 0, "shipping_pence": 0, "discount_pence": 0, "total_pence": 2500, "currency": "GBP", "payment_method": "card" } } ``` `extensions` is present when the session was created with non-empty `extensions` (excluding `b2b`, which is broken out into its own `data.b2b` field for back-compat). ## Verifying signatures [Section titled “Verifying signatures”](#verifying-signatures) ```ts import { verifyStandardWebhook } from '@swft-checkout/js/webhooks' if (!verifyStandardWebhook({ id: req.headers.get('webhook-id')!, timestamp: req.headers.get('webhook-timestamp')!, body: rawBody, signatureHeader: req.headers.get('webhook-signature')!, candidateSecrets: [process.env.SWFT_WEBHOOK_SECRET!], })) return new Response('bad signature', { status: 401 }) ``` Pass MULTIPLE entries in `candidateSecrets` while you’re rotating secrets — both the old and new will verify until you remove the old one. ## Delivery log [Section titled “Delivery log”](#delivery-log) Every attempted delivery is persisted: ```ts GET /v2/webhooks/deliveries?event=order.completed&status=failed&limit=50 ``` Inspect a single delivery (includes the partner’s response body): ```ts GET /v2/webhooks/deliveries/{id} ``` ## Replay [Section titled “Replay”](#replay) ```ts POST /v2/webhooks/deliveries/{id}/replay Idempotency-Key: ``` Resets the attempt counter and re-fires immediately. Use this to recover after fixing a bug on your endpoint. ## Retry policy [Section titled “Retry policy”](#retry-policy) Exponential backoff with ±20% jitter, 16 attempts spread across roughly 3 days, then `abandoned`. Replay manually if you need to retry an abandoned delivery. # DNS delegation for partner subdomains To let Swft programmatically create `{studio}.checkouts.unstack.co.uk` subdomains on your behalf, delegate a sub-zone to Swft’s Cloudflare account. This is a one-time setup per workspace. ## One-time setup [Section titled “One-time setup”](#one-time-setup) 1. Decide on a sub-zone you control (e.g. `checkouts.unstack.co.uk`). 2. At your registrar, set the NS records for the sub-zone to point at the nameservers Swft gives you. 3. Swft creates the zone in its Cloudflare account and writes the `zone_id` + `apex` to your workspace row (`workspaces.partner_default_zone_id` / `workspaces.partner_default_zone_apex`). 4. From then on you call: ```http POST /v2/agencies/{ws}/merchants/{mid}/custom-domain { "subdomain": "cobalt-pilates" } ``` Swft registers `cobalt-pilates.checkouts.unstack.co.uk` as a Cloudflare custom hostname against the delegated zone. SSL provisions automatically; the merchant doesn’t add any DNS records. ## Manual hostname (no delegation) [Section titled “Manual hostname (no delegation)”](#manual-hostname-no-delegation) If you’d rather keep DNS in your own Cloudflare account, pass a fully-qualified `hostname` instead of `subdomain`. The merchant (or you) then add the `CNAME` + verification `TXT` records returned in the response. ```http POST /v2/agencies/{ws}/merchants/{mid}/custom-domain { "hostname": "checkout.example.com" } ``` The response includes: ```json { "hostname": "checkout.example.com", "cname_target": "checkout.swft.co.uk", "ssl_status": "pending_validation", "ownership_verification": { "type": "txt", "name": "_cf-custom-hostname.checkout.example.com", "value": "..." } } ``` `ssl_status` flips to `active` within \~5 minutes once both records resolve. # Observability Two tabs in your dashboard, both fed from the public API: ## Request log [Section titled “Request log”](#request-log) ```http GET /v2/logs/requests?limit=50 ``` Returns every `/v2/*` call your partner key made in the last 30 days (method, path, status code, latency, error code, request\_id). Filter with `method`, `path_prefix`, `status_min`, `status_max`. Inspect a single call: ```http GET /v2/logs/requests/{request_id} ``` The `request_id` is the value of the `swft-request-id` response header on every API call. ## Webhook deliveries [Section titled “Webhook deliveries”](#webhook-deliveries) ```http GET /v2/webhooks/deliveries?status=failed&limit=50 ``` Lists every delivery Swft attempted to your endpoint. Inspect bodies and replay individual rows — see the webhooks guide. ## Tracing [Section titled “Tracing”](#tracing) Every Swft response carries a W3C `traceparent` header. If you’re running OpenTelemetry on your side, ingest that header on inbound webhooks to correlate spans end-to-end. ## Audit log [Section titled “Audit log”](#audit-log) Every mutating action — `merchant.created`, `partner_key.revoked`, `webhook_delivery.replayed`, etc. — is recorded in the workspace audit log. Today the audit log is exposed only in the dashboard UI; an unauthenticated read endpoint is planned for a future date version. ## Rate limits [Section titled “Rate limits”](#rate-limits) Every response carries: ```plaintext X-RateLimit-Limit: 5000 X-RateLimit-Remaining: 4998 ``` 429 responses include `meta.retry_after` in seconds. # Releasing Plugins Every WordPress plugin in this monorepo ships via the same GitHub Actions workflow: [`.github/workflows/release-plugins.yml`](../.github/workflows/release-plugins.yml). Tag pattern → release. No manual zipping, no panel uploads. ## Tag conventions [Section titled “Tag conventions”](#tag-conventions) | Tag format | Plugin family | Plugin directory | | --------------------------------------------- | ------------------- | ----------------------------------------------------------------------- | | `swft-checkout-v` | Swft Checkout | `plugin/` | | `swft-wallet-v` | Swft Wallet | `plugin-wallet/` | | `swft-lottery-compat-v` | Swft Lottery Compat | `plugin-lottery-compat/` | | `swft--v` | other Swft plugins | `plugin-/` | | `fp-prize-portal-v` | FP Prize Portal | `Lottery Ecosystem/Current Plugins/fp-prize-portal/` | | `fp-instant-win-terrawallet-cu-sync-v` | FP Instant Win Sync | `Lottery Ecosystem/Current Plugins/fp-instant-win-terrawallet-cu-sync/` | | `fp-addons-v` | FP Add-ons | `Lottery Ecosystem/Current Plugins/fp-addons/` | | `fp--v` | other FP plugins | `Lottery Ecosystem/Current Plugins/fp-/` | Version segment must be three dot-separated numbers (`2.5.2`, not `2.5` and not `2.5.2-rc1`) so Plugin Update Checker can compare it. ## Release flow [Section titled “Release flow”](#release-flow) 1. **Bump the plugin’s version** in BOTH the file header and any `define( '*_VERSION', ... )` constant. The values must match — the WP admin UI reads the header, but the plugin’s update checker reads the constant. 2. **Commit the bump** (any message; the commit doesn’t have to mention “release”): ```bash git add path/to/plugin/ git commit -m "fix(fp-prize-portal): v2.5.2 — ..." git push ``` 3. **Tag the commit and push the tag:** ```bash git tag fp-prize-portal-v2.5.2 git push origin fp-prize-portal-v2.5.2 ``` 4. **CI takes over**: * Workflow `Release Plugin` triggers on the tag * Resolves the plugin directory by parsing the tag prefix * Stages the plugin as `/` (canonical WP folder name) * For `fp-*` tags only: downloads pinned Plugin Update Checker (`PUC_VERSION` env in the workflow) and vendors it into `/lib/plugin-update-checker/` * Builds `.zip` with the dev cruft excluded * Creates a GitHub Release named e.g. `fp-prize-portal v2.5.2` with the zip attached 5. **Verify** at `https://github.com/BuiltByGo/swft-app/releases` — the new release should appear within \~30 seconds with the zip asset. ## Installing on staging [Section titled “Installing on staging”](#installing-on-staging) ```bash # Replace the URL with the asset URL from the release page wp plugin install \ https://github.com/BuiltByGo/swft-app/releases/download/fp-prize-portal-v2.5.2/fp-prize-portal.zip \ --activate --force ``` `--force` overwrites an existing install (the WP “Replace current with uploaded” prompt). Skip it on first install. ## Auto-updates on production [Section titled “Auto-updates on production”](#auto-updates-on-production) Once a plugin has been installed from a GitHub Release (so the bundled Plugin Update Checker is present), subsequent releases on this repo will appear in **WP admin → Dashboard → Updates** within \~12 hours, or instantly if the merchant clicks “Check Again.” To force a check immediately: ```bash wp eval 'delete_site_transient("update_plugins");' wp plugin update fp-prize-portal # or fp-addons, etc. ``` ## Yanking a release [Section titled “Yanking a release”](#yanking-a-release) If a release is broken: 1. Delete the release on GitHub (web UI → release → Delete) — this removes the asset and the Update URI lookup will roll back to the previous release. 2. Delete the tag locally + on origin: ```bash git tag -d fp-prize-portal-v2.5.2 git push origin :refs/tags/fp-prize-portal-v2.5.2 ``` 3. Bump the version (e.g. v2.5.3) with the fix and tag again. Don’t re-use the deleted version number — sites that managed to update before you yanked won’t pick up a re-tagged v2.5.2. ## Plugin Update Checker details [Section titled “Plugin Update Checker details”](#plugin-update-checker-details) PUC v5 is bundled into `fp-*` zips at build time (`PUC_VERSION` env in the workflow controls the pinned version; currently `v5.5`). Each FP plugin’s main file loads PUC behind a `file_exists` guard, so: * **Source installs** (cloning the repo into `wp-content/plugins/` for dev) → no PUC, no auto-updates, plugin works manually. * **Release zip installs** (the standard path) → PUC loaded, WP detects new tags via GitHub Releases API. Each plugin’s PUC config restricts release matching to its own tag prefix so a `swft-wallet` tag never offers an update to `fp-prize-portal`. The pattern is `/^-v(\d+\.\d+\.\d+)$/i` matched against tag names, plus an asset filename filter `/^\.zip$/i`. To bump PUC version globally: edit `PUC_VERSION` in the workflow, tag any FP plugin, verify the resulting zip’s `lib/plugin-update-checker/` matches the new pinned version. ## When a release fails [Section titled “When a release fails”](#when-a-release-fails) The workflow logs are at **Actions → Release Plugin → \**. Common failure modes: | Symptom | Cause | Fix | | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `ERROR: directory 'X' not found for slug 'Y'` | Tag prefix doesn’t match an existing plugin dir | Check the tag — typo, or the dir hasn’t been added to the resolver in `release-plugins.yml` | | `curl: (22) The requested URL returned error: 404` during PUC vendor | `PUC_VERSION` env points at a tag that doesn’t exist on YahnisElsts/plugin-update-checker | Check upstream, update `PUC_VERSION` | | Zip built but release not created | `GITHUB_TOKEN` missing `contents: write` permission | Verify the workflow has `permissions: contents: write` | | Release created but PUC doesn’t detect it on Mercury | Tag format mismatch with `setReleaseVersionFilter()` regex | Confirm tag is exactly `-v` (no `-rc1`, no two-segment versions) | # Swft Cart Swft Cart is a free WooCommerce cart drawer plugin. It is completely independent of Swft Checkout — you can run either or both. **Free forever. No licence key. No upsell.** ## What it does [Section titled “What it does”](#what-it-does) Swft Cart replaces the WooCommerce mini-cart with a slide-in drawer. When a customer adds a product to cart, the drawer opens automatically (or on click of the cart icon, depending on configuration). The drawer is powered by 18 opt-in modules. Each module is a self-contained UI component that can be enabled or disabled independently. Modules are loaded only when enabled — no dead weight in production. ## Installation [Section titled “Installation”](#installation) 1. Download `swft-cart.zip` from [swft.co.uk/cart](https://swft.co.uk/cart) 2. Install via **Plugins → Add New → Upload Plugin** 3. Activate 4. Go to **WooCommerce → Swft Cart** to configure modules and theme No API key required for Swft Cart. ## Integration with Swft Checkout [Section titled “Integration with Swft Checkout”](#integration-with-swft-checkout) When both plugins are active, the **Checkout** button in the Swft Cart drawer redirects to the Swft Checkout flow. The `swftcart:checkout` event fires before the redirect — you can cancel it with `e.preventDefault()` if needed. ## Public API [Section titled “Public API”](#public-api) The cart exposes `window.SwftCart` for programmatic control. See [Public API](/swft-cart/public-api) for the full reference. ## Extension hooks [Section titled “Extension hooks”](#extension-hooks) Swft Cart provides PHP filters for customising every aspect of the cart data before it is sent to the frontend: * `swftcart_cart_data` — full cart object * `swftcart_cart_item` — individual item * `swftcart_modules` — module configuration * `swftcart_i18n` — all UI strings * `swftcart_announcements` — announcement bar content * `swftcart_theme_vars` — CSS custom property overrides * `swftcart_delivery_data` — delivery countdown data * `swftcart_upsell_ids` — product IDs to show in the upsell module See [PHP Filters](/extensions/php-filters) for full signatures and examples. # Modules Swft Cart ships with 18 modules. Each is opt-in — disabled by default unless noted. Enable modules in **WooCommerce → Swft Cart → Modules**. ## Module reference [Section titled “Module reference”](#module-reference) | # | Module | Option name | Default | Description | | -- | -------------------------------- | ------------------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | **Announcement Bar** | `swftcart_module_announcements` | off | A dismissible banner at the top of the cart drawer. Supports plain text, links, and icons. Content controlled via `swftcart_announcements` filter. | | 2 | **Free Shipping Progress** | `swftcart_module_shipping_progress` | off | A progress bar showing how far the customer is from free shipping. Threshold pulled from WooCommerce Free Shipping settings. | | 3 | **Upsells** | `swftcart_module_upsells` | off | Horizontal scrollable strip of product recommendations shown below cart items. IDs set via `swftcart_upsell_ids` filter. | | 4 | **Cross-sells** | `swftcart_module_crosssells` | off | WooCommerce cross-sell products for the items in cart, shown as cards. | | 5 | **Save for Later** | `swftcart_module_save_for_later` | off | Per-item “Save for later” button. Saved items move to a separate list below the cart. | | 6 | **Order Notes** | `swftcart_module_order_notes` | off | A text field in the cart drawer that writes to `order_comments` when the order is created. | | 7 | **Coupon Field** | `swftcart_module_coupon` | off | An inline coupon code input. Applies the coupon via `wc_add_to_cart` and refreshes totals. Fires `swftcart:coupon-applied` and `swftcart:coupon-removed`. | | 8 | **Delivery Countdown** | `swftcart_module_delivery_countdown` | off | A countdown timer showing time remaining to order for same-day or next-day dispatch. Data supplied via `swftcart_delivery_data` filter. | | 9 | **Share Cart** | `swftcart_module_share_cart` | off | Generates a shareable URL encoding the current cart contents. Fires `swftcart:cart-shared` on copy. | | 10 | **Sticky Checkout Button** | `swftcart_module_sticky_checkout` | off | Pins the checkout button to the bottom of the drawer when the item list is taller than the drawer. | | 11 | **Empty Cart CTA** | `swftcart_module_empty_cta` | off | Custom content shown when the cart is empty (text, button, image). Configured in WP Admin. | | 12 | **Item Images** | `swftcart_module_item_images` | on | Product thumbnail per line item. Uses `woocommerce_thumbnail` image size. Disable for a compact list-only layout. | | 13 | **Quantity Stepper** | `swftcart_module_quantity_stepper` | on | +/- quantity buttons per item. Without this module, quantity is shown as a read-only number. | | 14 | **Remove Item** | `swftcart_module_remove_item` | on | Per-item remove button. Fires `swftcart:item-removed`. | | 15 | **Cart Total** | `swftcart_module_cart_total` | on | Subtotal, discount, and total rows above the checkout button. | | 16 | **Trust Badges** | `swftcart_module_trust_badges` | off | A row of configurable trust icons (lock, returns, secure payment) below the checkout button. | | 17 | **Recently Viewed** | `swftcart_module_recently_viewed` | off | Products the customer viewed in the current session, shown below cart items. | | 18 | **FAB (Floating Action Button)** | `swftcart_module_fab` | on | The floating cart icon shown on all pages. Click opens the drawer. Fires `swftcart:fab-clicked`. Badge shows item count. | ## Enabling modules via PHP [Section titled “Enabling modules via PHP”](#enabling-modules-via-php) You can force-enable or force-disable modules programmatically using the `swftcart_modules` filter: ```php add_filter( 'swftcart_modules', function( array $modules ): array { $modules['swftcart_module_upsells'] = true; $modules['swftcart_module_delivery_countdown'] = true; $modules['swftcart_module_sticky_checkout'] = false; // force-disable return $modules; } ); ``` The filter receives the current module state array (option name => bool) and must return the same structure. # Public API — window.SwftCart `window.SwftCart` is available on all pages where the Swft Cart plugin is active. It is initialised synchronously on DOM ready — no event to wait for. ## Methods [Section titled “Methods”](#methods) | Method | Signature | Description | | --------- | -------------------------------------------------------- | -------------------------------------------------------------------------- | | `open` | `() => void` | Open the cart drawer | | `close` | `() => void` | Close the cart drawer | | `refresh` | `() => Promise` | Reload cart data from WooCommerce and re-render | | `toast` | `(msg: string, label?: string, fn?: () => void) => void` | Show a toast notification. Optional `label` and `fn` add an action button. | | `on` | `(event: string, fn: (detail: unknown) => void) => void` | Subscribe to a `swftcart:*` event | | `off` | `(event: string, fn: (detail: unknown) => void) => void` | Unsubscribe from a `swftcart:*` event | ## Properties [Section titled “Properties”](#properties) | Property | Type | Description | | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `config` | `object` | Read-only PHP configuration object. Contains all enabled module flags, theme variables, and i18n strings as output by the PHP plugin. | ## Examples [Section titled “Examples”](#examples) ### Open the cart from any button [Section titled “Open the cart from any button”](#open-the-cart-from-any-button) ```js document.querySelectorAll('[data-open-cart]').forEach(btn => { btn.addEventListener('click', () => SwftCart.open()) }) ``` ### Show a toast when a product is saved for later [Section titled “Show a toast when a product is saved for later”](#show-a-toast-when-a-product-is-saved-for-later) ```js SwftCart.on('item-saved-for-later', (detail) => { SwftCart.toast('Saved for later', 'View saved items', () => { // scroll to saved items section }) }) ``` ### Refresh cart after a custom AJAX action [Section titled “Refresh cart after a custom AJAX action”](#refresh-cart-after-a-custom-ajax-action) ```js fetch('/wp-admin/admin-ajax.php', { method: 'POST', body: new URLSearchParams({ action: 'my_custom_action' }), }).then(() => SwftCart.refresh()) ``` ### Block checkout until a condition is met [Section titled “Block checkout until a condition is met”](#block-checkout-until-a-condition-is-met) ```js SwftCart.on('checkout', (e) => { if (!document.getElementById('terms-accepted').checked) { e.preventDefault() SwftCart.toast('Please accept the terms and conditions before continuing.') } }) ``` Note: the `checkout` event (fired as `swftcart:checkout`) is cancelable. Call `e.preventDefault()` to stop the redirect to checkout. ### Read config [Section titled “Read config”](#read-config) ```js console.log(SwftCart.config.modules) // { swftcart_module_upsells: true, ... } console.log(SwftCart.config.theme) // 'dark' console.log(SwftCart.config.currency) // 'GBP' ``` # Themes Swft Cart ships with 12 preset themes and a custom theme system built on CSS custom properties. Select a theme in **WooCommerce → Swft Cart → Appearance**. ## Preset themes [Section titled “Preset themes”](#preset-themes) ### 1. Dark (default) [Section titled “1. Dark (default)”](#1-dark-default) The signature Swft dark theme. Matches the Swft Checkout aesthetic. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#060608` | | `--swftcart-surface` | `#0e0e10` | | `--swftcart-text-1` | `#e8e8e8` | | `--swftcart-text-2` | `#b0b0b0` | | `--swftcart-accent` | `#00ffd1` | | `--swftcart-border` | `#1e1e1e` | | `--swftcart-btn-text` | `#000000` | ### 2. Light [Section titled “2. Light”](#2-light) Clean white theme for light-mode stores. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#ffffff` | | `--swftcart-surface` | `#f8f8fa` | | `--swftcart-text-1` | `#0a0a0a` | | `--swftcart-text-2` | `#404040` | | `--swftcart-accent` | `#0a0a0a` | | `--swftcart-border` | `#e0e0e0` | | `--swftcart-btn-text` | `#ffffff` | ### 3. Slate [Section titled “3. Slate”](#3-slate) Dark slate with indigo accent. Good for fashion and lifestyle brands. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#0f172a` | | `--swftcart-surface` | `#1e293b` | | `--swftcart-text-1` | `#f1f5f9` | | `--swftcart-text-2` | `#94a3b8` | | `--swftcart-accent` | `#6366f1` | | `--swftcart-border` | `#334155` | | `--swftcart-btn-text` | `#ffffff` | ### 4. Forest [Section titled “4. Forest”](#4-forest) Dark green, earthy tones. Good for food, wellness, and outdoor brands. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#0a1a0f` | | `--swftcart-surface` | `#122318` | | `--swftcart-text-1` | `#e2f0e8` | | `--swftcart-text-2` | `#8fad98` | | `--swftcart-accent` | `#4ade80` | | `--swftcart-border` | `#1e3828` | | `--swftcart-btn-text` | `#0a1a0f` | ### 5. Rose [Section titled “5. Rose”](#5-rose) Light rose theme. Good for beauty, gifting, and lifestyle. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#fff1f2` | | `--swftcart-surface` | `#ffffff` | | `--swftcart-text-1` | `#1c0a0c` | | `--swftcart-text-2` | `#6b2430` | | `--swftcart-accent` | `#e11d48` | | `--swftcart-border` | `#fecdd3` | | `--swftcart-btn-text` | `#ffffff` | ### 6. Amber [Section titled “6. Amber”](#6-amber) Warm amber for food, coffee, and artisan brands. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#1c1408` | | `--swftcart-surface` | `#2a1f0e` | | `--swftcart-text-1` | `#fef3c7` | | `--swftcart-text-2` | `#d97706` | | `--swftcart-accent` | `#f59e0b` | | `--swftcart-border` | `#3d2e10` | | `--swftcart-btn-text` | `#1c1408` | ### 7. Ocean [Section titled “7. Ocean”](#7-ocean) Deep blue-teal for tech and SaaS-adjacent brands. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#071928` | | `--swftcart-surface` | `#0d2a40` | | `--swftcart-text-1` | `#e0f2fe` | | `--swftcart-text-2` | `#7dd3fc` | | `--swftcart-accent` | `#06b6d4` | | `--swftcart-border` | `#164e63` | | `--swftcart-btn-text` | `#071928` | ### 8. Mono [Section titled “8. Mono”](#8-mono) Pure monochrome. Works on any brand. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#111111` | | `--swftcart-surface` | `#1a1a1a` | | `--swftcart-text-1` | `#f5f5f5` | | `--swftcart-text-2` | `#a3a3a3` | | `--swftcart-accent` | `#f5f5f5` | | `--swftcart-border` | `#262626` | | `--swftcart-btn-text` | `#111111` | ## Swft brand themes [Section titled “Swft brand themes”](#swft-brand-themes) Four themes that match the Swft design system. These are the defaults for new installations. ### swft-paper (default for new installs) [Section titled “swft-paper (default for new installs)”](#swft-paper-default-for-new-installs) Warm off-white with lime accent. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#F5F1EA` | | `--swftcart-t1` | `#14110F` | | `--swftcart-accent` | `#D4EF3B` | | `--swftcart-btn-text` | `#0B0F03` | ### swft-technical [Section titled “swft-technical”](#swft-technical) Near-black with lime accent. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#0D0D0E` | | `--swftcart-t1` | `#F3F1EC` | | `--swftcart-accent` | `#D4EF3B` | | `--swftcart-btn-text` | `#0B0F03` | ### swft-warm [Section titled “swft-warm”](#swft-warm) Linen and sand tones with terracotta accent. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#F0E5D2` | | `--swftcart-t1` | `#2A1F15` | | `--swftcart-accent` | `#C4763E` | | `--swftcart-btn-text` | `#FDF5E6` | ### swft-electric [Section titled “swft-electric”](#swft-electric) Clean white with electric violet accent. | Variable | Value | | --------------------- | --------- | | `--swftcart-bg` | `#F7F6F2` | | `--swftcart-t1` | `#0A0A12` | | `--swftcart-accent` | `#7A4FD6` | | `--swftcart-btn-text` | `#F7F6F2` | ## Custom theme [Section titled “Custom theme”](#custom-theme) Select **Custom** in the theme picker to expose all CSS variable fields. You can also override variables programmatically via the `swftcart_theme_vars` filter: ```php add_filter( 'swftcart_theme_vars', function( array $vars ): array { $vars['--swftcart-accent'] = '#ff6b35'; $vars['--swftcart-btn-text'] = '#ffffff'; return $vars; } ); ``` The filter receives the full variable array for the selected base theme and must return the same structure. This runs after the theme picker selection, so you can use any preset as a base and override individual tokens. ## Full variable list [Section titled “Full variable list”](#full-variable-list) | Variable | Purpose | | --------------------- | ---------------------------------------- | | `--swftcart-bg` | Drawer background | | `--swftcart-surface` | Card and section backgrounds | | `--swftcart-text-1` | Primary text | | `--swftcart-text-2` | Secondary and muted text | | `--swftcart-accent` | Buttons, active states, links | | `--swftcart-btn-text` | Text colour on accent buttons | | `--swftcart-border` | Dividers and input borders | | `--swftcart-radius` | Border radius (default: `10px`) | | `--swftcart-font` | Font stack (default: inherit from theme) | | `--swftcart-shadow` | Drawer box shadow | | `--swftcart-width` | Drawer width (default: `420px`) | | `--swftcart-z-index` | Drawer z-index (default: `9999`) | # Swft Checkout Swft Checkout is the hosted edge-rendered checkout that replaces WooCommerce’s native checkout page. ## How it works [Section titled “How it works”](#how-it-works) The WordPress plugin intercepts the WooCommerce checkout URL via `template_redirect` (priority 1). When the cart is non-empty and Swft is enabled with a valid API key, the plugin looks up a pre-created session URL from a WordPress transient and redirects with `wp_safe_redirect()`. The checkout runs at `checkout.swft.co.uk/{sessionId}`. It is a React application rendered by a Cloudflare Worker. The Worker reads the session from Cloudflare KV and injects it as `window.__SWFT_SESSION__` into the HTML before it reaches the browser — the app boots with full data, no loading state. ## Checkout flow [Section titled “Checkout flow”](#checkout-flow) ### Step 1 — Details [Section titled “Step 1 — Details”](#step-1--details) * Express checkout (Apple Pay, Google Pay) rendered first * Email address * Shipping address (auto-filled for returning customers) * Shipping method (radio cards from WooCommerce rates) ### Step 2 — Payment [Section titled “Step 2 — Payment”](#step-2--payment) * Review strip summarising step 1 data with inline Edit links * Stripe Elements card input * Pay button showing exact total: `Pay £[amount]` ### Confirmation [Section titled “Confirmation”](#confirmation) * Order reference * Delivery address * Estimated delivery window * Interactive map (requires Google Maps API key) ## Templates [Section titled “Templates”](#templates) Three checkout layout templates are available. Select in **Settings → Swft Checkout → Checkout Layout**. See [Templates](/swft-checkout/templates) for details. ## Order sync [Section titled “Order sync”](#order-sync) After payment, the Swft API creates a native WooCommerce order via the WooCommerce REST API with: * `status: processing` * `payment_method: swft` * `payment_method_title: Card (Swft)` * All line items and shipping line * Billing and shipping address * `_swft_session_id` and `_swft_payment_intent` order meta * All extension data as `_swft_ext_{key}` meta fields This requires WooCommerce REST API credentials to be configured. See [Installation](/getting-started/installation#woocommerce-api-credentials-optional). ## Session lifecycle [Section titled “Session lifecycle”](#session-lifecycle) | Status | Meaning | | ------------ | ------------------------------------------------------ | | `pending` | Session created, customer has not reached checkout yet | | `processing` | Customer is on the checkout page | | `complete` | Payment succeeded, order created | | `expired` | Session TTL exceeded (28 minutes from creation) | Sessions expire 28 minutes from creation. The transient caching on the WordPress side matches this TTL. If a session expires while the customer is mid-checkout, they see “This checkout link has expired — please return to the store and try again.” # AI Checkout An LLM-powered chat assistant that lives inside Swft Checkout. Shoppers can ask product questions, get help with objections, and (in advanced modes) complete the whole checkout conversationally. ## What it does [Section titled “What it does”](#what-it-does) Three operating modes you can enable independently: | Mode | What it does | Approx cost | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | **Mode A** | Conversational checkout flow — chat collects the shopper’s email, address, and payment info instead of the standard form. Always on once AI Checkout is enabled. | Negligible (no LLM call per message) | | **Mode B** | Product question answering — when a shopper asks about a product, an LLM fetches relevant products and policy excerpts and crafts a reply. | \~$0.001 per question | | **Mode C** | Objection handling — when the shopper hesitates (cart abandonment indicators, second-guessing the price, asking “why this and not that?”), Mode C uses Claude Sonnet to craft a tailored objection-handler reply citing your policies. | \~$0.005 per reply | You’re billed monthly for LLM usage; set a **cost cap** and Mode B/C will auto-pause once you hit it. ## Where to configure it [Section titled “Where to configure it”](#where-to-configure-it) Swft Dashboard → **AI Checkout**. Four tabs: ### Settings [Section titled “Settings”](#settings) | Setting | Default | What it does | | --------------- | ---------------- | ---------------------------------------- | | **Mode A** | On | Always-on conversational flow. | | **Mode B** | Off | Product question answering. | | **Mode C** | Off | Objection handling (Sonnet). | | **Monthly cap** | 5000 cents (£40) | Hard cap on LLM cost per calendar month. | | **Alert email** | Empty | Email to ping at 80% of cap. | When the cap is hit, Modes B and C pause for the rest of the month. Mode A keeps running because it doesn’t burn LLM credits. ### Branding [Section titled “Branding”](#branding) Pick a tone preset that shapes the chat’s voice: * **Professional** — formal, concise, business-appropriate. * **Casual** — friendly, contraction-heavy, like a chat with a friend. * **Playful** — uses humour and emoji (where appropriate to your brand). * **Luxury** — slow, considered, premium language. There’s also a free-form “brand voice notes” textarea where you can describe your tone in your own words. The LLM uses this when crafting replies. ### Knowledge [Section titled “Knowledge”](#knowledge) Upload (or sync from your store) two corpora: * **Product catalog** — every product’s title, description, attributes, price. Used by Mode B for product Q\&A. * **Policy pages** — returns, shipping, FAQ. Used by Mode C for objection-handler citations. Re-sync after big catalog changes; otherwise daily auto-sync keeps things current. ### Transcripts [Section titled “Transcripts”](#transcripts) Every chat conversation is logged. Use this tab to: * Review what shoppers are actually asking (often surprising — feeds into your product copy). * Spot edge cases your knowledge base doesn’t cover. * Audit the assistant’s responses to make sure it’s representing your brand correctly. ## Customer experience [Section titled “Customer experience”](#customer-experience) The chat appears as a floating button on the checkout, or as inline cards on the product page if you’ve enabled the Cart entry point. Entry points: * **Floating chat button** on the checkout itself. * **Cart CTA** (“Got questions? Ask our assistant”). * **Product page CTA** (via Swft Cart’s product-page embed). * **Abandonment trigger** — opens proactively if the shopper hesitates on payment. * **Header link** if you enable the persistent header entry. A typical conversation might be: > Shopper: “Will this dress fit me? I’m UK 12.” Assistant: (Mode B) “Our size chart shows UK 12 = chest 88cm. The model in the photos is also a UK 12. The fabric has 5% stretch, so it sits comfortably. Want to add it to your cart?” Shopper: *adds to cart* Shopper: “How much is shipping?” Assistant: “Free over £40. Your order is £52 so you’re covered. Ready to check out?” ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) Sessions are stored in `ai_chat_sessions` with a state machine: `consent → addressing → addressing_objection → payment → confirming → complete`. Each user message advances the state via `applyInput()` in the state machine engine. Modes are dispatched via an adapter pattern (`getAdapterForMerchant`). Mode A uses a Noop adapter — no LLM call, just templated responses. Mode B uses OpenAI’s GPT-4o-mini for cost; Mode C uses Anthropic’s Claude Sonnet for quality on harder reasoning. Cost tracking is per-message — every LLM call writes `llm_cost_cents` to the AI chat session. A cron job sums spend per merchant per month and compares it to the cap; if exceeded, `cap_status` flips to `over` and Modes B/C are skipped until the next month. Knowledge base data is embedded with OpenAI embeddings (`pgvector` in Supabase) and retrieved by semantic search for each shopper question. ## Gotchas [Section titled “Gotchas”](#gotchas) * **LLM responses can be wrong.** Don’t run Modes B or C without reviewing transcripts regularly. Wrong product info or invented policies can mislead shoppers and create legal risk. * **The cap is a hard stop, not a soft limit.** Once hit, advanced features stop until the month rolls. Set the cap generously the first month while you learn your traffic. * **Mode A is opinionated.** It replaces the standard checkout form with a chat flow. Test with real shoppers before launching — conversational checkout converts well for some demographics, badly for others. * **Knowledge base must be kept fresh.** If you change your shipping policy, re-sync. Otherwise the assistant will quote your old policy. * **Cap reset is per calendar month.** A spike late in the month means a hard pause until the 1st. # Custom Domains By default, the Swft Checkout runs at `checkout.swft.co.uk/{sessionId}`. You can serve it from your own subdomain, e.g. `checkout.yourstore.com`. ## Setup [Section titled “Setup”](#setup) ### 1. Configure the domain in Swft [Section titled “1. Configure the domain in Swft”](#1-configure-the-domain-in-swft) 1. Go to **Settings → Swft Checkout** 2. Enter your desired subdomain in the **Custom Domain** field (e.g. `checkout.yourstore.com`) 3. Click **Save** — Swft provisions a Cloudflare Pages custom domain for your account ### 2. Add a CNAME record [Section titled “2. Add a CNAME record”](#2-add-a-cname-record) Add a CNAME record in your DNS provider pointing your subdomain to Swft’s edge: | Type | Name | Target | | ----- | ---------- | ------------------- | | CNAME | `checkout` | `custom.swft.co.uk` | DNS propagation typically takes a few minutes. If your domain is on Cloudflare, set the record to **DNS only** (grey cloud) initially until the SSL certificate is issued. ### 3. Verify [Section titled “3. Verify”](#3-verify) After DNS propagates, go to **Settings → Swft Checkout** and click **Check domain status**. When verified, the status changes to **Active**. ### 4. SSL [Section titled “4. SSL”](#4-ssl) SSL is provisioned automatically by Cloudflare. No action required. ## Domain status [Section titled “Domain status”](#domain-status) | Status | Meaning | | ------- | ----------------------------------------- | | Pending | Domain saved, awaiting DNS propagation | | Active | CNAME resolves and SSL is valid | | Failed | CNAME not found or SSL failed — check DNS | ## Removing a custom domain [Section titled “Removing a custom domain”](#removing-a-custom-domain) Click **Remove** in the Custom Domain field. The checkout reverts to `checkout.swft.co.uk`. The CNAME record in your DNS can be deleted after removing. ## Notes [Section titled “Notes”](#notes) * Custom domains must be subdomains (e.g. `checkout.yourstore.com`), not root domains * One custom domain per Swft account * Session URLs in transients and emails update automatically once the custom domain is active — no cache flush required # Dunning — Failed Payment Recovery When a subscription renewal fails, Dunning retries on a schedule you configure and lets you send the customer a signed, time-limited link to update their card without exposing any internal IDs. ## What it does [Section titled “What it does”](#what-it-does) Subscription renewals fail for many reasons — expired cards, insufficient funds, fraud-flag declines, lost cards. Dunning automates the recovery loop: 1. **Auto-retry** — the failed payment is retried after a configurable delay (defaults: 3 days, 5 days, 7 days, then cancel at 14 days). 2. **Customer notification** — the merchant can send the customer a “update your payment method” email with a signed link that lasts 72 hours. 3. **Tracking** — every failure is logged with status `pending → retrying → resolved | cancelled` so the merchant has a clear view of recovery health. ## Where to configure it [Section titled “Where to configure it”](#where-to-configure-it) Swft Dashboard → **Dunning Management**. | Setting | Default | Purpose | | ----------------------- | ------- | ------------------------------------------------------------------ | | **Enable dunning** | Off | Master toggle. | | **Retry 1 (days)** | 3 | Days after the first failure before the first retry. | | **Retry 2 (days)** | 5 | Days after retry 1. | | **Retry 3 (days)** | 7 | Days after retry 2. | | **Cancel after (days)** | 14 | Days after the first failure before the subscription is cancelled. | The retry schedule is cumulative — by default the customer’s card is retried at day 3, day 8, day 15, and then cancelled at day 17 if all three retries fail. ## Customer experience [Section titled “Customer experience”](#customer-experience) When you click **Send update link** in the Dunning page, the customer receives an email like: > Your recent payment failed. Update your payment method to keep your subscription active: \[Update payment method] The link points at `https://checkout.swft.co.uk/update-card?token=` and is valid for **72 hours**. The token is HMAC-signed and encodes the failure ID, expiry, and signature — no raw IDs are exposed to the customer or anyone who might inspect the URL. When the customer clicks through, they see a streamlined Swft Checkout that asks for new card details only (no shipping, no review). On success, the failure is marked **resolved** and the subscription resumes. ## Managing failures [Section titled “Managing failures”](#managing-failures) The Dunning page lists every recent failure with: * Customer email and order reference * Failed amount and currency * Retry attempt count and next retry timestamp * Current status (pending, retrying, resolved, cancelled) * Actions: **Send update link**, **Mark resolved manually**, **Cancel subscription** ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) Failed payments are stored in the `subscription_failures` table with the schema: `id`, `merchant_id`, `subscription_id`, `customer_email`, `amount_pence`, `currency`, `status`, `retry_count`, `next_retry_at`, `created_at`, `resolved_at`. Retries are scheduled by a cron job that walks the `subscription_failures` table for `status = 'pending'` rows whose `next_retry_at <= now()`, calls Stripe’s retry endpoint, and either advances the retry counter or marks the row `resolved` / `cancelled`. The update-card link is generated by the `POST /api/dunning/send-update-link/:id` endpoint, which builds a JWT-like token (`failure_id | expiry | hmac`) signed with a per-merchant secret. The receiving page (`/update-card`) verifies the signature before showing any failure detail. Recovery emails are sent via Resend (`RESEND_API_KEY` in the API env). Delivery is fire-and-forget — if a send fails, the merchant can re-send from the dashboard. ## Gotchas [Section titled “Gotchas”](#gotchas) * The 72-hour token window is hard. If a customer misses it, you must re-send the link from the dashboard. * Email deliverability depends on your Swft sending domain’s SPF/DKIM. Talk to support if recovery emails land in spam. * Manually marking a failure **resolved** doesn’t retry the payment — use that for cases where the customer paid via another channel. * Dunning only handles **subscription** payments. One-off failed checkouts aren’t retried; the shopper has to start over. # Fraud & Risk Rules Swft leverages Stripe Radar’s risk scoring on every card transaction. You set thresholds; Swft auto-blocks high-risk orders, flags elevated-risk orders for manual review, and surfaces every score on your Sessions and Fraud dashboard pages. ## What it does [Section titled “What it does”](#what-it-does) When a card payment is processed, Stripe Radar scores the transaction 0–100 (higher = riskier) and assigns a risk level (`normal`, `elevated`, `highest`). Optionally Radar also names the rule that triggered the flag (e.g. “IP is anonymous proxy”, “Card has been used by many emails”). You configure two thresholds: | Threshold | Effect | | ---------------- | ------------------------------------------------------------------------------------------- | | **Block score** | Orders scoring ≥ this fail payment with a generic decline. Default 75. | | **Review score** | Orders scoring ≥ this complete payment but are flagged amber in your dashboard. Default 50. | The shopper never sees that an order was flagged or blocked for fraud — blocked orders just see a standard “Your card was declined” message. ## Where to configure it [Section titled “Where to configure it”](#where-to-configure-it) Swft Dashboard → **Fraud & Risk**. | Setting | What it does | | ------------------------- | ------------------------------------------------------------------- | | **Enable fraud blocking** | Master toggle. When off, no orders are blocked regardless of score. | | **Block score** | 0–100. Orders ≥ this score are blocked. | | **Review score** | 0–100. Orders ≥ this score are flagged but completed. | Sensible starting points: **Block ≥ 75**, **Review ≥ 50**. Tighten the block score to 65 if you’re seeing chargebacks; loosen to 85 if legitimate customers are being declined. ## Dashboard view [Section titled “Dashboard view”](#dashboard-view) The Fraud page shows: * **KPIs** — blocked count, review count, fraud rate, total scored orders. * **Flagged orders** — every order with `radar.level = 'elevated'` or `'highest'`, with the score, rule name, customer, and amount. Click through to the session detail. * **Blocked orders** — every payment intent declined for risk. Helpful for spotting patterns (e.g. the same IP / card BIN repeatedly). ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) On `payment_intent.succeeded` and `payment_intent.payment_failed`, the Swft webhook fetches the underlying Stripe Charge (`stripe.charges.retrieve(charge_id)`) and reads `charge.outcome`: * `outcome.risk_score` → number 0–100 * `outcome.risk_level` → string `'normal' | 'elevated' | 'highest'` * `outcome.rule.predicate` → the Radar rule that triggered, if any The result is written to `session_data.radar` on the `checkout_sessions` row. The Fraud page filters sessions by `radar.level` to populate its lists. For **blocked** orders, Radar declines the PaymentIntent at Stripe’s level before it confirms — Swft just records the declined attempt and surfaces it in the Blocked list. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Radar runs after payment is attempted, not before.** Blocking happens at confirm-time, so the shopper sees a declined message. You can’t pre-filter carts based on risk. * **Rule names are Stripe-internal codes.** When Radar shows “Rule X triggered”, consult the Stripe Radar dashboard for the human-readable name of that rule. Custom rules you create in Stripe will appear by their custom name. * **Historical orders have no radar data.** Orders placed before you enabled the integration show `—` in the dashboard. There’s no backfill. * **Radar only applies to card payments.** PayPal, Klyme (Open Banking), and other gateways have their own fraud screens — Swft surfaces nothing for those. * **Your Stripe Radar plan matters.** Stripe’s free Radar layer is rule-based and rough. Radar for Fraud Teams ($0.07/transaction) is the full ML-driven scoring and what most of this page assumes. ## Tuning advice [Section titled “Tuning advice”](#tuning-advice) 1. Start with the defaults (block 75, review 50). 2. Watch the Fraud page for two weeks. 3. If you’re getting chargebacks despite the scores being low, your Stripe Radar plan may be too basic — upgrade. 4. If your review queue is too noisy, raise the review threshold to 60 or 65. 5. If legitimate customers are getting blocked, raise the block threshold to 80 or 85. If you have a high-risk vertical (digital goods, downloads, high AOV), consider running an external fraud screen (Signifyd, Sift) on top of Radar — Swft will preserve the Radar data either way. # Gift Cards Issue gift card codes with fixed balances. Shoppers redeem them at checkout for partial or full order coverage. Codes can have expiry dates, custom strings, and full lifecycle tracking. ## What it does [Section titled “What it does”](#what-it-does) Each gift card has: * A **balance** (in pence) that decrements as it’s used. * A **code** — either auto-generated or a custom string you specify. * An **optional expiry** — date after which the card can no longer be redeemed. * A **status** (active, redeemed, expired, deactivated). Shoppers enter a code on the Swft Checkout payment step. If valid, the balance is applied as a discount. Partial coverage is allowed — if the gift card balance is less than the cart total, the shopper pays the difference with their normal payment method. ## Where to configure it [Section titled “Where to configure it”](#where-to-configure-it) Swft Dashboard → **Gift Cards**. ## Creating a gift card [Section titled “Creating a gift card”](#creating-a-gift-card) Click **Create gift card**. Fill in: | Field | Notes | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Amount** | £1 – £10,000. Stored in pence internally. | | **Code** | Leave blank to auto-generate a random 16-character code. Or specify your own (alphanumeric + hyphens, max 50 chars; must be unique within your store). | | **Expiry date** | Optional. Leave blank for no expiry. | | **Note** | Internal-only note (e.g. “Customer service compensation for order #1234”). | Click **Create**. The code is displayed once for you to send to the recipient — make sure to copy it. ## Managing gift cards [Section titled “Managing gift cards”](#managing-gift-cards) The Gift Cards page lists every card with: * Code (partially masked unless you click to reveal) * Original balance and remaining balance * Status (active, redeemed, expired, deactivated) * Created date and creator * Redeemed by (customer email) and redeemed at (timestamp) Per-card actions: * **Reveal** — show the full code. * **Deactivate** — invalidate immediately (e.g. if it’s been compromised). * **Adjust balance** — manual increase or decrease (creates a ledger entry). ## Customer experience [Section titled “Customer experience”](#customer-experience) On the Swft Checkout payment step, the shopper sees a **Have a gift card?** input. They paste the code and click **Apply**. * If the code is valid and has balance: the discount is applied immediately; the order total updates; if the balance covers the whole order, the payment method selector is hidden. * If the code is invalid, expired, or zero-balance: a friendly error appears (“Sorry, this gift card isn’t valid”). The shopper can apply only **one** gift card per order. ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) Gift cards are stored in the `gift_cards` table: | Column | Type | | ------------------------ | ------------------------------- | | `id` | UUID | | `merchant_id` | UUID | | `code` | TEXT, unique per merchant | | `balance_pence` | INTEGER | | `original_balance_pence` | INTEGER | | `expires_at` | TIMESTAMPTZ, nullable | | `redeemed_by` | TEXT (customer email), nullable | | `redeemed_at` | TIMESTAMPTZ, nullable | | `status` | TEXT enum | The shopper-facing lookup is `GET /sessions/:id/apply-gift-card` (POST to apply). The lookup endpoint is rate-limited to 10/min per session to discourage brute-force code guessing. When applied, the gift card balance is recorded against the session in `session_data.cart.applied_gift_card`. On `payment_intent.succeeded`, the balance is decremented atomically and `redeemed_by` / `redeemed_at` are stamped if the full balance is consumed. Partial redemption leaves the card active with the remaining balance. ## Gotchas [Section titled “Gotchas”](#gotchas) * **One gift card per order.** Stacking isn’t supported. If a customer has £20 across two cards, they need to use them on separate orders. * **Refunds don’t restore balance.** If you refund an order paid for partly by gift card, the refunded amount goes to the customer’s original payment method (or your manual choice), not back to the gift card. To re-credit the card, manually adjust the balance in the dashboard. * **Brute-force resistance** is best-effort. Use long, random codes — auto-generated 16-character codes are fine. Short or guessable custom codes (`GIFT2024`, `WELCOME10`) are vulnerable. * **Expiry is enforced at apply-time.** A card that expires while sitting in a cart will fail on the next attempt to apply it. Cards that have already been applied to a session don’t expire mid-session. * **Gift card sales are accounting liabilities.** A £50 card sold today is £50 you owe to the bearer later. Reconcile carefully. # KYC & B2B Verification For B2B orders above a threshold you set, Swft can require company verification via Companies House (UK) and/or identity verification via Stripe Identity. Verification happens inline in the checkout — failure blocks the order. ## What it does [Section titled “What it does”](#what-it-does) Pick one or both verification methods: * **Companies House** — UK company-number lookup. Confirms the company exists, is active, matches the customer’s input, and meets your minimum incorporation-age rules. * **Stripe Identity** — biometric ID verification. The customer is redirected to Stripe to submit a government ID and a selfie; Stripe confirms the match. Verification is gated behind a **liability waiver** the first time you enable it. By acknowledging the consent gates, you’re accepting that you’re responsible for the verification decisions Swft surfaces. ## Where to configure it [Section titled “Where to configure it”](#where-to-configure-it) Swft Dashboard → **KYC & Compliance**. ### First-time consent [Section titled “First-time consent”](#first-time-consent) On first enable, you’re required to tick four checkboxes and type `I ACCEPT` to confirm you understand: 1. KYC checks are best-effort and not a guarantee of identity. 2. You retain responsibility for any orders you accept. 3. Companies House data can be out of date. 4. You’re collecting personal data — your privacy policy must reflect this. This is logged with timestamp and IP. After the first acknowledgement, the toggles below become editable. ### Settings [Section titled “Settings”](#settings) | Setting | Default | What it does | | ------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- | | **Require Companies House** | On | Run the UK company lookup on every B2B order above the threshold. | | **Require Stripe Identity** | Off | Add Stripe Identity (document + selfie) on top of Companies House. Expensive — Stripe Identity is \~$1.50 per session. | | **Minimum order value (pence)** | Empty | Only trigger KYC above this order total. Leave empty to require it on every B2B order. | ## Customer experience [Section titled “Customer experience”](#customer-experience) A B2B order above the threshold renders an extra step between **Details** and **Payment**: 1. **Company verification** — the shopper enters their company name. The plugin queries Companies House and shows the matched company (incorporation date, status, registered address). If it matches the input, the shopper confirms. 2. **Identity verification** (if enabled) — the shopper is redirected to Stripe’s hosted Identity flow. They take a photo of their ID document and a selfie. They return to checkout once Stripe completes verification. 3. **Payment** — only proceeds if all configured checks passed. If verification fails, the shopper sees a “Couldn’t verify, please try again” message. Repeated failures block the order entirely. ## Managing the audit log [Section titled “Managing the audit log”](#managing-the-audit-log) The KYC page has an **Audit log** tab listing every verification attempt: * Timestamp, customer email, IP * Action (`companies_house_lookup`, `stripe_identity_redirect`, `stripe_identity_result`) * Outcome (`verified`, `failed`, `abandoned`) * Metadata: matched company name, company status, age, name-match score, Stripe verification session ID Keep this for audit and compliance reporting — Swft retains the log for **2 years** by default. ## How it works under the hood [Section titled “How it works under the hood”](#how-it-works-under-the-hood) KYC settings are stored on the `merchants` table: `kyc_enabled`, `kyc_consent_at`, `kyc_consent_ip`, `kyc_require_companies_house`, `kyc_require_stripe_identity`, `kyc_min_order_pence`. The checkout’s KYCVerification component (`checkout/src/components/KYCVerification.tsx`) drives the flow. Companies House lookups call `GET /api/sessions/:id/companies-house?name=...` which proxies to the Companies House public API. Stripe Identity uses `POST /api/sessions/:id/kyc-identity` to create a verification session, then polls `GET /api/sessions/:id/kyc-identity-result/:vsid` until the result is final. Every action is appended to the `kyc_audit_log` table with `merchant_id`, `customer_email`, `actor_type` (system / customer), `action`, `outcome`, and JSON metadata. ## Gotchas [Section titled “Gotchas”](#gotchas) * **Stripe Identity costs money.** \~$1.50 per session, billed to your Stripe account. Don’t enable it for every order — use the minimum order value threshold. * **Customer can abandon.** If they close the Stripe Identity tab without completing, the verification is recorded as `abandoned` and they have to start over. * **Companies House can lag.** A company that just registered may not appear in the API for 24-48 hours. Plan around this if you’re onboarding very new businesses. * **Name matching is fuzzy.** Companies House might match “Acme Ltd” to “ACME LIMITED” — the audit log shows the match score so you can review. * **B2B mode must be enabled.** If your store doesn’t have B2B mode active, KYC won’t trigger because there are no B2B orders to gate. * **Privacy law applies.** Verification collects PII (name, ID document, selfie). Your privacy policy must disclose this and customers must consent. Swft doesn’t generate consent text for you — work with your legal team. # Plugin Settings The settings on this page are the **WordPress plugin** settings — what you configure in **Settings → Swft Checkout** in your WP admin. They cover the integration between your WordPress site and the Swft API. Two settings surfaces Most merchant-facing configuration (branding, payments, recovery emails, tracking pixels, fraud thresholds, custom fields) lives in your **Swft Dashboard** at [app.swft.co.uk](https://app.swft.co.uk), not in WordPress. See [Dashboard → Settings](/dashboard/settings) for that. ## Core [Section titled “Core”](#core) ### Enable Swft Checkout [Section titled “Enable Swft Checkout”](#enable-swft-checkout) **Option:** `swft_enabled` (string: `'yes'` | `'no'`) Master toggle. When set to `yes` and an API key is present, Swft intercepts the WooCommerce checkout page. Set to `no` to disable without uninstalling — native WooCommerce checkout is used as the fallback. ### API Key [Section titled “API Key”](#api-key) **Option:** `swft_api_key` (string) Your Swft API key. Obtained from your Swft Dashboard. The plugin will not create sessions without a valid API key. ### Debug Logging [Section titled “Debug Logging”](#debug-logging) **Option:** `swft_debug` (string: `'yes'` | `'no'`) When enabled, every decision in the checkout redirect flow is logged to `swft_debug_log` (stored as a WordPress option). The last 200 entries are kept. Viewable in the admin panel under the **Dashboard** tab. Logged events include: `template_redirect` firing, `is_checkout()` result, plugin enabled status, cart contents count, session transient hits and misses, and the full redirect URL. Disable in production — each checkout page visit writes to the database when debug is on. ### WooCommerce REST API Credentials [Section titled “WooCommerce REST API Credentials”](#woocommerce-rest-api-credentials) **Options:** `swft_wc_consumer_key`, `swft_wc_consumer_secret` WooCommerce REST API key and secret with **Read/Write** permissions. Required for order sync back to WooCommerce after payment. Generate in **WooCommerce → Settings → Advanced → REST API**. After saving, Swft syncs these credentials to the API immediately. ### Stripe Connection [Section titled “Stripe Connection”](#stripe-connection) **Option:** `swft_stripe_connected` (bool) Read-only display of Stripe Connect status. Use the **Connect with Stripe** or **Disconnect** buttons to change this. See [Connecting Stripe](/getting-started/connecting-stripe). ### Custom Domain [Section titled “Custom Domain”](#custom-domain) **Option:** `swft_custom_domain` (string) A custom domain for your checkout, e.g. `checkout.yourstore.com`. Requires DNS configuration. See [Custom Domains](/swft-checkout/custom-domains). ### Google Maps API Key [Section titled “Google Maps API Key”](#google-maps-api-key) **Option:** `swft_google_maps_key` (string) Enables address autocomplete (Google Places) on the details form and the interactive map on the confirmation screen. ### Checkout Template [Section titled “Checkout Template”](#checkout-template) **Option:** `swft_checkout_template` (string: `'minimal'` | `'split'` | `'express'`) Layout template for the checkout page. Default: `minimal`. See [Templates](/swft-checkout/templates). ### Store Policies [Section titled “Store Policies”](#store-policies) **Options:** `swft_policy_returns`, `swft_policy_shipping`, `swft_policy_privacy` URLs to your store’s returns, shipping, and privacy policy pages. When set, links appear in the policies bar at the bottom of the checkout. ### Fallback to WC checkout on failure [Section titled “Fallback to WC checkout on failure”](#fallback-to-wc-checkout-on-failure) **Option:** `swft_fallback_enabled` (string: `'yes'` | `'no'`) When set to `yes`, if the Swft API is unreachable (network failure, API outage, invalid API key), the shopper falls through to the standard WooCommerce checkout page instead of seeing a blank page or error. Strongly recommended in production. ## Server-side tracking pixels [Section titled “Server-side tracking pixels”](#server-side-tracking-pixels) ### Meta Pixel (CAPI) [Section titled “Meta Pixel (CAPI)”](#meta-pixel-capi) **Options:** `swft_meta_pixel_id`, `swft_meta_capi_token` Meta (Facebook) Conversions API. When set, `AddPaymentInfo` and `Purchase` events fire server-side on payment intent creation and on payment success respectively. ### GA4 [Section titled “GA4”](#ga4) **Options:** `swft_ga4_measurement_id`, `swft_ga4_api_secret` Google Analytics 4 Measurement Protocol. Same events fired as Meta above. ### TikTok [Section titled “TikTok”](#tiktok) **Options:** `swft_tiktok_pixel_id`, `swft_tiktok_access_token` TikTok Events API. Same events fired. All tracking events are fire-and-forget — they never block the response to the customer. ## Repeat customer recognition [Section titled “Repeat customer recognition”](#repeat-customer-recognition) **Option:** `swft_repeat_customer_window_days` (int, default `30`) When a returning shopper visits checkout within this window, Swft pre-fills their details from the previous session’s data (matched on email). Set to `0` to disable. ## Address validation provider [Section titled “Address validation provider”](#address-validation-provider) **Option:** `swft_address_provider` (string: `'google'` | `'smartystreets'` | `'none'`) Pick the address autocomplete provider: * `google` — uses Google Places. Best global coverage. Requires `swft_google_maps_key`. * `smartystreets` — uses SmartyStreets. Better for US-only stores. Requires a SmartyStreets API key. * `none` — disables autocomplete; shoppers type addresses manually. ## Tax automation [Section titled “Tax automation”](#tax-automation) **Option:** `swft_tax_provider` (string: `'wc'` | `'avalara'` | `'taxjar'`) How tax is calculated: * `wc` — use WooCommerce’s built-in tax rules (default). * `avalara` — call Avalara AvaTax for real-time tax. Requires Avalara API credentials. * `taxjar` — call TaxJar. Requires TaxJar API key. When using `avalara` or `taxjar`, WooCommerce’s local tax rules are ignored entirely. Test thoroughly before relying. ## Custom fields [Section titled “Custom fields”](#custom-fields) Custom fields you’ve defined in your Swft Dashboard appear automatically — there’s no WP-side option for them. See [Dashboard → Settings → Custom Fields](/dashboard/settings). ## Reading options in PHP [Section titled “Reading options in PHP”](#reading-options-in-php) All options are standard WordPress options: ```php $enabled = get_option( 'swft_enabled' ) === 'yes'; $api_key = get_option( 'swft_api_key', '' ); $template = get_option( 'swft_checkout_template', 'minimal' ); ``` Avoid hard-coding sensitive values — store API keys in env vars or a secrets manager and `get_option` only the toggle flags. ## What’s NOT here [Section titled “What’s NOT here”](#whats-not-here) Settings that live in your **Swft Dashboard** (not in WordPress): * Brand colour, logo, fonts → see [Checkout Editor](/dashboard/checkout-editor) * Webhook URL and secret → see [Webhooks Reference](/integrations/webhooks) * Recovery email content → see [Dashboard → Settings](/dashboard/settings) → Recovery emails * Fraud thresholds → see [Fraud Rules](/swft-checkout/fraud-rules) * Dunning retry schedule → see [Dunning](/swft-checkout/dunning) * KYC configuration → see [KYC](/swft-checkout/kyc) * Payment gateway credentials (Stripe, PayPal, Klyme, NomuPay) → see [Payment Gateways](/getting-started/payment-gateways) * Subscription plans, Offload tier, custom field definitions, A/B tests → all in [Dashboard](/dashboard/) # Templates Swft Checkout supports three layout templates. Select in **Settings → Swft Checkout → Checkout Layout**. The template is stored as `swft_checkout_template` and passed through the session to the checkout frontend as `merchant.checkoutTemplate`. ## minimal (default) [Section titled “minimal (default)”](#minimal-default) The standard two-panel layout. Form on the left, sticky order summary on the right. * Max width: `1020px`, centred * Grid: `1fr 360px`, `32px` gap * Padding: `48px` top, `32px` horizontal, `80px` bottom * On mobile: single column, order summary collapses to a toggleable strip Best for: most stores. Provides full context (order summary always visible) alongside the form. ## split [Section titled “split”](#split) A variant two-panel layout where the order summary panel has a distinct light background card (even in dark mode) to create a visual split between the form area and the order context. * Max width: `960px`, centred * Grid: `1fr 400px`, `40px` gap * Padding: `40px` top and sides * Order summary: sticky, `background: #f9fafb`, `border-radius: 12px`, `padding: 24px` * On mobile: single column, order summary renders below the form Best for: stores with complex order summaries that benefit from visual separation. ## express [Section titled “express”](#express) A streamlined single-column layout with an “express checkout available” banner at the top. The order summary does not render in a separate panel — only the form and the sticky order summary below the form. * No desktop order summary panel * Banner: `background: #fafafa`, centred, `12px` padding, uppercase text * Best for: impulse purchases, simple single-product checkouts ## Switching templates [Section titled “Switching templates”](#switching-templates) Changing the template in **Settings → Swft Checkout → Checkout Layout** and saving takes effect immediately for all subsequent sessions. Existing sessions use the template that was active when they were created. ## Template via PHP [Section titled “Template via PHP”](#template-via-php) You can override the template programmatically using the `swft_session_payload` filter: ```php add_filter( 'swft_session_payload', function( array $payload ): array { // Force express template for a specific product category if ( has_product_from_category( 'impulse-buys' ) ) { $payload['template'] = 'express'; } return $payload; } ); ``` The `template` key in the payload is passed to the checkout frontend as `merchant.checkoutTemplate`. Valid values: `'minimal'`, `'split'`, `'express'`.