Checkout flow
Set addresses, shipping and payment on a cart, review the order and convert it into a Propeller order using cartProcess. This page picks up where Cart management leaves off (you already have a cart with items) and ends when the order is created. For payment processing after order creation, see Payment integration.
Guest, B2C and B2B checkout
The checkout flow works for three types of users. The main difference is where address data comes from.
| Scenario | User type | Address source | Prefill possible |
|---|---|---|---|
| Guest | None (anonymous cart) | Manual entry | No |
| B2C | Customer | customer.addresses | Yes |
| B2B | Contact | contact.company.addresses | Yes |
For guest checkout, the user must enter all address fields manually. For logged-in users (B2C customers or B2B contacts), you can prefill addresses from their account data.
Prefilling addresses for logged-in users
Use the viewer query to fetch the current user's details and stored addresses. The query returns either a Contact (B2B) or a Customer (B2C), determined by __typename.
Query
query {
viewer {
__typename
firstName
middleName
lastName
email
phone
mobile
... on Contact {
contactId
company {
companyId
name
addresses {
id
firstName
middleName
lastName
company
street
number
numberExtension
postalCode
city
country
email
phone
type
isDefault
icp
notes
}
}
}
... on Customer {
customerId
addresses {
id
firstName
middleName
lastName
street
number
numberExtension
postalCode
city
country
email
phone
type
isDefault
icp
notes
}
}
}
}
Response
{
"data": {
"viewer": {
"__typename": "Contact",
"firstName": "Sophie",
"middleName": "van",
"lastName": "Dijk",
"email": "sophie@brouwerindustrie.nl",
"phone": "+31201234567",
"mobile": "+31612345678",
"contactId": 42,
"company": {
"companyId": 10,
"name": "Brouwer Industrie",
"addresses": [
{
"id": 101,
"firstName": "Sophie",
"middleName": "van",
"lastName": "Dijk",
"company": "Brouwer Industrie",
"street": "Keizersgracht",
"number": "452",
"numberExtension": null,
"postalCode": "1016 GE",
"city": "Amsterdam",
"country": "NL",
"email": "sophie@brouwerindustrie.nl",
"phone": "+31201234567",
"type": "delivery",
"isDefault": "Y",
"icp": "N",
"notes": null
},
{
"id": 102,
"firstName": "Sophie",
"middleName": "van",
"lastName": "Dijk",
"company": "Brouwer Industrie",
"street": "Herengracht",
"number": "100",
"numberExtension": "A",
"postalCode": "1015 BS",
"city": "Amsterdam",
"country": "NL",
"email": "finance@brouwerindustrie.nl",
"phone": "+31201234567",
"type": "invoice",
"isDefault": "Y",
"icp": "N",
"notes": null
}
]
}
}
}
}
How it works
For a Contact, addresses come from the company (contact.company.addresses). For a Customer, addresses are on the customer directly (customer.addresses). Use the type field (delivery, invoice, home) and the isDefault field to pre-select the appropriate address for each step of checkout.
A company can have multiple addresses of the same type. For example, a company with two warehouses might have two delivery addresses. Present all addresses to the user and pre-select the one where isDefault is Y.
The
typefield on user addresses uses lowercase values (delivery,invoice), while theCartAddressTypeenum used incartUpdateAddressuses uppercase values (DELIVERY,INVOICE). Map between these when prefilling.
Setting addresses
Use cartUpdateAddress to set the delivery and invoice addresses on the cart. Call this mutation twice: once for each address type.
Set the delivery address
Mutation
mutation SetDeliveryAddress($cartId: String!) {
cartUpdateAddress(
id: $cartId
input: {
type: DELIVERY
company: "Brouwer Industrie"
firstName: "Sophie"
middleName: "van"
lastName: "Dijk"
street: "Keizersgracht"
number: "452"
postalCode: "1016 GE"
city: "Amsterdam"
country: "NL"
email: "sophie@brouwerindustrie.nl"
phone: "+31201234567"
}
) {
deliveryAddress {
firstName
middleName
lastName
company
street
number
numberExtension
postalCode
city
country
email
phone
}
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cartUpdateAddress": {
"deliveryAddress": {
"firstName": "Sophie",
"middleName": "van",
"lastName": "Dijk",
"company": "Brouwer Industrie",
"street": "Keizersgracht",
"number": "452",
"numberExtension": null,
"postalCode": "1016 GE",
"city": "Amsterdam",
"country": "NL",
"email": "sophie@brouwerindustrie.nl",
"phone": "+31201234567"
}
}
}
}
Set the invoice address
Mutation
mutation SetInvoiceAddress($cartId: String!) {
cartUpdateAddress(
id: $cartId
input: {
type: INVOICE
company: "Brouwer Industrie"
firstName: "Sophie"
middleName: "van"
lastName: "Dijk"
street: "Herengracht"
number: "100"
numberExtension: "A"
postalCode: "1015 BS"
city: "Amsterdam"
country: "NL"
email: "finance@brouwerindustrie.nl"
phone: "+31201234567"
}
) {
invoiceAddress {
firstName
middleName
lastName
company
street
number
numberExtension
postalCode
city
country
email
phone
}
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cartUpdateAddress": {
"invoiceAddress": {
"firstName": "Sophie",
"middleName": "van",
"lastName": "Dijk",
"company": "Brouwer Industrie",
"street": "Herengracht",
"number": "100",
"numberExtension": "A",
"postalCode": "1015 BS",
"city": "Amsterdam",
"country": "NL",
"email": "finance@brouwerindustrie.nl",
"phone": "+31201234567"
}
}
}
}
How it works
The type field determines which address you are setting: DELIVERY for shipping or INVOICE for billing. Both mutations return the full cart, so you can request either or both address objects in the response to confirm the update.
Required fields are type, firstName, lastName, street, postalCode and city. The country field defaults to "NL" if not provided. The icp field (Intra-Community Performance) defaults to N and controls whether tax is applied for B2B cross-border orders within the EU.
Propeller recalculates totals after each address update, because the delivery address can affect shipping costs and the tax calculation.
Choosing a shipping method
Shipping configuration has three parts: selecting a shipping method, optionally selecting a carrier or pickup location, and updating the cart.
Fetch available shipping methods
Query
query GetShippingMethods($cartId: String!) {
cart(id: $cartId) {
shippingMethods {
name
code
}
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cart": {
"shippingMethods": [
{ "name": "Delivery", "code": "DELIVERY" },
{ "name": "Pickup", "code": "PICKUP" }
]
}
}
}
Present these options to the customer. If they choose delivery, fetch available carriers. If they choose pickup, fetch available pickup locations.
Fetch available carriers
Query
query GetCarriers($cartId: String!) {
cart(id: $cartId) {
carriers {
id
name
price
logo
deliveryDeadline
}
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cart": {
"carriers": [
{
"id": 1,
"name": "PostNL",
"price": 6.95,
"logo": null,
"deliveryDeadline": "2026-03-03T17:00:00Z"
},
{
"id": 2,
"name": "DHL",
"price": 8.95,
"logo": null,
"deliveryDeadline": "2026-03-02T12:00:00Z"
}
]
}
}
}
The price on a carrier is an estimate. The final shipping cost is calculated by Propeller's business rules when you set the carrier on the cart.
Fetch pickup locations
If the customer chooses self-pickup, fetch active pickup locations using the warehouses query.
Query
query GetPickupLocations {
warehouses(input: { isActive: true, isPickupLocation: true }) {
items {
id
name
description
address {
street
number
city
postalCode
country
phone
}
businessHours {
dayOfWeek
openingTime
closingTime
}
}
itemsFound
}
}
Response
{
"data": {
"warehouses": {
"items": [
{
"id": 1,
"name": "Magazijn Amsterdam",
"description": "Afhaallocatie Amsterdam",
"address": {
"street": "Industrieweg",
"number": "12",
"city": "Amsterdam",
"postalCode": "1099 DC",
"country": "NL",
"phone": "+31201234000"
},
"businessHours": [
{ "dayOfWeek": 1, "openingTime": "08:00", "closingTime": "17:00" },
{ "dayOfWeek": 2, "openingTime": "08:00", "closingTime": "17:00" },
{ "dayOfWeek": 3, "openingTime": "08:00", "closingTime": "17:00" },
{ "dayOfWeek": 4, "openingTime": "08:00", "closingTime": "17:00" },
{ "dayOfWeek": 5, "openingTime": "08:00", "closingTime": "15:00" }
]
}
],
"itemsFound": 1
}
}
}
Set shipping method for delivery
After the customer selects a shipping method and carrier, update the cart using cartUpdate with postageData.
Mutation
mutation SetShippingMethod($cartId: String!) {
cartUpdate(
id: $cartId
input: {
postageData: {
method: "DELIVERY"
carrier: "postnl"
partialDeliveryAllowed: Y
}
}
) {
postageData {
method
carrier
price
priceNet
taxPercentage
partialDeliveryAllowed
requestDate
priceMode
}
total {
subTotal
subTotalNet
totalGross
totalNet
discount
discountNet
}
taxLevels {
taxPercentage
price
}
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cartUpdate": {
"postageData": {
"method": "DELIVERY",
"carrier": "postnl",
"price": 6.95,
"priceNet": 8.41,
"taxPercentage": 21,
"partialDeliveryAllowed": "Y",
"requestDate": null,
"priceMode": "PLATFORM"
},
"total": {
"subTotal": 4785.12,
"subTotalNet": 5789.99,
"totalGross": 4792.07,
"totalNet": 5798.40,
"discount": 0,
"discountNet": 0
},
"taxLevels": [
{ "taxPercentage": 21, "price": 1006.33 }
]
}
}
}
The priceMode field indicates whether the shipping cost was calculated by Propeller's business rules (PLATFORM) or set externally (EXTERNAL). To override shipping costs, pass a price in the postageData input, which sets priceMode to EXTERNAL.
The partialDeliveryAllowed flag lets the buyer indicate whether the supplier can ship available stock immediately and send remaining items in a follow-up shipment. B2B buyers often prefer partial deliveries because delaying the entire order until all items are available can halt production or operations.
Set shipping method for self-pickup
Mutation
mutation SetPickupMethod($cartId: String!) {
cartUpdate(
id: $cartId
input: {
postageData: {
method: "PICKUP"
pickUpLocationId: 1
}
}
) {
postageData {
method
price
priceNet
pickUpLocationId
}
total {
totalGross
totalNet
}
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cartUpdate": {
"postageData": {
"method": "PICKUP",
"price": 0,
"priceNet": 0,
"pickUpLocationId": 1
},
"total": {
"totalGross": 4785.12,
"totalNet": 5789.99
}
}
}
}
The pickUpLocationId corresponds to a warehouse id from the warehouses query.
Requesting a specific delivery date
You can pass a preferred delivery date using the requestDate field in postageData:
postageData: {
method: "DELIVERY"
carrier: "postnl"
requestDate: "2026-03-10T00:00:00Z"
}
This stores the customer's requested date on the cart. Whether this date is honored depends on carrier availability and Propeller's business rules.
Choosing a payment method
Fetch available payment methods
Query
query GetPaymentMethods($cartId: String!) {
cart(id: $cartId) {
payMethods {
code
name
type
price
taxCode
}
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cart": {
"payMethods": [
{
"code": "ACCOUNT",
"name": "Pay on account",
"type": "ACCOUNT",
"price": 0,
"taxCode": "N"
},
{
"code": "IDEAL",
"name": "iDEAL",
"type": "IDEAL",
"price": 0.29,
"taxCode": "H"
},
{
"code": "CREDITCARD",
"name": "Credit Card",
"type": "CREDITCARD",
"price": 0.50,
"taxCode": "H"
}
]
}
}
}
The price on a payment method is the transaction fee. The final fee is calculated by Propeller's business rules when you set the method on the cart.
Set the payment method
Mutation
mutation SetPaymentMethod($cartId: String!) {
cartUpdate(
id: $cartId
input: {
paymentData: {
method: "IDEAL"
status: "OPEN"
}
}
) {
paymentData {
method
price
priceNet
taxPercentage
status
statusDate
priceMode
}
total {
subTotal
subTotalNet
totalGross
totalNet
discount
discountNet
}
taxLevels {
taxPercentage
price
}
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cartUpdate": {
"paymentData": {
"method": "IDEAL",
"price": 0.29,
"priceNet": 0.35,
"taxPercentage": 21,
"status": "OPEN",
"statusDate": null,
"priceMode": "PLATFORM"
},
"total": {
"subTotal": 4785.12,
"subTotalNet": 5789.99,
"totalGross": 4792.36,
"totalNet": 5798.75,
"discount": 0,
"discountNet": 0
},
"taxLevels": [
{ "taxPercentage": 21, "price": 1006.39 }
]
}
}
}
The default payment method is ACCOUNT (pay on account). If the customer selects a different method, the totals are recalculated to include the transaction fee. Like shipping costs, you can override the transaction fee by passing a price in the paymentData input.
Pay on account is the most common payment method in B2B commerce. Instead of paying at checkout, the company receives an invoice with payment terms (typically 30, 60 or 90 days). When the customer selects pay on account, no PSP redirect is needed after order creation. See Payment integration for the simplified flow.
Reviewing the cart
Before creating the order, fetch the complete cart for a final review. This gives the customer a chance to verify items, addresses, shipping, payment and totals.
Query
query ReviewCart($cartId: String!) {
cart(id: $cartId) {
items {
itemId
productId
quantity
price
priceNet
totalPrice
totalPriceNet
}
deliveryAddress {
firstName
middleName
lastName
company
street
number
numberExtension
postalCode
city
country
}
invoiceAddress {
firstName
middleName
lastName
company
street
number
numberExtension
postalCode
city
country
}
postageData {
method
carrier
price
priceNet
taxPercentage
}
paymentData {
method
price
priceNet
taxPercentage
}
total {
subTotal
subTotalNet
discount
discountNet
discountPercentage
totalGross
totalNet
}
taxLevels {
taxPercentage
price
}
actionCode
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cart": {
"items": [
{
"itemId": "019c77b7-72a0-75fe-8a20-b98927d76652",
"productId": 25,
"quantity": 3,
"price": 1595.04,
"priceNet": 1929.9984,
"totalPrice": 4785.12,
"totalPriceNet": 5789.99
}
],
"deliveryAddress": {
"firstName": "Sophie",
"middleName": "van",
"lastName": "Dijk",
"company": "Brouwer Industrie",
"street": "Keizersgracht",
"number": "452",
"numberExtension": null,
"postalCode": "1016 GE",
"city": "Amsterdam",
"country": "NL"
},
"invoiceAddress": {
"firstName": "Sophie",
"middleName": "van",
"lastName": "Dijk",
"company": "Brouwer Industrie",
"street": "Herengracht",
"number": "100",
"numberExtension": "A",
"postalCode": "1015 BS",
"city": "Amsterdam",
"country": "NL"
},
"postageData": {
"method": "DELIVERY",
"carrier": "postnl",
"price": 6.95,
"priceNet": 8.41,
"taxPercentage": 21
},
"paymentData": {
"method": "IDEAL",
"price": 0.29,
"priceNet": 0.35,
"taxPercentage": 21
},
"total": {
"subTotal": 4785.12,
"subTotalNet": 5789.99,
"discount": 0,
"discountNet": 0,
"discountPercentage": 0,
"totalGross": 4792.36,
"totalNet": 5798.75
},
"taxLevels": [
{ "taxPercentage": 21, "price": 1006.39 }
],
"actionCode": ""
}
}
}
Display this information clearly so the customer can verify everything before confirming. The pricing convention used throughout the cart: price fields are gross (excluding VAT) and priceNet fields are net (including VAT).
Creating the order
Convert the cart to an order using cartProcess.
Mutation
mutation CreateOrder($cartId: String!) {
cartProcess(
id: $cartId
input: { orderStatus: "UNFINISHED" }
) {
cartOrderId
order {
id
uuid
status
type
}
}
}
Variables
{
"cartId": "019c77b7-565f-7ea0-8660-42bc63a7f728"
}
Response
{
"data": {
"cartProcess": {
"cartOrderId": 534,
"order": {
"id": 534,
"uuid": "019c8a12-3456-7890-abcd-ef1234567890",
"status": "UNFINISHED",
"type": "dropshipment"
}
}
}
}
The cartOrderId is the Propeller order ID. You need this for payment tracking and order confirmation. The order field returns the full created order object if you need additional fields.
Choosing an order status
| Status | When to use |
|---|---|
UNFINISHED | Customer needs to complete online payment (iDEAL, credit card, etc.). Create the order first, then redirect to the PSP |
NEW | No online payment required (e.g. pay on account). The order goes directly to a confirmed state |
For orders requiring online payment, always use UNFINISHED first. After payment succeeds, update the status with orderSetStatus. See Payment integration for the full payment flow.
Order confirmation language
The language field on CartProcessInput controls which language the order confirmation email is sent in:
cartProcess(
id: $cartId
input: { orderStatus: "UNFINISHED", language: "NL" }
)
If not provided, the default language is used.
Purchase authorization
Purchase authorization is a standard procurement control in B2B. Companies configure spending limits per contact to prevent unauthorized purchases. When a contact places an order that exceeds their limit, the cart enters a pending state until an authorization manager (typically a procurement lead or department head) approves or rejects it. Your frontend should show a clear message explaining that the order is awaiting approval.
The cart has a purchaseAuthorizationRequired field. When this is true, the contact's spending limit has been exceeded and the order requires approval before it can be processed.
Check this field before calling cartProcess:
query CheckAuthorization($cartId: String!) {
cart(id: $cartId) {
purchaseAuthorizationRequired
status
}
}
Expected response:
{
"data": {
"cart": {
"purchaseAuthorizationRequired": false,
"status": "OPEN"
}
}
}
If purchaseAuthorizationRequired is true, call cartRequestPurchaseAuthorization instead of cartProcess. The cart status changes to PENDING_PURCHASE_AUTHORIZATION until an authorization manager approves it with cartAcceptPurchaseAuthorizationRequest.
For more on how purchase authorization roles and limits work, see Understanding companies, contacts and customers.
Typical checkout sequence
Here is the full sequence of API calls for a standard checkout:
viewerquery — fetch stored addresses for logged-in users (skip for guests)cartUpdateAddress(DELIVERY) — set the delivery addresscartUpdateAddress(INVOICE) — set the invoice addresscartquery — fetch available shipping methods and carrierscartUpdate(postageData) — set the shipping method and carriercartquery — fetch available payment methodscartUpdate(paymentData) — set the payment methodcartquery — fetch the complete cart for final reviewcartProcess— create the order- Redirect to PSP → handle payment → update order status (see Payment integration)
Steps 4 and 6 can be combined into a single query if you fetch shippingMethods, carriers and payMethods at the same time. Similarly, step 8 can be combined with step 7 by requesting all review fields in the cartUpdate response.
Next steps
- Payment integration — track payments, handle PSP callbacks and confirm orders
- Cart management — create and manage cart items before checkout
- Understanding the order lifecycle — how orders, quotes and quote requests relate