Payment method plugins
Add any payment provider with an API. The plugin starts payments and reads callbacks; ShopFast keeps the amounts, the ledger and the order.
A payment plugin is the seller’s own gateway (stores on their own gateway model; under ShopFast Pay, ShopFast
collects every payment and plugins are not used). The platform’s plugin gateway switch in the admin panel decides
whether payment plugins may run at all.
manifest.json
{
"name": "Example Pay",
"version": "1.0.0",
"kind": "payment",
"permissions": { "http": ["api.example-pay.com"], "kv": false },
"settings": [
{ "key": "api_key", "label": "API key", "type": "secret", "required": true },
{ "key": "webhook_secret", "label": "Webhook signing secret", "type": "secret", "required": true }
],
"payment": { "label": "Card (Example Pay)", "methods": ["card"] }
}
payment.methods are the methods buyers can pick at checkout (card, crypto, bank_transfer, paypal, manual,
apple_pay, google_pay).
create_payment
Called when the buyer places the order. Start the payment with your provider and return where to send the buyer and your provider’s reference.
function plugin.create_payment(ctx, input)
local s = input.session
local res, err = http.fetch({
url = "https://api.example-pay.com/v1/payments", method = "POST",
headers = { authorization = "Bearer " .. ctx.settings.api_key },
body = { amount = s.amountMinor, currency = s.currency, reference = s.id,
success_url = s.returnUrl, cancel_url = s.cancelUrl, notify_url = s.callbackUrl },
})
if not res then error(err) end
if not res.ok then error("Example Pay answered " .. res.status) end
local p = res.json()
return { redirect_url = p.checkout_url, provider_ref = p.id }
end
input.session: id, amountMinor, currency, method, buyerEmail, items (name, quantity, unitMinor),
returnUrl, cancelUrl, callbackUrl and expiresAt. Give callbackUrl to the provider as its webhook or
notification URL. If the hook fails, the buyer sees “This payment method is not available right now” and can pick
another way to pay.
verify_callback
The provider calls callbackUrl (/v1/plugins/plg_…/callback, GET or POST). The plugin gets the raw request and
decides what happened. Verify the provider’s signature first.
function plugin.verify_callback(ctx, request)
local expected = crypto.hmac("sha256", ctx.settings.webhook_secret, request.body)
if not crypto.equal(request.headers["x-signature"] or "", expected) then
return { status = "ignore", reply = "bad signature" }
end
local e = json.decode(request.body)
if e.type ~= "payment.succeeded" then return { status = "ignore" } end
return { status = "paid", provider_ref = e.payment.id, event_id = e.id,
amount_minor = e.payment.amount, currency = e.payment.currency }
end
request: method, headers (lowercase names; cookies and authorization removed), query and body. Return
status = paid, pending, failed or ignore, and for paid also provider_ref, event_id, amount_minor and
currency. reply is the body ShopFast answers the provider with (ok by default).
What ShopFast checks for you
A paid answer becomes a gateway event and goes through the same rules as Stripe or BTCPay:
- the payment must be one this plugin created for this store (
provider_refis matched against the checkout); amount_minorandcurrencymust equal the checkout total exactly, or the payment is refused and the run is marked failed with the reason;- each
event_idis applied once, so provider retries are harmless; - fees, the ledger, fulfillment and webhooks then run as for any other payment.
A plugin can say a payment happened, but it can never pay a different checkout, another store’s order or a lower amount. Refunds of plugin payments are made at the provider; ShopFast records them and reverses the ledger.