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 row

1. 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_id persisted on your invoice/order row.
  • [ ] Pay now reads the stored session before creating anything.
  • [ ] pending / processing reuse the existing gateway_url.
  • [ ] completed shows a receipt instead of a payment link.
  • [ ] cancelled / expired / failed create a new session and overwrite the stored id.
  • [ ] Webhook handler matches on invoice_id and is idempotent per session_id.