Skip to content

Powered by Grav CMS + Helios Theme

Payment providers

Set up offline, Stripe, Polar, PayPal and Moneris payments, and monitor webhook deliveries.

How providers work

The base plugin ships the offline method. Every other provider is a separate plugin that registers itself at runtime. Every enabled, configured provider appears at checkout together, and the customer picks one.

This page covers four providers:

Offline Stripe Polar PayPal
Slug offline stripe polar paypal
Plugin built in kahunacart-stripe kahunacart-polar kahunacart-paypal
Flow complete on the spot hosted redirect hosted redirect hosted redirect
Hosted checkout — yes yes yes
Refunds via API — yes yes yes
Partial refunds — yes yes yes
Recurring billing — yes yes —
Merchant of record — with Managed Payments yes —
Catalog sync — — yes (products + orders) —
Agent payments (Google Pay through UCP) — yes, from 1.2.4 — —
Mode values — test / live sandbox / production sandbox / live
Webhook auth — HMAC signing secret Standard Webhooks secret Webhook ID + API call-out
Check connection — yes yes yes
Set up webhook — yes yes yes
Label configurable yes yes yes yes

The three add-on plugins covered here (Stripe, Polar and PayPal) are marked premium: true. Each requires Grav 2.0 or later, KahunaCart 1.2 or later (PayPal 1.1 or later), and PHP 8.3 with ext-curl. Stripe 1.2.8 and PayPal 1.1.5 and later need KahunaCart 1.4.10. None vendors a provider SDK.

Six one-time add-ons have their own sections further down: Moneris for Canadian merchants, Mollie for European and Swiss ones, Square for shops that already use a Square till, SumUp for shops that already use SumUp's card readers, and Paddle and Lemon Squeezy, which are merchants of record. Moneris and SumUp work differently from the four above: there is no webhook to set up.

The Subscriptions add-on reads the Recurring billing row. Stripe (Managed Payments included), Polar and the kahunacart-dummy test provider run their own billing calendar. Eligible providers without that capability, including offline and PayPal, use invoice mode: the store keeps the calendar and emails a pay link each period. Declare the Subscriptions capability to put your own provider in the first group.

Connect a provider

Every provider connects the same way, and none of it happens in the provider's dashboard beyond creating the key:

  1. Install the provider's plugin.
  2. Open KahunaCart → Setup → Payments, press Settings on the provider's card, paste the API key for the mode you are using, and save.
  3. Press Set up webhook on the card. The plugin registers https://your-site.com/shop/webhook/{slug} with the provider, subscribes it to exactly the events the driver acts on, and saves what comes back: a signing secret for Stripe and Polar, a webhook ID for PayPal. Press it again later and it adopts the endpoint that is already there instead of creating a duplicate.
  4. Press Check connection. It proves the key reaches the provider, names the account it belongs to, looks up the webhook endpoint at the provider, and goes green.

Press Check connection before the webhook secret is stored and it fails on purpose, with a line saying the provider is reachable but no webhook is set and to press Set up webhook. A store in that state takes money and never hears about it, so the check refuses to pass with a warning nobody reads.

What the card tells you

Under each provider's name, a line says how it stands without you pressing anything: Connected, Needs attention, Not connected or Not checked yet, when it was last checked, whether its webhook endpoint is registered, and when the last webhook arrived. A card whose last check is more than an hour old checks again by itself when the screen opens.

Needs attention means checkout works but the provider may not be telling your store about payments, refunds or disputes. The reason is written under the line, and each one is fixed by pressing Set up webhook:

  • there is no webhook endpoint at your store's address
  • the provider has switched the endpoint off, which Stripe and Polar do after a long run of failed deliveries
  • the endpoint is missing events the plugin handles, which happens to one set up before a plugin release added them (a Stripe endpoint from before disputes were reported lacks charge.dispute.created)
  • the last three or more webhooks from that provider failed, which usually means the signing secret no longer matches

Stripe, Polar, PayPal, Square, Paddle and Lemon Squeezy look their endpoint up. Mollie, SumUp and Moneris send your store's address with each payment, so for them the card shows when the last webhook arrived and warns about a run of failures.

Recent webhooks on the card lists the last ten webhooks from that provider: when each arrived, whether it succeeded, what it was, the order it was about, and the error if there was one.

Each card also has an Instructions tab in its settings for the rare account that cannot manage its own webhooks, where the endpoint has to be added by hand. Those steps are under each provider below.

The webhook URL

Every provider uses the same webhook URL pattern:

TXT
https://your-site.com{route}/webhook/{slug}

With the default /shop route, that gives /shop/webhook/stripe, /shop/webhook/polar and /shop/webhook/paypal.

The endpoint is public, and each provider verifies its own signature before anything touches order state. A request that fails verification gets a 400 and changes nothing. An unhandled event type gets a 200, so the provider stops retrying it.

Webhooks are authoritative. The customer returning to your site is advisory.

Important

Change the store route and every webhook URL changes with it. Press Set up webhook again on each provider, then delete the old endpoint in the provider's dashboard yourself — the button reports one at a previous address but never deletes it, since one account often serves both a staging and a live store.

The webhook delivery log

Every inbound delivery is recorded as one row, whatever became of it.

Read the delivery log

In the admin, press Recent webhooks on the provider's card under Setup → Payments.

From the CLI:

BASH
bin/plugin kahunacart webhooks
bin/plugin kahunacart webhooks --provider stripe
bin/plugin kahunacart webhooks --check                    # the Payments card's check, for every provider
bin/plugin kahunacart webhooks --check --provider stripe

--check changes nothing at the provider and exits 1 when any provider fails or needs attention, so a deploy script can stop on it.

Over the API:

TXT
GET /kahunacart/webhooks?provider=stripe&page=1&per_page=25
GET /kahunacart/webhooks/summary

Both need the kahunacart.settings permission, not kahunacart.reports — a row can carry customer email addresses and provider references.

What is stored

Column Always Notes
provider, received_at, status_code yes What was addressed, when, and what this store answered.
action when there was one The provider's translated event. Null if nothing could be translated.
reference, order_id when resolvable Resolved from the event, never guessed.
error on failure The exception class and message, or the class of refusal.
payload_bytes yes How many bytes arrived.
payload_excerpt verified deliveries only The first 2 KB of the raw body.

Note

The excerpt is kept only for a verified delivery. A refused one keeps the slug, the byte count and the class of failure, and nothing of the body.

Retention

Deliveries are deleted after payments.webhook_log_days days. The default is 30; 0 keeps everything.

Failure streaks are reported, never acted on

The CLI and /kahunacart/webhooks/summary both show a current failure streak per provider: how many deliveries in a row have been refused or failed since that provider's last good one.

Nothing is disabled on the strength of it. A rotated webhook secret is the usual cause, and auto-disabling would turn that into a silent outage.

Important

Check the streak yourself. bin/plugin kahunacart webhooks prints it and names the two settings that are almost always at fault.

One provider, several checkout choices

A provider is an integration. One provider can carry several payment methods at checkout, each with its own label, logo, instructions, fee and conditions. That is how "Visa" and "Klarna" become two choices over one Stripe account, and how offline becomes Cash, Cheque and Bank transfer with three sets of account details.

Methods are optional. A store that defines none offers its providers directly, one radio button each, as the rest of this page describes. See Payment methods.

Deposits sit beside the provider, not inside it

Take part of an order up front and the rest later. Every provider on this page handles a deposit and a balance like any other charge — the provider contract did not change to allow it.

A payment collection records which part of an order a payment covers. It operates independently of the provider layer, reads the transaction ledger, and responds to two events: onKahunaCartPaymentCaptured settles whichever part was waiting, and onKahunaCartRefundApplied records a refund against the collection.

