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.
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.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.
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.
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.”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.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
- CPX API Reference: https://developer.cpxchange.com/reference
- Card events: https://developer.cpxchange.com/docs/card_event_alert-1
- Cancel: https://developer.cpxchange.com/docs/cancel-cardstransactions
- Payment types (lodged): https://developer.cpxchange.com/docs/setting-payment-types
This is a record of a decision at the time. It is not edited — write a new record that supersedes it.