Docs
Dashboard
View as Markdown
On this page
  1. manifest.json
  2. create_payment
  3. verify_callback
  4. What ShopFast checks for you

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_ref is matched against the checkout);
  • amount_minor and currency must equal the checkout total exactly, or the payment is refused and the run is marked failed with the reason;
  • each event_id is 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.

Was this page helpful?