A part-paid order carries payment status partially_paid, whose counts_as_paid flag is false. A £1000 order with a £300 deposit adds nothing to revenue until the balance clears, so every report, fulfillment queue and add-on gets the right answer without knowing deposits exist.

See Deposits and part payment.

Offline

Offline is built into the base plugin and enabled by default. Use it for bank transfers, cheques and cash on delivery. It cannot refund, cannot redirect, and has no webhook.

The order is placed with payment status pending, and the customer sees your instructions on their receipt. Mark it paid in the admin when the money arrives. The sidebar badge counts exactly these orders.

Offline config keys

Key Default What it does
offline.enabled true Shows the offline method at checkout.
offline.label Bank Transfer The checkout label.
offline.instructions empty Markdown shown on the confirmation page.

Offline notes and limits

  • Set downloads.grant_on to completed if you sell digital goods this way. Otherwise the customer waits for the transfer to clear before receiving their files.
  • Refunds are recorded by hand. There is nothing to call.

Stripe

Stripe is the kahunacart-stripe plugin. Its settings are behind the Settings button on its card at KahunaCart → Setup → Payments, or in user/config/plugins/kahunacart-stripe.yaml.

The customer is redirected to Stripe's checkout page and returns while the confirmation arrives by webhook. Card details never touch your server.

Note

Stripe checkout is the hosted Checkout page. On-site Elements is not available.

What you need for Stripe

  • A Stripe account.
  • The secret key for the mode you are configuring (sk_test_… or sk_live_…), from Developers → API keys.

That is the whole list. Set up webhook creates the endpoint and stores its signing secret for you.

Set up Stripe

  1. Open KahunaCart → Setup → Payments, press Settings on the Stripe card, paste the secret key into the field for your mode, and save.
  2. Press Set up webhook on the card. It registers https://your-site.com/shop/webhook/stripe on your Stripe account, subscribes it to the events the driver acts on, leaves it on your account's default API version, and writes the endpoint's whsec_… secret into the webhook field for that mode.
  3. Press Check connection. It names the Stripe account, its country and default currency, and goes green.

Stripe now appears at checkout as Credit / Debit Card (Stripe). Signatures are HMAC-SHA256 over the raw body with a 300-second tolerance, compared in constant time. Test and live are separate endpoints with separate secrets, so do steps 1 to 3 once per mode.

Register the Stripe webhook by hand

If your Stripe user cannot manage webhooks, add the endpoint yourself. In Stripe, go to Developers → Webhooks, add https://your-site.com/shop/webhook/stripe, and subscribe it to at least: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, payment_intent.payment_failed, refund.created, refund.updated, refund.failed, charge.refunded, and charge.dispute.created if you want disputes written on the order and sent to you. Copy the endpoint's signing secret into the webhook field for the matching mode and save. The same steps are on the Instructions tab of the Stripe settings.

Important

Subscribe to refund.created, refund.updated and refund.failed, or a refund that settles asynchronously stays pending in the ledger forever. A webhook endpoint carries its own Stripe API version, independent of your keys, and only endpoints on 2024-10-28.acacia or later receive these three events. Set up webhook leaves the endpoint on your account's default version, which is newer than that on any account opened since; a hand-made endpoint needs the version chosen in the Stripe dialog.

Stripe disputes

When a customer disputes a charge, Stripe sends charge.dispute.created. The store finds the order by the PaymentIntent the dispute names, writes Dispute opened at Stripe on the order's history with the amount, Stripe's reason and the date your evidence is due, and sends the Dispute opened message to the channels you ticked under Notifications. Affiliates holds the partner's commission on it. The order itself is not changed, because a dispute is not a refund and you may well win it.

When Stripe settles it, charge.dispute.closed says how. The store writes Dispute won at Stripe or Dispute lost at Stripe on the order, the order's dispute badge follows, and you get the Dispute won or lost message. An inquiry that closed without becoming a chargeback counts as won.

Each dispute is reported once, however many times Stripe delivers it. A dispute on a charge that is not one of this store's orders, which happens when the Stripe account is shared with something else, is logged and left alone. A dispute that arrives a moment before the store has recorded the payment is asked for again rather than lost.

Important

A store whose Stripe endpoint was set up before disputes were reported has to press Set up webhook once more. It adds charge.dispute.created and charge.dispute.closed to the endpoint you already have and changes nothing else on it. Until then Stripe does not send disputes to the store at all.

To see one in test mode, press Set up webhook first, then pay with the test card 4000 0000 0000 0259, which Stripe disputes as fraudulent a moment after the charge.

Stripe config keys

Key Default What it does
enabled true Turns the provider on.
mode test test or live. Selects the key pair.
statement_label empty Short label on the card statement. Empty uses the Stripe account default.
test_secret_key empty sk_test_…
test_webhook_secret empty whsec_…
live_secret_key empty sk_live_…
live_webhook_secret empty whsec_…
label empty Checkout label. Empty uses the default.
test_publishable_key, live_publishable_key empty pk_… keys, used only by the express-pay button to ask the browser for Apple Pay, Google Pay or Link. No key, no button.
capture_mode automatic manual places a hold and you press Capture or Void on the order. Only cards can be held, so manual capture gives up iDEAL, Bancontact and the bank debits.
save_payment_methods false Asks Stripe to keep the customer's payment method so it can be charged again from their account page. Customers with an account only.
billing_portal_configuration empty The Stripe customer portal configuration id. Stripe keeps one per mode, so set it up in test and in live.
managed_payments false Sends checkouts through Stripe Managed Payments. Needs an account approved for it and digital products only.
managed_payments_tax_code txcd_10000000 Product tax code sent with each Managed Payments checkout. Must be one of Stripe's digital-goods codes.

Stripe registers only when enabled is on and the active mode's secret key is non-empty. Without the webhook secret, every incoming webhook is rejected.

Test Stripe payments

Keep mode set to test and pay with a Stripe test card such as 4242 4242 4242 4242, any future expiry and CVC. For webhooks on a local site, use the Stripe CLI:

BASH
stripe listen --forward-to https://your-site.test/shop/webhook/stripe
stripe trigger checkout.session.completed

stripe listen prints its own whsec_…. While forwarding, paste that one into the test webhook field in place of the one Set up webhook stored, and press Set up webhook again when you are done to get the real one back.

Agent payments with Google Pay

Stripe 1.2.4 with KahunaCart 1.3.0 lets an agent platform such as Google AI Mode or Gemini complete a UCP checkout itself. The buyer picks a card in Google Pay on the platform's side, and Stripe charges the token the platform hands the store. No redirect, no browser.

  • It needs a publishable key and Managed Payments off. With both, the driver declares the agent_payments capability and the store advertises a Google Pay handler with Stripe's gateway parameters. Under Managed Payments no handler is advertised, and agents hand the buyer to your own checkout instead.
  • A card that asks for a 3-D Secure challenge fails, with a sentence the agent can pass on. There is nobody to answer a challenge in an agent checkout.
  • capture_mode: manual holds the money exactly as a two-step checkout does, with the same Capture and Void buttons.
  • A retried completion charges once. Both Stripe calls carry an idempotency key made from the order and the key the platform sent.
  • The PaymentIntent carries kahunacart_source: agent beside the usual order and transaction hashes, so you can pick agent orders out in the Stripe dashboard.
  • The token is never stored. It goes to Stripe in the one call that needs it.

To try it, keep mode: test and ucp.google_pay.environment: TEST. Google Pay's test environment with the stripe gateway returns Stripe test-mode tokens, so the whole flow runs with no real card. See UCP checkout → Google Pay with Stripe.

