Emberstore logoEmberstore
Payment gateways

Payment gateways

The eleven gateways, what each one supports, the webhook address they use and how a payment gets confirmed.

A gateway is the company that takes the player's money: Stripe, PayPal, a crypto processor and so on. Emberstore asks the gateway for a payment link, the player pays on the gateway's own page, and the gateway tells your server when the money arrived. You can turn on as many gateways as you like. Players pick between them in the checkout dialog.

A grid of the eleven gateways showing which can refund by API, bill recurring subscriptions, look up a payment status and report disputes, and whether you register the webhook in its dashboard.

Pick your gateways

GatewayPlayers pay withPage
StripeCards and walletsStripe
PayPalPayPal balance and cardsPayPal
RazorpayUPI, cards and wallets, India focusedRazorpay
CreemCards, as merchant of record (Creem handles tax)Creem
Dodo PaymentsCards, as merchant of record (Dodo handles tax)Dodo Payments
NOWPaymentsCryptoNOWPayments
CryptomusCryptoCryptomus
BTCPay ServerCrypto, on your own BTCPay ServerBTCPay Server
PlisioCryptoPlisio
BitPayCryptoBitPay
LitepayCryptoLitepay

What the columns mean

FeatureWith itWithout it
Refund by API/store admin refund sends the money back for youThe order is only marked refunded and you return the money from the gateway's dashboard
RecurringItems with billing: recurring can be bought through it, as a real subscriptionNot offered for recurring items. Timed items with billing: once still work
Status lookupA payment whose webhook got lost is still found within a few minutesThe order waits for its webhook, or expires
DisputesA chargeback arrives on its own and takes the reward backHandle chargebacks by hand

Every gateway section in config.yml

Each gateway has its own section named after its id. These three keys are the same in all of them:

stripe:
  # Offer this gateway at checkout
  enabled: false
  # Material shown for this gateway in the checkout menu
  icon: DIAMOND
  # Currency this gateway bills in, empty uses price-currency
  currency: ""

The rest are the gateway's own keys, listed on its page. A gateway that is enabled but still has a YOUR_... placeholder (or an empty value) in one of its keys stays off, and the console names the missing key at startup. It never stops the plugin.

The name players see on the button comes from the gateway.<id> line in the language file, for example "<#ffd166>ā—†</#ffd166> <white>Card <#b0b7c3>(Stripe)".

The webhook address

Gateways tell your server about payments by sending a request to Emberstore's built-in listener. Its address is:

<webhook.public-base-url>/webhook/<gateway id>

With public-base-url: "https://store.example.com", Stripe posts to https://store.example.com/webhook/stripe and Razorpay to https://store.example.com/webhook/razorpay.

  • Some gateways need you to add this address in their dashboard and copy back a signing secret. Each gateway page says so.
  • The others get the address from Emberstore with every checkout, so there is nothing to register.
  • The listener runs on webhook.listen-address and webhook.listen-port (8443 by default). Almost every gateway wants HTTPS, and Razorpay only delivers to ports 80 and 443, so put a reverse proxy with a certificate in front of it. The Web dashboard guide shows how, for the webhook listener and the dashboard together.

How a payment is confirmed

Signed webhooks, reconciliation lookups and test simulations all go into one status handler, which completes, reviews, reverses, ends or renews the order.

Every webhook is checked against the gateway's signature (or, for PayPal and BitPay, by asking the gateway). A request that fails the check gets a 401 answer and changes nothing. That is what stops anyone from faking a "paid" message.

If a webhook never arrives, reconciliation catches it. Every reconciliation.poll-interval-seconds (120 by default), orders that are still open and older than reconciliation.stale-after-minutes (5) are looked up at their gateway. Unpaid orders older than checkout.expire-minutes are marked expired, which releases their stock and code use.

Both paths end in the same place, so a payment is handled the same way whichever arrives first, and a second message about the same payment does nothing.

When a payment is not granted

These orders get the Needs review status, nothing is given, and staff are told in game, in the console and (if set up) in Discord:

  • less was paid than the order total (more than half a percent short)
  • the payment came in a different currency
  • a partial payment
  • the payment arrived after the order had already expired
  • the item was deleted from the category files before the payment arrived
  • a payment accepted by hand at the gateway, where the gateway reports that

Look at the order with /store admin order <id>, check the gateway's dashboard, and give the item with /store admin give if it should be given.

config.yml keys used by every gateway

KeyDefaultWhat it does
webhook.enabledtrueRuns the listener on this server. On a network, only one server. See Networks.
webhook.listen-address"127.0.0.1"Address the listener binds to. Keep 127.0.0.1 behind a reverse proxy, use 0.0.0.0 inside a container.
webhook.listen-port8443Port the listener binds to.
webhook.public-base-url"https://yourdomain.com"Public HTTPS address of the listener.
success-url"https://yourdomain.com/thanks"Page buyers land on after paying.
cancel-url"https://yourdomain.com/cancelled"Page buyers land on after cancelling.
checkout.expire-minutes30Minutes a payment link stays valid.
reconciliation.enabledtrueLook up stuck orders at their gateway.
reconciliation.poll-interval-seconds120Seconds between lookups.
reconciliation.stale-after-minutes5Only orders older than this are looked up.
fees.show and fees.percentoffShows an estimated fee per gateway in checkout. Display only, it never changes what is charged.

On this page