Pay now button#
If your system shows a Pay now button, the golden rule is: one live session per invoice. Creating a fresh session on every click is the single most common integration mistake, and it can double-charge a tenant.
Store the session reference
There is no endpoint to look up a session by invoice ID. If you do not persist the session_id we return, you cannot recover it later - your only option is to create a duplicate. Save it in the same transaction that creates the invoice.
The pattern#
Read your stored reference first. Only create when there is genuinely nothing live.
does your invoice row have a stored casapay_session_id?
yes -> GET /api/v1/gateway/sessions/{session_id}
|
+- status pending or processing -> reuse the existing gateway_url
+- status completed -> already paid, show the receipt
+- cancelled / expired / failed -> create a new session, overwrite the stored id
no -> POST /api/v1/gateway/agreements/{id}/invoice
store session_id + gateway_url on the invoice row1. Store the reference when you create the session#
curl -X POST https://manage.casapay.com/api/v1/gateway/agreements/4821/invoice \
-H 'Authorization: Bearer sk_live_...' \
-H 'Content-Type: application/json' \
-d '{
"amount": 650,
"description": "April rent",
"success_url": "https://your-app.com/done",
"cancel_url": "https://your-app.com/cancelled"
}'{
"session_id": "gwy_4tR8yN2wQe6xV3bM",
"gateway_url": "https://gateway.casapay.com/s/gwy_4tR8yN2wQe6xV3bM",
"status": "pending",
"expires_at": null,
"total_amount": 650.0,
"invoice_id": 91801
}Persist session_id, gateway_url and invoice_id against your own invoice or order record. session_id is the key you will need on every later click.
2. On every Pay now click, read before you create#
def pay_now_url(order):
# Fast path: we already have a session for this invoice.
if order.casapay_session_id:
session = casapay.get_session(order.casapay_session_id)
if session["status"] in ("pending", "processing"):
return session["gateway_url"] # reuse - do NOT create
if session["status"] == "completed":
return None # already paid
# cancelled / expired / failed -> fall through and create a new one
session = casapay.create_invoice_session(
agreement_id=order.casapay_agreement_id,
amount=order.amount_due,
success_url=...,
cancel_url=...,
)
order.casapay_session_id = session["session_id"] # overwrite
order.save()
return session["gateway_url"]The customer who comes back later#
Invoice sessions have expires_at: null - they never expire. A link you emailed in April still works in June.
So when a customer abandons checkout and returns days later, the correct behaviour is to hand them the same gateway_url. Nothing needs recreating, and the invoice is still the same invoice.
The customer who is mid-payment#
A session in processing means the tenant has already been sent to the payment provider and the result is still in flight. Do not start a second payment.
Show a "payment in progress" state and let the webhook or a status poll settle it. If you create a new session here, the tenant can end up paying twice for the same invoice.
Why duplicates are dangerous#
Two live sessions can both charge the same invoice
CasaPay makes each individual session idempotent: completion locks the session row, so the provider reporting success twice (browser return plus the server-to-server webhook) books the payment exactly once.
That guard is per session. It does not stop two different sessions from each charging the same invoice. CasaPay auto-cancels the other open sessions only after the first payment settles - which is too late if the tenant opened both links.
One live session per invoice is the only reliable protection, and it is your system's responsibility.
Reconciling webhooks#
Match incoming webhooks on invoice_id, and treat session_id as the idempotency key.
An invoice legitimately accumulates several sessions over its life - one that succeeded, plus earlier ones that were cancelled or expired. Keying your reconciliation on session_id alone will make a successful payment look unmatched if it arrived on a session you had already replaced.
Checklist#
- [ ]
casapay_session_idpersisted on your invoice/order row. - [ ] Pay now reads the stored session before creating anything.
- [ ]
pending/processingreuse the existinggateway_url. - [ ]
completedshows a receipt instead of a payment link. - [ ]
cancelled/expired/failedcreate a new session and overwrite the stored id. - [ ] Webhook handler matches on
invoice_idand is idempotent persession_id.
Related#
- Changing invoice amounts - when the amount is wrong, cancel and recreate.
- Sessions - the full session object and status values.