Bundul
Internal
Browse docs
Waiting for review

archive

Virtual Card Lifecycle: CPX Concepts & Bundul Approach

A frozen record of a decision at the time. Superseded by a new record rather than edited.

Written by the build · 2 September 2026

Virtual Card Lifecycle: CPX Concepts & Bundul Approach

This doc covers core CPX concepts, what they mean for Bundul, the ideal VC approach per user across services, a gap vs current implementation, and a roadmap.


1. Core CPX Concepts

Hierarchy

Concept Meaning
Network Top-level org in CPX. Groups institutions. We don’t operate at this level.
Institution Under network. Defines products (VCN, lodged, ACH) and processor options. Bundul’s VC capability depends on institution config.
Buyer The entity that makes payments. Bundul is the buyer in CPX, not the end user.
Supplier (Payee) The entity that receives payment. In Bundul, each service (Netflix, utility, etc.) is represented as a supplier per user+service.
Bank account Under the buyer. The funding source CPX debits for card/ACH. We add the user’s Passport account as a buyer bank account.

Payment & card

Concept Meaning
PIF (Payment Instruction File) The request we send to create/load a payment. One PIF = one payment instruction (initial load or top-up).
Payment record CPX’s record for one PIF. Has an id (internal, per payment) and a transactionId (we send it in the PIF).
VCN Virtual card number – the card product. Can be single-use (new card per PIF) or lodged.
Lodged card (LDG) One card per buyer–supplier. Multiple PIFs add funds to the same card; card details (number, expiry) do not change. Best fit for Bundul.
Card events Webhooks CPX sends for status changes: Ready, Settled, Declined, Failed, etc.

Identifiers

Term Meaning
id (CPX) Internal payment/card id from CPX. Changes per PIF. Used for decrypt card, balance, cancel.
transactionId (CPX) Value we send in each PIF. We should use it as our stable VC identifier (same for the VC’s lifetime or a clear pattern).
Supplier id / sid CPX supplier id (from create-supplier). We use sid like BundulInc-{userId}-{subId} so we can derive user and service.

Lifecycle (brief)

  • Create VC: Create supplier (if needed) → send PIF with buyer, supplier, amount, accountType (e.g. LDG). Store returned id (e.g. first one for card details) and transactionId (our stable VC id).
  • Top-up (lodged): Send another PIF to same supplier, LDG. Same card gets more funds; CPX may return a new id for the new payment record.
  • Cancel: POST cancelCard with payment id (before authorization). Delete is manual in CPX UI.
  • Card details: From disposition file (RSA) or decrypt API using a payment id (we keep one, e.g. first PIF id).
  • Balance: VCN balance API using payment id.

2. What This Means for Bundul

  • Bundul = CPX buyer. End user = owner of the funding (Passport); we pay suppliers on their behalf.
  • One VC per user per service = one supplier (user+service) in CPX, one lodged card, one stable identifier in our system.
  • Stable VC id = we control it. Use transactionId we send in the PIF (or a composite like BundulInc-{userId}-{subId}) and don’t use CPX’s per-PIF id as the VC’s primary id.
  • Tracking = link every PIF and every webhook to one VC record (and thus user + service) via that stable id and/or supplier.

3. Ideal Approach Across Services for a User

