Handle the result
The customer's browser brings them back, so nothing plain in that URL can be trusted. The broker appends a signed token instead.
The return
return_to
https://yourapp.com/billing/callback?session_id=7f3a9c1e-…&pay_token=eyJhbGciOiJIUzI1NiJ9…
pay_token is a JWS (HS256) signed with your secret key. Verify it on your backend, never in the browser. It is not called token so that auth libraries that read a token from the URL (Clowk, Devise and the like) leave it alone:
Ruby
claims = JWT.decode(params[:pay_token], ENV["PAY_SECRET_KEY"], true, algorithm: "HS256", iss: "pay", verify_iss: true, sub: params[:session_id], verify_sub: true).first claims["status"] # "paid", "failed", "expired", … claims["amount"] # "49.9", with "currency", "method" and "customer"
Node
import jwt from "jsonwebtoken" const claims = jwt.verify(req.query.pay_token, process.env.PAY_SECRET_KEY, { algorithms: ["HS256"], issuer: "pay", subject: req.query.session_id, })
Rules
- 1Pin HS256 and reject anything else, including none.
- 2The token expires 15 minutes after the redirect and can be replayed until then: grant what was bought once per session_id.
- 3It proves the status at redirect time. Refunds and late payments arrive by webhook (session.paid, session.refunded).
- 4Rather not verify tokens? Ignore it and call GET /api/v1/sessions/:session_id with your key.