Seller Card
The Seller Card lets a supplier spend their Payability balance directly. Two issuers are live at once: the original Marqeta cards and the newer Airwallex cards, presented through a single unified UI.
Eligibility
private isSellerCardAvailable(user: User): boolean {
const hasPaymentHistory = user?.status?.hasPaymentHistory;
const pcardAvailable = user?.pcardAvailable;
const isFromUS = this.isFromUS(user);
return hasPaymentHistory && (pcardAvailable || isFromUS) && !this.isUserDuringOnboarding(user);
}
private isFromUS(user: User): boolean {
const country = user?._businessInformation?.address?.country;
const residentialCountry = user?._personalInformation?.residentialAddress?.country;
return [country, residentialCountry].every(code => code === CountryCode.US);
}
| Condition | Note |
|---|---|
|
At least one payment must have been processed. |
|
Explicitly enabled by Payability, or both addresses are |
Not onboarding |
|
|
|
Two issuers, one UI
| Marqeta | Airwallex | |
|---|---|---|
Enum value |
|
|
Modelled as |
A payment method ( |
A card ( |
Status enum |
|
|
Form factor |
|
|
Shipping |
Physical cards shipped to a recipient address |
Uses the cardholder address on file |
The unified card model
src/app/core/pyb/interfaces/unified-supplier-card.interface.ts normalizes both
into one shape so the "Your Cards" panel can render them together.
export enum UnifiedCardStatus {
Active = 'ACTIVE',
Blocked = 'BLOCKED',
Pending = 'PENDING',
Closed = 'CLOSED',
}
export interface UnifiedSupplierCard extends CardShipping {
id: string;
provider: CardProvider;
formFactorIndicator: string;
formFactorLabelKey: string;
last4: string | null;
status: UnifiedCardStatus;
statusLabelKey: string;
statusColor: string;
providerLabelKey: string;
createdAt?: string;
raw: PaymentMethodModel | AirwallexCard; (1)
}
| 1 | raw retains the original provider model so the page can route an action to
the correct service. |
Status mapping:
| Unified | Marqeta (PCardStatus) |
Airwallex (AirwallexCardStatus) |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Unmapped values fall through to Closed.
Presentation mapping:
| Unified status | Label key | Colour |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Synthesized payment methods
The unified /cards endpoint is authoritative for display data, but Marqeta
actions historically route through the payment-methods store. When a Marqeta
card in the /cards response has no matching payment method,
synthesizeMarqetaPaymentMethod() fabricates a minimal stand-in carrying the
card token:
const synthesizeMarqetaPaymentMethod = (dto: UnifiedCardDto): PaymentMethodModel => ({
paymentConfig: {
pcard: {
card_token: dto.token,
cardNumber: dto.last_four ?? '',
pcard_status: dto.state,
pcard_type_indicator: marqetaFormFactor(dto.instrument_type),
},
},
} as unknown as PaymentMethodModel);
|
A synthesized method is enough for block and activate, which only need the card
token. The View card flow requires a real |
Form factor is derived from Marqeta’s instrument_type string:
const marqetaFormFactor = (instrumentType: string | undefined): string =>
(instrumentType ?? '').toUpperCase().includes('VIRTUAL')
? PCardTypeIndicator.Virtual
: PCardTypeIndicator.Physical;
AirwallexCardService
src/app/core/pyb/services/airwallex-card.service.ts — the HTTP surface, all
supplier-scoped.
| Method | Endpoint |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The order request is shaped per provider:
newCard(supplierKey, { provider, formFactor, recipientAddress, expediteCard }) {
return this.http.post(`${this.env.apiUrl}/suppliers/${supplierKey}/cards/newcard`, {
provider,
form_factor: formFactor,
...(recipientAddress ? { recipient_address: recipientAddress } : {}), (1)
...(expediteCard != null ? { expedite_card: expediteCard } : {}),
});
}
| 1 | recipient_address and expedite_card are only sent for issuers that ship a
physical card. Airwallex uses the cardholder address already on file. |
|
Airwallex consent must be recorded before a card is ordered. If |
Card operations
Each operation is a small service in
views/pages/seller-card/services/, and each follows the same shape:
confirm → 2FA challenge → API call → analytics.
block(card: AirwallexCard): Observable<AirwallexCardDto> {
return this.openBlockCardConfirmationModal().pipe(
filter(Boolean),
switchMap(() => this.check2FaState()),
filter(Boolean),
switchMap(() => this.airwallexCardService.blockCard(this.userService.userId, card.cardId)),
tap(() => this.eventLoggerService.track({ eventMessage: AirwallexCardEventTypes.CARD_BLOCKED })),
);
}
| Service | Behaviour |
|---|---|
|
Confirmation modal → 2FA → |
|
Confirmation → 2FA → |
|
2FA → |
|
Marqeta equivalents. |
|
Marqeta card details. |
|
Marqeta card activation. |
|
Every card operation requires a passed 2FA challenge. |
Ordering a card — /newcard
NewCardModule (views/pages/newcard/) is the ordering funnel. It is where
SellerCardListGuard sends a supplier who has no cards yet.
|
The ordering page. |
|
Benefit presentation. |
|
Layout shell. |
|
Physical-card delivery address form. |
|
Page copy and option constants. |
Backed by the cardOrder slice: loadCardProviders resolves the available
CardProvider / CardFormFactor combinations, then orderCard submits.
export enum CardProvider { Marqeta = 'marqeta', Airwallex = 'airwallex' }
export enum CardFormFactor { Physical = 'PHYSICAL', Virtual = 'VIRTUAL' }
The Airwallex transition
Existing Marqeta cardholders must accept Airwallex’s terms before their card is migrated. This is delivered as a dashboard notification, not a page.
|
Accept the new provider’s terms. Opens |
|
Identity document expired — opens |
GlobalSubscriptionsService subscribes to both, and the identity notification
takes precedence:
private subscribeToAirwallexPcardTermsNotification(): Subscription {
return this.store.pipe(
select(selectDashboardNotifications),
filter(notifications =>
this.canSeeAirwallexPcardTerms(notifications?.airwallexPcardTerms) &&
!this.canSeeIdentityDocumentExpired(notifications?.identityDocumentExpired)), (1)
take(1),
).subscribe(() => this.displayAirwallexPcardTermsModal());
}
| 1 | If the identity document has also expired, the standalone terms dialog is suppressed — the transition modal handles both in one pass. |
Terms-only dialog
private displayAirwallexPcardTermsModal() {
this.dialogService.open(AirwallexPcardTermsModalComponent) // no disableClose → dismissible
.afterClosed()
.pipe(take(1))
.subscribe(result => {
if (result?.actionType === CtaActionTypesEnum.Accept) {
this.store.dispatch(acceptDashboardNotifications({
payload: { type: DashboardNotificationType.AirwallexPcardTerms },
}));
}
});
}
Only Accept resolves the notification. Dismissing does nothing, so the dialog
returns on the next load until the terms are accepted. The dialog itself carries
an amber "Important" callout about the cutover date and a mandatory checkbox
linking the Terms & Conditions and the Cardholder Agreement; the accept button is
disabled until it is ticked. Copy lives under MODALS.AIRWALLEX_PCARD_TERMS in
src/assets/i18n/en.ts.
Combined transition dialog
private displayIdentityDocumentExpiredModal(notifications: DashboardNotifications): void {
const showTermsStep = !notifications?.identityDocumentExpired?.data?.airwallex_terms_accepted; (1)
this.dialogService.open(SellerCardTransitionModalComponent, { data: { showTermsStep } })
.afterClosed().pipe(take(1))
.subscribe(result => {
if (result?.actionType !== CtaActionTypesEnum.Accept) { return; }
if (result.termsAccepted) {
this.store.dispatch(acceptDashboardNotifications({
payload: { type: DashboardNotificationType.AirwallexPcardTerms, silent: true }, (2)
}));
}
this.store.dispatch(acceptDashboardNotifications({
payload: { type: DashboardNotificationType.IdentityDocumentExpired, silent: true },
}));
this.store.pipe(select(selectCurrentUser), take(1)).subscribe(user => {
if (user?.id) { this.entrustLauncher.launch(user.id).subscribe(); } (3)
});
});
}
| 1 | The terms step is skipped when the notification payload already reports
airwallex_terms_accepted. |
| 2 | silent: true suppresses the per-notification success modal — the dialog has
already given the supplier a single confirmation. |
| 3 | Accepting launches Entrust identity re-verification. See Entrust Identity. |
UI surface
|
|
|
The ordering funnel. |
|
Card transaction history. |
|
Dashboard card summary. |
|
The unified "Your Cards" panel. |
|
Card artwork per issuer. |
|
Activate, order, block, unblock, view, transition. |
|
Reveal Airwallex card details. |
|
Provider terms acceptance. |
Decline codes
NetworkResponseCodeEnum maps issuer decline codes to explanatory copy — expired
card, lost/stolen pick-up, insufficient funds, limit exceeded, CVV failure,
account blocked, suspected fraud, and around thirty more. See
Enum Reference.
Analytics
| Constant | Segment event |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The application never handles a raw PAN or PIN. Card details are revealed through
a short-lived provider token ( |