Goal: One user can bundul many services. Each service has one VC. We can track funding, charges, and errors per VC and roll up per user.

  1. VC record (DB)
    One row per user+service VC: userId, subscriptionId, service, cpxSupplierId, cpxSupplierSid, vcStableId, cpxCardIdForDetails (one id for “get card details”), status. Created when we create the CPX supplier for that user+service.

  2. Stable identifiers

    • vcStableId = our unique id for the VC (e.g. same as supplier sid or the transactionId we always send for that VC).
    • cpxPaymentId = CPX’s per-PIF id (for balance, cancel, decrypt).
    • cpxTransactionId = only when referring to the value in a specific PIF or webhook.
  3. PIF

    • Use accountType LDG for lodged.
    • Send a stable transactionId (or consistent pattern) so webhooks and search always tie back to the same VC.
    • Store first PIF’s id as cpxCardIdForDetails; don’t overwrite it when top-ups return new ids.
  4. Ledger (DB)
    One row per event: funding, top_up, charge, refund, error. Fields: userId, vcId, entryType, amount, service, referenceId (event id for dedupe), cpxPaymentId/cpxTransactionId when relevant, metadata (e.g. paymentAuthMessage for errors), occurredAt. This gives “how much, where, when top-up, and supports bloom/true-up.”

  5. Webhooks
    Map event to VC via transactionId/supplier → vcId. Dedupe by event id. Write ledger rows for: Ready (funding/top-up), Settled/Partially Settled (charge), Declined/Denied/Failed/Email Failed/Auth Expired (error + reason). Optionally sync to Airtable for ops view.

  6. Admin
    Actions on VCs: trigger top-up, cancel, refresh balance, refresh card details. All keyed by vcStableId or vcId; audit who did what.


4. Gap: Proper vs Current

Area Proper (target) Current
VC identity Stable vcStableId (we control); one VC record per user+service. Storing CPX id as “VC SID”; new id per PIF → hard to track.
transactionId Stable or deterministic per VC; used to link all events to one VC. Random per PIF; not used as VC identifier.
Supplier Persist supplier (user+service) in DB; every PIF/event links to it. Supplier created in CPX but not stored as first-class record; user/service derived from elsewhere.
Card type LDG (lodged) so same card, top-up adds funds. Likely single-use VCN; new card per PIF.
Top-up Same VC, same card; new ledger entry; stored refs (e.g. cpxCardIdForDetails) unchanged. New id per PIF; top-up doesn’t update stored refs; no ledger.
Ledger DB ledger: funding, top_up, charge, refund, error; supports true-up/bloom. No unified ledger; Airtable/events scattered.
Webhooks Dedupe by event id; map to VC via vcStableId/transactionId; write ledger; capture error reason (e.g. paymentAuthMessage). Partial handling (e.g. Ready/Settled); Ready vs VCN Generated mismatch; no ledger; error reason not consistently stored.
Naming Clear: vcStableId, cpxPaymentId, cpxTransactionId, cpxSupplierId. Confusing: transactionId, Tid, “VC SID” used for different things.
Cancel User cancel calls CPX cancel; stored VC status updated. Cancel path doesn’t call CPX cancel; only admin path does.

5. Other Details

  • Errors: Persist webhook error events (Declined, Denied, Failed, Email Failed, Auth Expired) and paymentAuthMessage; persist API errors (PIF submit, cancel) with message and context.
  • Admin: Start with Actions on VCs (trigger top-up, cancel, refresh balance, refresh card details); add visibility/export later. Restrict to admins; audit log.
  • Existing users: Plan to clean up old VCs and create new ones with the new method (new model only for going forward, or one-time migration – TBD).
  • Card expiry: Deferred; clarify with CPX later for lodged expiry/reissue.

6. Roadmap

Phase Focus Outcomes
1. Semantics & storage DB model and naming. VC table (supplier + user + service + vcStableId + cpxCardIdForDetails). Ledger table (entryType, amount, vcId, referenceId, etc.). Clear names in code (vcStableId, cpxPaymentId, cpxTransactionId).
2. PIF & supplier Lodged + stable id; persist supplier. Create supplier → save VC record. PIF sends LDG and stable transactionId; store first id as cpxCardIdForDetails. Every PIF linked to VC record.
3. Webhooks & ledger Event handling and ledger writes. Map events to VC; dedupe; write ledger (funding, top_up, charge, error with reason). Fix Ready vs VCN Generated if needed.
4. Admin actions Actions on VCs. Trigger top-up, cancel VC, refresh balance, refresh card details (by vcId/vcStableId). Audit log.
5. Visibility & cleanup Lists, detail view, existing users. Admin list/detail for VCs and ledger; optional Airtable sync. Plan and execute cleanup/recreate for existing users.

References

This is a record of a decision at the time. It is not edited — write a new record that supersedes it.