Skip to main content

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.

ScenarioUser typeAddress sourcePrefill possible
GuestNone (anonymous cart)Manual entryNo
B2CCustomercustomer.addressesYes
B2BContactcontact.company.addressesYes

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 type field on user addresses uses lowercase values (delivery, invoice), while the CartAddressType enum used in cartUpdateAddress uses 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

StatusWhen to use
UNFINISHEDCustomer needs to complete online payment (iDEAL, credit card, etc.). Create the order first, then redirect to the PSP
NEWNo 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:

  1. viewer query — fetch stored addresses for logged-in users (skip for guests)
  2. cartUpdateAddress (DELIVERY) — set the delivery address
  3. cartUpdateAddress (INVOICE) — set the invoice address
  4. cart query — fetch available shipping methods and carriers
  5. cartUpdate (postageData) — set the shipping method and carrier
  6. cart query — fetch available payment methods
  7. cartUpdate (paymentData) — set the payment method
  8. cart query — fetch the complete cart for final review
  9. cartProcess — create the order
  10. 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