Stripe card readers

With the Point of Sale add-on, KahunaCart 1.5.7 and Stripe 1.2.10 or later, a counter sale can be paid on a Stripe reader. The customer taps or inserts a card on the reader, and the payment lands on the order like any other Stripe payment. Card details never reach your store.

  • What's supported. The BBPOS WisePOS E and the Stripe Reader S700 (or S710), ordered from Terminal in the Stripe dashboard. They connect over Wi-Fi or Ethernet and are driven from your store, so nothing is installed on the till. Bluetooth and USB readers (the M2, WisePad 3 and Chipper 2X) and Tap to Pay need an app, so they aren't offered.
  • When readers are offered. The mode's secret key is set, Managed Payments is off, the payment method uses your own Stripe account, and there is a Terminal location or a store address to make one from. Fill in Store Address under Invoices in KahunaCart's settings. Check connection says which of these is missing and never creates anything.
  • Pairing. On the reader, open its settings and choose Generate pairing code. In KahunaCart → Payments, press Card readers on the Stripe card, then Pair a reader, and type the three words it shows with a name such as Front counter (or use POST /kahunacart/providers/stripe/readers, the pair_provider_reader tool). The first reader you pair makes a Stripe Terminal location from your store address and saves its id in Test Terminal Location or Live Terminal Location. Forgetting a reader deletes it from your Stripe account. See Registers and devices to put a paired reader on a register.
  • The webhook. Press Set up webhook once more on a store set up before card readers. It adds the two reader events and changes nothing else. Without them the till still settles every payment by asking Stripe.
  • What the till shows. Waiting on the reader, paid, declined with Stripe's reason, cancelled on the reader, or timed out. If Stripe's answer never comes back, the till shows the payment as checking and keeps asking. Staff can check again and, if nothing was taken, give up on it. A card is never charged twice. See Taking payment.
  • Cancel and refunds. Cancel at the till clears the reader. A refund goes back through the ordinary refund, from the order.
  • Test mode. Pair a simulated reader with the code simulated-wpe (WisePOS E) or simulated-s700. A simulated reader has no screen, so a payment on it waits until a card is presented with Stripe's present_payment_method test helper. Amounts ending in .05 are declined.
  • Not supported yet. Interac debit in Canada, and holding a reader payment to capture later. Reader payments are always captured at once, whatever capture_mode says for online checkout.

Stripe notes and limits

  • The Stripe Checkout session is a single line item named "Order payment" for the whole order total, not an itemized cart. Your KahunaCart order is the itemized record.
  • Stripe tells the store what it paid out. On a store that sells in more than one currency, KahunaCart asks Stripe once per payment, on the job worker, what it converted the charge at, and records it on the order. The driver reads the charge's balance transaction: exchange_rate, and net, what reached your balance in the payout currency after Stripe's fee. A €94.00 charge reads "Paid out as $100.90 at 1.1202" on the order screen. A charge in the account's own currency has no exchange rate and records nothing, and a charge whose money hasn't reached the balance yet is asked about again later. It reads with the key of the account the payment was taken on. A subscription's first period and its renewals are followed through their invoice to the payment behind it. An invoice paid in several payments records nothing, rather than one payment's payout as the whole invoice's. See What the payment provider paid out.
  • Full details are in the kahunacart-stripe plugin README.

Polar

Polar is the kahunacart-polar plugin. Its settings are behind the Settings button on its card at KahunaCart → Setup → Payments, or in user/config/plugins/kahunacart-polar.yaml.

Polar is a merchant of record with two-way catalog sync. The customer is redirected to Polar's hosted checkout.

What you need for Polar

  • A Polar organization. Sandbox and production are separate accounts at sandbox.polar.sh and polar.sh, with their own organizations, products and tokens. Nothing carries over between them.
  • An organization access token from Settings → Developers in the Polar dashboard, with the scopes checkouts:write, products:read, products:write, refunds:write and organizations:read, plus webhooks:read and webhooks:write so the plugin can register its own webhook. Selling subscriptions through Polar also needs subscriptions:write and customer_sessions:write.

That is the whole list. The organization id and the carrier product are discovered from the token.

Set up Polar

  1. Open KahunaCart → Setup → Payments and press Settings on the Polar card. On the General tab, set Polar Environment to Sandbox while you test and Production when you are ready to take real money.
  2. Paste the access token into Sandbox Access Token or Production Access Token, whichever matches, and save. Leave Organization ID and Checkout Product ID empty: the plugin asks Polar which organization the token belongs to, and on the first sale it creates a private product called "KahunaCart Order" and charges every cart against it.
  3. Back on the card, press Set up webhook. The plugin registers https://your-site.com/shop/webhook/polar with Polar, subscribes it to every event the driver acts on, and writes the signing secret into the matching field. Pressing it again adopts the endpoint that is already there rather than adding a second one.
  4. Press Check connection. It reports which environment it reached, which organization Polar resolved and which product carts will be charged against. Pressed before step 3 it fails on purpose and tells you to set up the webhook.
  5. Set the tax behavior on both sides, as described under Tax with Polar.
  6. In sandbox, place an order with the test card 4242 4242 4242 4242, any future expiry and any CVC, and confirm the order moves to paid. Polar has to reach your webhook over the internet, so a local .test site needs an HTTPS tunnel in front of it.

Polar now appears at checkout. Signatures follow the Standard Webhooks spec (webhook-id, webhook-timestamp, webhook-signature) with a ±300-second window.

Register the Polar webhook by hand

If the token lacks the webhook scopes, add the URL above in Polar under Settings → Webhooks with the Raw payload format, subscribe it to order.paid, order.updated, order.refunded, refund.created, refund.updated, product.created, product.updated and the six subscription.* events, then paste the endpoint's signing secret into the webhook secret field for the matching environment.

Tax with Polar

Polar is the merchant of record, so it works out the customer's tax, adds it at checkout and pays it to the tax authority. Two settings have to agree for that to add up:

  1. On the KahunaCart side, leave the flat tax rate at 0 and create no tax zones for a store that sells only through Polar. KahunaCart hands Polar the pre-tax order total, and charging your own tax as well would charge the customer twice. See Tax → Merchant of record.
  2. On the Polar side, open your organization's settings, find Default tax behavior under Payments, and choose Exclusive. That tells Polar the amount it receives is the price before tax and the customer's tax goes on top. KahunaCart records Polar's net amount as what was paid and measures refunds against it, so the two match only when tax is exclusive.

Do not pick Inclusive. Polar would treat the order total as already containing tax, a European order would net you the total less VAT, and the store would record a shortfall against an order it priced in full. Location-based behaves the same way for customers in countries where inclusive pricing is the custom, which passes a test from the United States and fails on the first order from Germany.

Polar config keys

Key Default What it does
enabled true Turns the provider on.
mode sandbox sandbox (talks to sandbox-api.polar.sh) or production.
label empty Checkout label. Empty uses Polar (cards, global tax handled).
allow_discount_codes false Lets customers enter a Polar-side discount code. Off by default: a Polar discount changes the amount charged after KahunaCart has priced the order.
sandbox_access_token empty Organization access token.
sandbox_webhook_secret empty Endpoint signing secret.
sandbox_organization_id empty UUID. Leave empty; the plugin asks Polar which organization the token belongs to, and only a token that can reach several needs this.
sandbox_checkout_product_id empty UUID of the carrier product. Empty means the plugin finds or creates it.
production_access_token empty As above, for production.
production_webhook_secret empty As above, for production.
production_organization_id empty As above, for production.
production_checkout_product_id empty As above, for production.

Polar registers only when enabled is on and the active mode's access token is non-empty. Mode resolves to production only on the exact string production; anything else is sandbox.

