Card payments and other gateways
Out of the box the shop takes bank transfer, cash on delivery and Bizum.
Out of the box the shop takes bank transfer, cash on delivery and Bizum. That works without registering with anyone, and without anyone being able to close your account.
If you also want cards, five gateways are ready. Each one appears by itself as soon as you supply its credentials; without them, it does not exist.
Which to choose
| Best for | Brings | |
|---|---|---|
| MONEI | Spain | Bizum, cards, PayPal, Apple and Google Pay |
| Stripe | International | Cards, Apple and Google Pay, SEPA, iDEAL |
| PayPal | The button buyers ask for most | PayPal and cards |
| Mollie | Northern Europe | iDEAL, Bancontact, cards, SEPA |
| Adyen | High volume | Cards and local methods across half the world |
If you sell in Spain, MONEI. It is a Payment Institution licensed by the Bank of Spain, reaches the same banking rails as your bank's card terminal, and does real Bizum.
What about Redsys? It is the terminal behind CaixaBank, BBVA and Santander, and it would be the first choice, but its official libraries are PHP, Java and .NET. Integrating it here would mean writing its cryptographic signature by hand, and in a payment that is the class of bug no test finds: the customer who got charged twice finds it. MONEI covers that ground without anybody inventing crypto.
Turning one on
The main way is the panel: Settings → Payments → Payment gateways. Paste the
gateway's keys and press Save and enable. They are stored encrypted, take
effect at once with no restart, and what you save there overrides the
.env. Saving needs an encryption key on the server: JWT_SECRET or
PCC_CLAVE_CIFRADO.
Each gateway also has a Test mode switch, and the notification address to copy (see below).
You can also give the keys in the backend's .env and restart:
# The backend's public address: the payment notification URL is built from it
PCC_BACKEND_URL=https://api.your-shop.com
# MONEI: Bizum and cards
MONEI_API_KEY=pk_...
MONEI_ACCOUNT_ID=... # optional
# Stripe: the three are needed; without the public key Stripe is not mounted
STRIPE_API_KEY=sk_live_...
STRIPE_PUBLIC_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
# PayPal
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_WEBHOOK_ID=... # optional, see below
PAYPAL_SANDBOX=false # without this, PayPal runs in sandbox
# Mollie
MOLLIE_API_KEY=live_...
# Adyen: key, merchant account and client key are required
ADYEN_API_KEY=...
ADYEN_MERCHANT_ACCOUNT=...
ADYEN_CLIENT_KEY=...
ADYEN_HMAC_KEY=... # optional, but without it no notification is accepted
ADYEN_SANDBOX=false # without this, Adyen runs in sandbox
# Where buyers come back after paying elsewhere; if missing, STOREFRONT_URL is used
PAYMENT_RETURN_URL=https://your-shop.comIf a required value is missing, the gateway is simply not mounted: there is no error, it just does not appear.
Whether it charges for real depends on the gateway. Stripe, Mollie and MONEI
tell from the key itself (sk_live_, live_, pk_test_). PayPal and Adyen
start in sandbox unless you set PAYPAL_SANDBOX=false or
ADYEN_SANDBOX=false, or turn off Test mode in the panel.
The storefront does not need the Stripe public key: the backend hands it over
with the payment. The factory themes send the buyer back to /checkout/volver,
and from there to the same address with their language, /en or /es.
A new gateway is switched on in every region when it is registered. To turn it off in a region, go to Settings → Payments → Payment methods: each method has one switch per region.
If you sell CBD or hemp
Stripe forbids it in its UK and EU terms, and so does Shopify Payments. It is not a technical problem and no amount of configuration fixes it: the account gets closed, usually without warning and with the money held.
Specialist processors charge 1 % to 5 % and hold reserves of up to 10 % for 180 days.
That is why the built-in methods — transfer, cash on delivery and Bizum — are not a stopgap until cards arrive. For that sector they are the reliable route, and for anyone they are what stops you depending on nobody objecting to what you sell.
How paying works, per gateway
| What the buyer sees | |
|---|---|
| Transfer, cash on delivery, Bizum | Confirms the order and that is it |
| PayPal, Mollie, MONEI | Goes to the gateway's page and comes back |
| Stripe | Pays without leaving the shop, in a Stripe form |
| Adyen | Pays without leaving, in Adyen's Drop-in with all its methods |
With the last two the order is closed on return, not when the button is pressed. Closing it earlier would mean accepting orders nobody has paid for.
And the return page does not believe the URL. The buyer coming back only
means "you may look now": the gateway is asked, and if the money is not there,
there is no order — however much it says ?success=true, which is something
anyone can type.
Adyen depends on its webhook
With the others, on return you ask the gateway and you are done. With Adyen you cannot: its API cannot answer about a session without a value only the browser holds, and its own documentation says the result arrives asynchronously, in a webhook.
That webhook is HMAC-signed, so what it says can be trusted, and it carries the status and the amount. But it means one important thing:
With Adyen, the webhook has to arrive. If its URL is not reachable from the internet, the buyer pays and the order never closes. It does not work locally without a tunnel.
The Drop-in shows every method enabled on your account at once — cards, iDEAL, Klarna, whatever — and handles 3-D Secure itself.
The gateway's notifications
Every gateway notifies the shop when a payment changes. The URL is always:
<PCC_BACKEND_URL>/pagos/aviso/pp_pasarela_<gateway>For example, https://api.your-shop.com/pagos/aviso/pp_pasarela_stripe. The
panel shows it already put together, with a copy button, in each gateway's
settings.
That is why PCC_BACKEND_URL has to be the backend's public address, the
one reachable from the internet. Without it there is no notification URL to
give out.
- MONEI and Mollie are handed that URL with every payment: there is nothing to paste anywhere.
- Stripe, PayPal and Adyen need it registered in their own dashboard, as the destination for their notifications.
If a gateway needs a different URL — a proxy, a tunnel while developing — force
it with <GATEWAY>_WEBHOOK_URL, for example MONEI_WEBHOOK_URL. It overrides
the computed one.
What arrives in that notification is not believed. The signature is checked, only the payment's identifier is taken from it, and the gateway is asked again what the status is. That sounds excessive until you think about what a webhook is: a request anyone who guesses the URL can send, saying "this one is paid".
Per-gateway details:
- Stripe and Adyen sign with a secret. With no secret configured, the shop
accepts none of their notifications — an open route that marks orders as
paid cannot be left alive by an oversight in
.env. - MONEI signs with its own header; its SDK validates it.
- Mollie does not sign, on purpose: its notification carries only the identifier precisely so that you have to ask again.
- PayPal uses no shared secret: you ask PayPal whether the notification is
theirs.
PAYPAL_WEBHOOK_IDperforms that check; without it, the safety comes from re-reading the order.
If something fails
"falta el paquete stripe" — the gateway is configured but its library is
not installed. npm i stripe in the backend.
A gateway does not appear at checkout — a required value is missing (for Adyen, the merchant account or the client key; for Stripe, the public key) and the gateway is not mounted. There is no error message: check its card in Settings → Payments, where it shows as Not configured. Also check its switch for the buyer's region.
A payment stays "pending" — look at the gateway's notification log. Almost always it is the webhook URL, wrong or unreachable from outside.
Adding another gateway
The five live in @pcreative/payments-contract, each on top of its gateway's
official SDK. To add one more you implement six operations — create, read,
capture, cancel, refund and webhook — and register it like the rest. The rest of
the system never notices.