Skip to main content
Version: next

Implement a custom payment plan simulator

The Product.paymentPlans core GraphQL field returns the BNPL installment plans available for a given product. The amount is derived from the product's final price (incl. or excl. tax depending on the shop's theme.priceDisplay.variants configuration), and the result is aggregated from any number of registered simulators.

How aggregation works

loaders.Payment exposes a registry similar to registerEmbeddedPaymentMethod:

  • Each PSP package registers its simulator in its GraphQL module's contextEnhancer.
  • loaders.Payment.simulatePaymentPlans(amount) calls every registered simulator in parallel with Promise.allSettled.
  • Plans returned by a fulfilled simulator are concatenated; rejections are logged and skipped, so a single PSP outage cannot prevent the others from rendering.

Implement the simulator

Result

Once registered, the simulator contributes to Product.paymentPlans without any further wiring. Add the field to the product page query and your simulator shows up alongside every other registered one:

Performance

Product.paymentPlans is resolved per product, and simulator calls are not batched across products in a single GraphQL request. A query that returns N products fires N HTTP calls per registered simulator, in parallel.

Treat the field as a product-detail-page field. Avoid querying it on listings (PLP, cart, search results, related products). If your PSP exposes a batched simulation endpoint, share the call across the request with a DataLoader inside simulate(amount).

query ProductPaymentPlans($sku: String!) {
product(sku: $sku) {
paymentPlans {
id
type
provider {
id
label
logoUrl
}
installments {
dueDate
amount {
amount
currency
}
fees {
amount
currency
}
}
apr
debitRate
}
}
}