Note

The slug polar is fixed. It is the webhook route segment and the key sync mappings are stored under, so changing it would orphan every mapping and every registered endpoint.

Test Polar payments

Keep mode set to sandbox and use a sandbox organization with its own token. Pay with a Stripe test card. Point a sandbox webhook endpoint at your local site through an HTTPS tunnel, then paste its secret into the sandbox webhook field.

Tax with Polar as merchant of record

Polar sells to your customer on your behalf and remits sales tax, VAT and GST itself, so the amount KahunaCart hands it is the pre-tax total. You configure nothing: Polar declares the MerchantOfRecord capability, and KahunaCart recalculates the order without local tax first. A store selling through both Polar and Stripe taxes the Stripe path normally and the Polar path not at all.

Two things to expect:

  • The cart page still shows the local tax estimate. Suppression happens when the customer submits checkout with Polar selected.
  • Inclusive prices still contain the tax you built into them. Price net for Polar.

See Tax → Merchant-of-record providers.

The Polar carrier product

Polar's checkout API always references a catalog product, so the plugin sends one product, the carrier, at an ad-hoc price equal to the order total. Leave {mode}_checkout_product_id empty and the plugin finds or creates a private product called KahunaCart Order with a placeholder $1.00 price that is never charged.

Note

Polar's order records show the carrier product rather than the items bought. Your KahunaCart order remains the itemized record. See Catalog sync for the details.

When Polar stops serving the API version

KahunaCart asks Polar for one dated version of its API, and Polar retires old versions each quarter. If Polar ever stops serving the version your plugin asks for, calls fall back to whichever version is current, so your store keeps selling while you are told to update the plugin. You hear about it in three places: the Grav log, a line at the top of the store admin, and your notification channels.

On KahunaCart 1.5.3 or later the third is the Polar API version problem row under Notifications → Store owner. The background worker sends one message when the line starts showing and a "Cleared" one when it stops, so it can arrive up to six hours after the problem first shows. The same message covers Polar not answering and a webhook endpoint that could not be moved. Warn me when the API version pin stops working on the Polar settings screen switches the line and the message off. An older KahunaCart sends one message the first time the fallback fires instead.

Polar refunds

order.refunded carries running totals, not per-refund amounts, so a second partial refund arrives with the sum of both. The webhook reports that cumulative figure as such, with its net and tax components, and Polar refunds the tax it collected alongside the net. The ledger dedupes on provider, reference and action. A refund can come back pending and settle later.

Polar catalog sync

See Catalog sync for the default-variant limitation and the rest. Full details are in the kahunacart-polar plugin README.

PayPal

PayPal is the kahunacart-paypal plugin. Its settings are behind the Settings button on its card at KahunaCart → Setup → Payments, or in user/config/plugins/kahunacart-paypal.yaml.

The customer is redirected to PayPal's approval page, and the order is captured and completed from the webhook.

What you need for PayPal

  • An account at developer.paypal.com.
  • A Merchant-type app in the matching environment, giving a client ID and secret.

That is the whole list. Set up webhook writes the webhook ID for you.

Set up PayPal

  1. Sign in at developer.paypal.com and switch to Sandbox or Live to match your mode.
  2. Go to Apps & Credentials → Create App and choose type Merchant.
  3. Open KahunaCart → Setup → Payments, press Settings on the PayPal card, copy the Client ID and Secret into the matching fields, and save.
  4. Back on the card, press Set up webhook.
  5. Press Check connection.

Step 4 registers https://your-site.com/shop/webhook/paypal on your app, subscribes it to the twelve events the driver acts on, and saves the webhook ID for the current mode. Step 5 proves the credentials reach PayPal, names the app they belong to, and reads the webhook ID back. It also says so if the app was not granted webhook management, in which case register the webhook by hand as below. A webhook registered before PayPal disputes were reported still passes, and the card says Needs attention until Set up webhook is pressed once more to add CUSTOMER.DISPUTE.CREATED.

PayPal now appears at checkout as PayPal. Sandbox and live webhooks are separate objects with separate IDs, so do this once per mode.

Pressing Set up webhook twice adopts the webhook already registered for that URL instead of creating a second one, adds any missing events, and leaves your own subscriptions alone. Reset webhook deletes the one at this exact URL and registers a fresh one — use it after moving the site. A webhook on this store's route at a different address is reported, never deleted, since one REST app often serves both a staging and a live store.

Register the PayPal webhook by hand

If your app is not allowed to manage webhooks, add the endpoint yourself (the same steps are on the Instructions tab of the PayPal settings): open the app, scroll to Webhooks, click Add Webhook, and paste https://your-site.com/shop/webhook/paypal. Subscribe to these eleven event types:

Event What it does
CHECKOUT.ORDER.APPROVED Triggers the capture call
CHECKOUT.ORDER.DECLINED Fails an order declined on an alternative payment method
PAYMENT.AUTHORIZATION.CREATED Records a hold placed by a manual-capture checkout
PAYMENT.AUTHORIZATION.VOIDED Records a hold released or expired
PAYMENT.CAPTURE.COMPLETED Completes the order
PAYMENT.CAPTURE.DENIED Marks the payment failed
PAYMENT.CAPTURE.DECLINED Marks the payment failed
PAYMENT.CAPTURE.REVERSED Marks the payment failed after PayPal pulls a capture back
PAYMENT.CAPTURE.REFUNDED Records a refund in the ledger
PAYMENT.REFUND.FAILED Releases a refund that did not go through
CUSTOMER.DISPUTE.CREATED Writes a dispute on the order and tells you about it
CUSTOMER.DISPUTE.RESOLVED Writes how the dispute ended on the order and tells you

Save, then copy the generated Webhook ID, which looks like 1JE43649LP004314D, into the webhook field for the matching mode.

Tip

CHECKOUT.ORDER.DECLINED is missing from PayPal's published event-name list, but PayPal offers it to a Merchant app and it fires on alternative payment methods such as iDEAL and BLIK. The button asks PayPal which events your app is offered and skips the rest, so one unavailable name never fails the registration.

PayPal disputes

When a customer opens a dispute against a payment, PayPal sends CUSTOMER.DISPUTE.CREATED. The store finds the order by the capture the dispute names, writes Dispute opened at PayPal on the order's history with the amount, PayPal's reason and the date you must answer by, and sends the Dispute opened message to the channels you ticked under Notifications. Affiliates holds the partner's commission on it. The order itself is not changed, and each dispute is reported once however often PayPal delivers it. A dispute on a payment that is not one of this store's orders, which happens when the REST app is shared with something else, is logged and left alone.

When PayPal settles it, CUSTOMER.DISPUTE.RESOLVED says how. You winning, the customer withdrawing, or PayPal paying out of its own protection is won; the customer winning is lost. The store writes it on the order, the order's dispute badge follows, and you get the Dispute won or lost message.

Important

A webhook registered before disputes were reported does not have the events. Press Set up webhook once more on the PayPal card: it adds CUSTOMER.DISPUTE.CREATED and CUSTOMER.DISPUTE.RESOLVED and changes nothing else on that webhook. Until you do, the card says Needs attention and names the missing events.

To see one in the sandbox, pay with a sandbox buyer account, then sign in to the sandbox site as that buyer, open the Resolution Center and report a problem with the payment. PayPal sends the event within a few minutes.

PayPal config keys

Key Default What it does
enabled true Turns the provider on.
mode sandbox sandbox or live.
label empty Checkout label. Empty uses the default.
capture_mode automatic manual places a hold on approval and you press Capture or Void on the order. PayPal honours a hold for three days and can reauthorise it for up to twenty-nine.
brand_name empty Store name on PayPal's approval page. Empty uses the account business name.
shipping_preference GET_FROM_FILE GET_FROM_FILE (use the buyer's PayPal address), NO_SHIPPING (digital-only stores), or SET_PROVIDED_ADDRESS.
sandbox_client_id empty Client ID for sandbox.
sandbox_client_secret empty Client secret for sandbox.
sandbox_webhook_id empty The webhook ID for sandbox. Written by Set up webhook, and stored encrypted — with it, a delivery can be forged past verification.
live_client_id empty Client ID for live.
live_client_secret empty Client secret for live.
live_webhook_id empty The webhook ID for live. Same again, for the live app.

PayPal registers only when enabled is on and both the active mode's client ID and secret are set.

Tip

If you sell digital goods only, set shipping_preference to NO_SHIPPING so the buyer is never asked for an address.

Test PayPal payments

Keep mode set to sandbox, create a sandbox buyer account under Testing Tools → Sandbox Accounts, and pay with it. PayPal has no CLI tunnel, so a webhook has to reach a public URL. Two options:

  • Simulate. Testing Tools → Webhooks Simulator posts a sample event to your endpoint. Simulated events carry valid transmission headers but do not verify against a real webhook ID, so use them to check routing, not to complete an order.
  • Tunnel. Expose your local site through an HTTPS tunnel and register that URL as a sandbox webhook. This gives the full flow, capture and completion included.

How PayPal verification works

PayPal signs webhooks with a certificate chain, and the only server-side check it supports is asking PayPal itself, so every accepted webhook costs one API call.

A request is rejected without calling out when it misses any of the five Paypal-Transmission-* / Paypal-Auth-Algo / Paypal-Cert-Url headers, carries a cert URL that is not on a PayPal host, or arrives while no webhook ID is configured. Anything PayPal does not answer SUCCESS gets a 400.

PayPal notes and limits

  • Capture happens from the webhook. CHECKOUT.ORDER.APPROVED triggers the capture call, and the PAYMENT.CAPTURE.COMPLETED event that follows completes the order. This works whether or not the customer's browser returns.
  • A failed capture is answered with a 200, so it is not logged as a signature failure.
  • The PayPal order is one purchase unit with the total amount, not itemized.
  • PayPal-Request-Id idempotency means retrying a payment for the same pending ledger row returns the same PayPal order. A redelivered capture returning ORDER_ALREADY_CAPTURED counts as success.
  • HUF and TWD have an exponent of 2 in ISO 4217, but PayPal transacts both in whole units and rejects decimals. The plugin follows PayPal.
  • Credential failures always report the generic PayPal rejected the API credentials. Other errors append PayPal's debug_id.
  • Check connection fails, rather than warns, when no webhook ID is set or PayPal does not recognise the one that is. In that state checkout works, the money moves, and nothing settles — usually a sandbox webhook ID left in the live fields.
  • PayPal allows ten webhooks per app. Set up webhook says so plainly when you hit the limit.
  • PayPal tells the store what it paid out. On a store that sells in more than one currency, KahunaCart asks PayPal once per capture, on the job worker, what it converted the payment at, and records it on the order. The driver reads the capture's seller_receivable_breakdown: exchange_rate.value, and receivable_amount, what PayPal credited to your account after its fee. PayPal only adds those two when it converted the money, so a capture in the account's own currency records nothing. A capture still pending is asked about again later, and a payment held first and captured afterwards is followed from its authorization to the capture. See What the payment provider paid out.
  • Full details are in the kahunacart-paypal plugin README.

Moneris

Moneris is the kahunacart-moneris plugin, a one-time add-on sold in the shop. Moneris is Canada's largest card processor, and the plugin is for merchants who already have a Moneris account. Its settings are behind the Settings button on its card at KahunaCart → Setup → Payments, or in user/config/plugins/kahunacart-moneris.yaml.

Moneris has no redirect checkout any more. Its hosted card form, Moneris Checkout, has to sit inside a page of the merchant's own, so the plugin serves that page on your store at /shop/moneris/pay/.... The shopper picks Moneris at checkout, lands on that page, types their card into Moneris's own frame, and comes back to the normal receipt. Card details never reach your server.

What you need for Moneris

  • A Moneris account, with your store ID and API token from the Merchant Resource Center (Admin → Store Settings).
  • A Moneris Checkout profile. Create it under Admin → Moneris Checkout Config and pick I have my custom order form and want to use Moneris simply for payment processing, since KahunaCart already has the order form and the receipt. Its ID starts with chkt.
  • The KahunaCart worker running, which it does when Grav's scheduler is in your crontab.

Set up Moneris

  1. Install the plugin with bin/gpm install kahunacart-moneris, or from Admin → Plugins.
  2. Open KahunaCart → Setup → Payments and press Settings on the Moneris card.
  3. Paste the store ID, API token and checkout ID, and turn Test mode off for a live account.
  4. Pick the currency your store ID settles in. Nearly every Moneris account is CAD, the default. Choose USD only if Moneris set your store up for US dollars.
  5. Save, then press Check connection. It asks Moneris for a real checkout ticket for 1.00, the same call every payment starts with, so a pass means a payment page can be opened.

There is no Set up webhook step. Moneris Checkout has no webhooks, so the store asks Moneris for the receipt itself, server to server, before it marks an order paid.

Test Moneris payments

Moneris runs a shared test environment at esqa.moneris.com/mpg. Sign in as DemoUser with the password password, use store ID store2 and API token yesguy, and create a Moneris Checkout profile of your own there. Other developers use the same test stores, so leave the rest of their settings alone.

  • With 3-D Secure on (the test profiles have it), pay with Visa 4000 0000 0000 2701 or Mastercard 5200 0000 0000 2235.
  • In test mode the cents decide the answer. Totals from 11.00 to 50.99 are approved, and a Visa total ending in .05 is declined.

Moneris notes and limits

  • The store never trusts the browser. Every payment is confirmed with Moneris before the order is marked paid, and a payment Moneris did not approve settles nothing.
  • A shopper who pays and closes the tab is still confirmed, by the worker, usually within a minute or two. The worker keeps asking for 24 hours, because an expired ticket is not proof that nobody paid it.
  • If the shopper pressed Pay and Moneris still has not answered after 24 hours, the order gets a notice on the Notices queue and a line in its history with the Moneris order number to look up. A shopper who opened the card form and left without paying is closed quietly.
  • Refunds, full or partial, go from the order in the admin through the Moneris Gateway with the same store ID and API token. They are done or refused on the spot, never pending.
  • Moneris is offered at checkout only for orders in the currency you picked.
  • One-time payments and refunds only. Subscriptions, saved cards, Apple Pay and Google Pay are not supported.
  • Full details are in the kahunacart-moneris plugin README.

Mollie

Mollie is the kahunacart-mollie plugin, a one-time add-on sold in the shop. Mollie is a European payment service for businesses registered in the EU, the UK and Switzerland, and it brings the local methods shoppers there expect: cards, TWINT, iDEAL, Bancontact, Pay by Bank and more, whichever you have switched on in your Mollie account. Its settings are behind the Settings button on its card at KahunaCart → Setup → Payments, or in user/config/plugins/kahunacart-mollie.yaml.

The shopper picks Mollie at checkout, pays on Mollie's own page, and comes back to the normal receipt. Card and bank details never reach your server.

What you need for Mollie

  • A Mollie account with a website profile and at least one payment method switched on.
  • Your API key from Developers → API keys in the Mollie Dashboard. A key starting test_ works in test mode and one starting live_ takes real money, so there is no separate test switch.
  • A store that Mollie can reach from the internet. Mollie will not call a localhost or .test address.

Set up Mollie

  1. Install the plugin with bin/gpm install kahunacart-mollie, or from Admin → Plugins.
  2. Open KahunaCart → Setup → Payments and press Settings on the Mollie card.
  3. Paste the API key and save.
  4. Press Check connection. It says whether the key is a test or live key, which website profile it belongs to, and which methods Mollie will offer in each currency your store sells in, for example CHF: TWINT, Card. It fails if nothing on your profile takes your store's base currency, so you find out before a shopper does.

There is no Set up webhook step. Mollie takes the webhook address with each payment, and the plugin sends your store's own address every time.

Test Mollie payments

With a test_ key, Mollie's page lets you pick a method and then choose how the payment ends: paid, failed, canceled or expired. The matching webhook reaches your store just as a real one would. Card payments in test mode use Mollie's test cards.

Mollie notes and limits

  • Every payment is confirmed by asking Mollie about it before anything changes. Mollie signs nothing, so a webhook is only ever a prompt to look.
  • TWINT takes Swiss francs only, so it appears on CHF orders. Which other currencies work depends on the methods you have switched on, and Check connection lists them.
  • Klarna, Billie, Riverty and in3 are not offered yet. Mollie needs the order's lines and billing address for them, and this release does not send those. Check connection names them as not offered.
  • A refund stays pending until Mollie says the money has gone back. Mollie can hold a refund until your Mollie balance covers it, so this can take a while. Refunds made in the Mollie Dashboard are recorded too, and so are chargebacks, which add a notice to the order and are also told to you as disputes (see Mollie chargebacks).
  • Capture mode set to manual holds card payments until you press Capture or Void on the order. Methods that cannot be held, such as TWINT and Pay by Bank, are taken at once and recorded as ordinary sales.
  • You are told when the rest of a hold is not released. After a partial capture the store asks Mollie to let go of the rest of the hold. When Mollie refuses, or the store cannot read the payment back to check, Part of a Mollie hold was not released goes to your channels with the order number, what may still be held and a link to the order, once per payment. It matters most when nobody is watching the capture. If Mollie refuses, the capture still stands and you can release the remainder in the Mollie Dashboard.
  • Full details are in the kahunacart-mollie plugin README.

Mollie chargebacks

Mollie names no chargeback in its webhook, only the payment, so the store asks Mollie for the payment with its chargebacks and reports each one it has not reported before. It finds the order by the payment id, writes Dispute opened at Mollie on the order's history with the amount, and sends the Dispute opened message to the channels you ticked under Notifications. Affiliates holds the partner's commission on it. The order itself is not changed, and the payment is still recorded as a reversal as before.

Mollie gives a reason only for SEPA Direct Debit chargebacks (the bank's code, such as AC01) and no response deadline, so the reason is usually blank and no date is given. A chargeback Mollie has reversed is not reported. Every later notification on the same payment sees the same chargebacks and reports none of them again. There is nothing to register for this: it arrives through the same payment webhook.

To see one in test mode, pay with a method that can be charged back (SEPA Direct Debit or a card), then open the paid test payment's changePaymentState link from the API response (_links.changePaymentState). Mollie's hosted page there lets you create a refund or a chargeback on a paid test payment.

Square

Square is the kahunacart-square plugin, a one-time add-on sold in the shop. It suits a business that already takes cards in person with a Square reader or till: online orders go into the same Square account. Its settings are behind the Settings button on its card at KahunaCart → Setup → Payments, or in user/config/plugins/kahunacart-square.yaml.

The shopper picks Square at checkout, pays on Square's own hosted page, and comes back to the normal receipt. Card details never reach your server.

What you need for Square

  • A Square account and an application in the Square Developer Dashboard.
  • The application's Access token from its Credentials page, and the Location ID of the location that takes the payments, from its Locations page. Sandbox and production each have their own token and locations.
  • A store that Square can reach from the internet. Square will not call a localhost or .test address.

Set up Square

  1. Install the plugin with bin/gpm install kahunacart-square, or from Admin → Plugins.
  2. Open KahunaCart → Setup → Payments and press Settings on the Square card.
  3. Leave Sandbox Mode on to test first, paste the access token and the location ID, and save.
  4. Press Check connection. It names the location, its country and the one currency it takes. Enter that currency as Location Currency so checkout only offers Square on orders in it.
  5. Press Set up webhook. It registers a webhook subscription for your store with Square and saves its signature key. A subscription you already made by hand for the same address is adopted rather than duplicated, so pressing it again is safe.

When you go live, turn Sandbox Mode off, paste the production token and location ID, and press Set up webhook again, since production has its own subscription and key.

Register the Square webhook by hand

If Square refuses the button (an OAuth token issued for another seller cannot manage subscriptions), add it in the Developer Dashboard under Webhooks → Subscriptions: use your store's webhook URL (see The webhook URL), subscribe to payment.created, payment.updated, refund.created and refund.updated, and copy its Signature key into Webhook Signature Key. Square signs the URL together with the body, so if you register any address other than the store's own, paste exactly the same text into Notification URL.

Test Square payments

In sandbox, Square's payment page is a testing panel rather than the real checkout: press Next, then Test Payment, and Square pays the order and sends the webhooks just as a real payment would. The panel has no way to make a payment fail, and wallets such as Apple Pay are not available there.

Square card readers

With the Point of Sale add-on, KahunaCart 1.5.7 and Square 1.1.7 or later, a Square Terminal on the same Square account takes card payments at the counter. The amount goes from the till to the Terminal, the customer taps, dips or swipes, and the payment lands on the order like any other Square payment, refunds included.

  • What's supported. The Square Terminal. Use the access token from your application's own Credentials page. It needs the DEVICE_CREDENTIAL_MANAGEMENT, PAYMENTS_WRITE and PAYMENTS_READ permissions, and an OAuth token needs them granted.
  • Pairing goes the other way round. With Stripe and SumUp the reader shows a code you give to the store. With Square the store asks Square for a code and you type it into the Terminal. In KahunaCart → Payments, press Card readers on the Square card, then Pair a reader, give it a name and press Get a code (or use POST /kahunacart/providers/square/readers/pairing, the begin_reader_pairing tool). The store shows a six-letter code that works for five minutes. On the Terminal's sign-in screen choose Device code and type it in. The Terminal then appears in the readers list. See Registers and devices to put it on a register.
  • Removing a Terminal. Square has no way for another app to unpair one. On the Terminal, swipe in from the left, open Settings and choose Sign Out. It stops taking payments for your store, but stays on the list because Square keeps the pairing on record.
  • What the till shows. Waiting for the card, paid, cancelled or timed out. A declined card keeps the payment waiting, because the customer can try another card on the same payment. Tipping is off. A cancel is confirmed straight away if the Terminal hasn't shown the payment yet, and shows as cancelling until the Terminal confirms if the customer is already paying.
  • When it says checking. Square cancels a payment nobody paid after five minutes, but a Terminal finishing at that very moment can still complete it. So the till doesn't call a payment timed out on the clock. It shows it as checking until the Terminal has moved on to the next payment, which usually takes seconds. If the order can't wait, staff can give up on the card payment and take it another way. If the Terminal did finish it, the money is recorded on the order, and you are told if the order was paid another way meanwhile so you can refund one of them.
  • Sent once. Every payment goes to the Terminal under your store's own key, and a payment sent again after a lost answer finds the one it already made, even days later. If Square can't answer, the payment is left as checking rather than guessed at. Look in the Square Dashboard, then give up on it at the till.
  • The webhook. Press Set up webhook once more on a store set up before card readers, to add terminal.checkout.updated. Without it the till refreshes every two seconds and still settles, only slower.
  • Test in sandbox. Square's sandbox has no Terminal to sign in, so the list shows Square's own test devices, each acting out one outcome: approves up to $25.00, the customer cancels, times out, or never picks up. The sandbox can't decline a card on a Terminal.
  • Not supported yet. Interac refunds on the Terminal, tipping on the Terminal, holding a payment to capture later, and refunds on a payment split across several cards on one checkout.

Square notes and limits

  • A location takes one currency, so Square is offered only for orders in it once Location Currency is set.
  • Payments are taken straight away. Square's hosted page cannot hold a payment for later capture, so there are no Capture or Void buttons for Square orders.
  • Refunds, full or partial, can be made from the order screen. A refund stays pending until Square confirms it. Refunds made in the Square Dashboard are recorded on the right order too.
  • A returning shopper keeps one customer profile in Square rather than getting a new one per order.
  • Full details are in the kahunacart-square plugin README.

Paddle

Paddle is the kahunacart-paddle plugin, a one-time add-on sold in the shop. Paddle is a merchant of record: it sells to the customer on your behalf, works out the sales tax or VAT for their country, adds it at checkout, and pays it to the tax authority. Its settings are behind the Settings button on its card at KahunaCart → Setup → Payments, or in user/config/plugins/kahunacart-paddle.yaml.

Paddle has no payment page of its own for websites. Its checkout opens over a page on your site, and the plugin serves that page itself, so the shopper picks Paddle, pays in Paddle's checkout without leaving your domain, and lands on the normal receipt. Card details never reach your server.

What you need for Paddle

  • A Paddle account. Test with a separate Paddle sandbox account first.
  • An API key from Developer tools → Authentication, with read and write on Transactions, Adjustments and Notification settings. Sandbox keys contain _sdbx_ and live keys _live_.
  • The client-side token from the same screen. It starts test_ in sandbox and live_ in live, and it is public by design.
  • Your domain approved under Checkout → Website approval in Paddle. Paddle will not open its checkout on any other domain. Sandbox approves it at once; live approval is reviewed by Paddle.
  • A store that Paddle can reach from the internet. Paddle will not call a localhost or .test address.

Set up Paddle

  1. Install the plugin with bin/gpm install kahunacart-paddle, or from Admin → Plugins.
  2. Open KahunaCart → Setup → Payments and press Settings on the Paddle card.
  3. Leave Sandbox Mode on to test first, paste the API key and the client-side token, and save.
  4. Press Check connection. It tests the key, catches a key or token for the wrong mode, and shows the payment page address: https://your-site.com/shop/paddle/pay.
  5. Press Set up webhook. It creates a notification destination for your store in Paddle and saves its secret key. A destination you already made for the same address is adopted rather than duplicated, and other destinations on your Paddle account are never touched.
  6. Set your own tax rate to 0 for a store that sells only through Paddle, so tax is not charged twice. See Tax → Merchant of record.

When you go live, turn Sandbox Mode off, paste the live key and token, approve your domain in the live account, and press Set up webhook again.

Test Paddle payments

In sandbox, pay with Paddle's test card 4242 4242 4242 4242, any future expiry and any CVC. The order completes when Paddle's transaction.completed webhook arrives, and the shopper is taken to the receipt. Refunds are reviewed by Paddle; in sandbox each was approved within a couple of minutes.

Paddle notes and limits

  • Paddle takes 33 currencies, and is only offered for orders in one of them.
  • Payments are taken straight away. Paddle has no way to hold a payment for later capture.
  • Refunds are made from the order screen in your store's own amounts, and Paddle gives back the matching share of the tax it charged, so a full refund returns everything the customer paid. Paddle reviews one refund at a time per order, so a second waits until the first is approved. Refunds made in Paddle's dashboard are recorded on the right order too.
  • Receipts show the tax Paddle charged beside your own total.
  • A store that already has its own Paddle.js page can leave the client-side token empty and keep using its default payment link.
  • Full details are in the kahunacart-paddle plugin README.

Lemon Squeezy

Lemon Squeezy is the kahunacart-lemonsqueezy plugin, a one-time add-on sold in the shop. Like Paddle, Lemon Squeezy is a merchant of record: it sells to the customer on your behalf, works out the sales tax or VAT for their country, adds it at checkout, and pays it to the tax authority. Its settings are behind the Settings button on its card at KahunaCart → Setup → Payments, or in user/config/plugins/kahunacart-lemonsqueezy.yaml.

The shopper picks Lemon Squeezy at checkout, pays on Lemon Squeezy's hosted page, and comes back to the receipt. Card details never reach your server.

What you need for Lemon Squeezy

  • A Lemon Squeezy account and store. Test in its Test mode first, which has its own API keys and products.
  • An API key from Settings → API, made in the mode you are setting up.
  • One placeholder "carrier" product, because a Lemon Squeezy checkout always names a product. Create it under Products → New Product: any name (customers never see it), a price above zero such as 1.00, no deliverables, and publish it. Every checkout charges your order total against it.
  • A store that Lemon Squeezy can reach from the internet. It will not call a localhost or .test address.

Set up Lemon Squeezy

  1. Install the plugin with bin/gpm install kahunacart-lemonsqueezy, or from Admin → Plugins.
  2. Open KahunaCart → Setup → Payments and press Settings on the Lemon Squeezy card.
  3. Turn Test Mode on to match a test-mode key, paste the API key, and save. Leave Store ID empty; the plugin finds your store from the key.
  4. Press Check connection. It names your store and its currency, and lists your products with their variant ids, such as KahunaCart Order → 2244614. Copy the carrier's id into Carrier Variant ID and save. Lemon Squeezy is not offered at checkout until it is set.
  5. Press Set up webhook. It registers a webhook for your store in Lemon Squeezy and saves a signing secret it generates.
  6. Set your own tax rate to 0 for a store that sells only through Lemon Squeezy, so tax is not charged twice. See Tax → Merchant of record.

When you go live, make a live key and a live carrier product (Lemon Squeezy can copy the test one to live mode), turn Test Mode off, enter the new variant id, and press Set up webhook again.

Test Lemon Squeezy payments

In test mode, pay with 4242 4242 4242 4242, any future expiry and any CVC. The order completes when the order_created webhook arrives. Test-mode receipt emails from Lemon Squeezy go to you and your team rather than to the shopper.

Lemon Squeezy notes and limits

  • A Lemon Squeezy store charges in one currency, so it is only offered for orders in that currency.
  • Payments are taken straight away; there is no way to hold one for later capture.
  • Refunds are made from the order screen in your store's own amounts and are confirmed at once. Lemon Squeezy gives back the matching share of the tax it charged, so a full refund returns everything the customer paid. Lemon Squeezy sends no message if it ever reverses a refund.
  • Lemon Squeezy's own records show the carrier product rather than the items bought; your KahunaCart order is the itemised record.
  • Full details are in the kahunacart-lemonsqueezy plugin README.

SumUp

SumUp is the kahunacart-sumup plugin, a one-time add-on sold in the shop. It suits shops that already take card payments in person with SumUp's readers and want the same account behind their online store. Its settings are behind the Settings button on its card at KahunaCart → Setup → Payments, or in user/config/plugins/kahunacart-sumup.yaml.

The shopper picks SumUp at checkout, pays on SumUp's hosted page, and comes back to the receipt. Card details never reach your server.

What you need for SumUp

  • A SumUp account with Hosted Checkout switched on. SumUp turns it on per account; a sandbox merchant has it.
  • An API key from Settings → For Developers → API Keys in the SumUp dashboard. It starts sup_sk_ and is shown once.
  • A store that SumUp can reach from the internet. It will not call a localhost or .test address.

Set up SumUp

  1. Install the plugin with bin/gpm install kahunacart-sumup, or from Admin → Plugins.
  2. Open KahunaCart → Setup → Payments and press Settings on the SumUp card.
  3. Paste the API key and save. Leave Merchant Code empty; the plugin uses the merchant the key belongs to.
  4. Press Check connection. It shows the merchant's name and code, its country and currency, and whether it is a sandbox.

There is no Set up webhook step. SumUp has no webhook settings at all: every checkout the store creates carries its own callback address, so there is nothing to register and nothing to redo when your address changes.

Test SumUp payments

A SumUp sandbox is a separate merchant account rather than a switch. Create one in the SumUp dashboard under Settings → For Developers → Sandboxes, make an API key while signed in to it, and paste that key. The hosted page shows Test mode at the top.

  • Pay with Visa 4200 0000 0000 0091, any future expiry and any CVV. Visa 4200 0000 0000 0042 asks for a 3-D Secure challenge.
  • An order totalling 11.00 is declined on purpose.

To go live, paste a key made on your live merchant.

SumUp card readers

With the Point of Sale add-on, KahunaCart 1.5.7 and SumUp 1.1.6 or later, a SumUp Solo takes card payments at the counter through SumUp's Cloud API. The Solo talks to SumUp over Wi-Fi or mobile data, your store sends it the amount and hears back how it went. Card details never reach your store.

  • What's supported. The SumUp Solo, the standalone reader with a touchscreen. It is the only reader SumUp's Cloud API pairs with. SumUp's other readers work through its apps and aren't offered. The Solo's software needs to be 3.3.24.3 or later to take Cloud API payments and 3.3.39.0 or later for the store to see what it's doing. Keep it plugged in while it is in use.
  • Pairing. On the Solo, log out if you are signed in (top menu, Settings, About, Log out). Then open the top menu, go to Connections, pick API and press Connect. It shows a code of eight or nine letters and digits, good for five minutes. In KahunaCart → Payments, press Card readers on the SumUp card, then Pair a reader, and type the code with a name (or use POST /kahunacart/providers/sumup/readers, the pair_provider_reader tool). The Solo shows a confirmation. Forgetting a reader in the store takes it off your SumUp account. On the Solo, Connections, API, Disconnect finishes the job. See Registers and devices to put it on a register.
  • The Affiliate Key. On a live SumUp account, SumUp requires an Affiliate Key on every Solo payment. It isn't a password. Create one in SumUp under Settings, For Developers, Toolkit, Affiliate Keys with an application identifier of your choice, such as com.yourshop.pos, and paste both into Affiliate Key and Affiliate Application ID on the API Credentials tab. The sandbox takes payments without one. Check connection says how many readers are paired and whether the key is set.
  • The webhook. SumUp calls your store's webhook address to say how a payment ended, so your store has to be reachable over https from the internet, as for online payments. The till's own check settles declined and cancelled payments, and settles everything if a call is slow.
  • What the till shows. Waiting, paid, declined with SumUp's reason, or cancelled on the Solo or from the till. A payment the Solo never took is shown as timed out about five minutes after it was sent. Cancelling from the till shows as cancelling until SumUp confirms it. A Solo that is offline can't be stopped from the store, so press the cancel button on the Solo itself.
  • A payment is sent to the Solo once. SumUp has no way to tell a repeat from a new payment, so your store never sends one twice, and only sends when the Solo is idle. If the answer to the send never comes back, the store looks for the payment in SumUp's history instead of sending it again.
  • When it says checking. If SumUp has no record of the payment, or ended it while the Solo is still busy or offline, the till shows it as checking and keeps asking. Look the payment up in the SumUp app or on the Solo. If it went through, it settles by itself when SumUp reports it. If nothing was taken, give up on it at the till and take the payment another way. A payment that turns up after you gave up on it is still recorded, and you are told if the order was already paid another way.
  • Test without a Solo. Use a sandbox merchant and SumUp's Virtual Solo at virtual-solo.sumup.com: press Get started, pick Cloud API, generate a pairing code and pair it as above. An amount of 11.00 is declined.
  • Not supported yet. Tips on the reader, instalments, card type selection (needed in Brazil), the Greek AADE signature, and SumUp Go terminals.

SumUp notes and limits

  • SumUp signs nothing it sends, so the store never trusts a callback by itself. It asks SumUp for the checkout before it marks an order paid, and ignores one SumUp has never heard of.
  • Refunds, full or partial, are made from the order screen and are normally confirmed at once. SumUp sends no message about refunds, so one it leaves pending does not update by itself.
  • Payments are taken straight away; SumUp cannot hold one for later capture.
  • SumUp takes sixteen currencies (BGN, BRL, CHF, CLP, COP, CZK, DKK, EUR, GBP, HRK, HUF, NOK, PLN, RON, SEK and USD), so it is only offered for orders in those.
  • Full details are in the kahunacart-sumup plugin README.

Currencies

A store that sells in more than one currency puts each order in exactly one of them, and a payment method that cannot take that currency is not offered for it. Each provider answers for itself: a driver with a list of currencies offers itself for those and for none other, and a driver that says nothing takes every currency, as every provider written for KahunaCart 1.3 does. A payment method you set up on top of a provider can narrow the currencies further, see Payment methods.

The shopper never sees a method that cannot take their order. When that leaves nothing at all, checkout and the pay page say "No payment method is available for orders in EUR." instead of drawing an empty payment step, and the store owner is told earlier than that:

  • bin/plugin kahunacart status prints, for each currency the store sells in (the base currency first), which payment methods take it, and a warning for any currency that nothing takes.
  • The Payments screen shows the same list above the method cards, with a red box for any currency nothing takes.

Both are only there on a store that sells in more than one currency. Store credit is left out of the answer, because it is offered only to a customer whose balance in that currency covers the order.

When a customer pays in a currency your payment account doesn't hold, the platform converts it at its own rate and pays out in your account's currency. Stripe and PayPal tell KahunaCart what they converted each charge at and paid out after their fee, and the order records it beside the store's own rate. A provider add-on can do the same. See What the payment provider paid out and Payout lookup.

When a platform reports that it took a payment or made a refund in a currency other than the order's, KahunaCart does not write the platform's figure into the order. The payment is recorded for the amount it was opened for, the figure and currency the platform reported are kept on the transaction and named in the order's history, and a notice goes to the store owner's queue. See the provider contract.

Catalogue sync pushes prices in the base currency and says so (currency on the product it hands the driver), and a price pulled in another currency is skipped rather than written as a base price. See Catalog sync.

Choose a provider

  • Widest card coverage → Stripe.
  • Digital goods worldwide without VAT/GST registration → Polar, which handles tax as merchant of record.
  • Customers who expect a PayPal button → PayPal, at the cost of an extra API call per webhook.
  • A Canadian merchant with a Moneris account → Moneris.
  • A European or Swiss business whose shoppers expect TWINT, iDEAL or Bancontact → Mollie.
  • A shop that already takes cards in person with Square → Square, so online and in-person sales share one account.
  • Software or digital products sold worldwide, with an established merchant of record handling the tax → Paddle.
  • Digital products or downloads with a merchant of record, from a store already on Lemon Squeezy → Lemon Squeezy.
  • Bank transfers, cheques, or cash on delivery → offline.

Offering more than one is normal, Polar next to a non-MoR provider included. Each path is priced for the provider that is about to charge it.

Write your own provider

See the provider contract for the specification, the provider developer guide for a walkthrough, and admin sections for adding your own admin screen.

Run bin/plugin kahunacart status to list every registered provider with its declared capabilities.