Payment integration
Propeller does not process payments. It stores payment records and transaction history while actual payment processing is handled by an external payment service provider (PSP) such as Mollie, Adyen or MultiSafepay. This page covers creating, updating and querying payment records in Propeller and confirming or failing orders based on the PSP result.
For the steps leading up to order creation (addresses, shipping, payment method selection and cartProcess), see Checkout flow. For order lifecycle concepts and status transitions, see Understanding the order lifecycle.
How payments work in Propeller
When a customer checks out with an online payment method, the order is created with status UNFINISHED (meaning it awaits payment). Your application then redirects the customer to the PSP. After the PSP processes the payment, it sends a callback to your backend. Your backend records the result in Propeller and confirms or fails the order.
The typical flow:
cartProcesscreates the order with statusUNFINISHED- Your application redirects the customer to the PSP
- The PSP processes the payment
- The PSP sends a callback to your backend
paymentCreaterecords the payment in PropellerpaymentUpdateupdates the payment when the PSP confirms the final resultorderSetStatusconfirms the order (or marks it as failed)
Each payment record in Propeller can contain multiple transactions. Transactions form an audit trail of every step in the payment lifecycle. For example, an authorization followed by a capture followed by a partial refund would produce three transaction records on the same payment.
Payment statuses and transaction types
Payment statuses
The PaymentStatuses enum tracks the overall state of a payment record.
| Status | Description |
|---|---|
OPEN | Payment created, awaiting processing |
PENDING | Payment is being processed by the PSP |
AUTHORIZED | Payment authorized but not yet captured |
CANCELLED | Payment cancelled by the customer or merchant |
EXPIRED | Payment expired before completion |
FAILED | Payment attempt failed |
PAID | Payment successfully completed |
REFUNDED | Payment has been refunded |
CHARGEBACK | Payment reversed by the bank (chargeback) |
Transaction types
The TransactionTypes enum categorizes individual transactions within a payment.
| Type | Description |
|---|---|
AUTHORIZATION | Funds reserved on the customer's account |
CANCEL_AUTHORIZATION | Previously authorized funds released |
PAY | Funds captured or paid |
REFUND | Funds returned to the customer |
CHARGEBACK | Funds reversed by the bank |
Transaction statuses
The TransactionStatuses enum tracks the outcome of an individual transaction.
| Status | Description |
|---|---|
OPEN | Transaction initiated, awaiting result |
PENDING | Transaction is being processed |
FAILED | Transaction failed |
SUCCESS | Transaction completed successfully |
Creating a payment record
After initiating the payment with the PSP, create a payment record in Propeller using paymentCreate. This records the initial payment state and optionally includes the first transaction.
Amounts are in cents. The
amountfield is an integer representing the amount in the smallest currency unit. For example, €128.50 is12850.
Mutation
mutation PaymentCreate($input: CreatePaymentInput!) {
paymentCreate(input: $input) {
id
orderId
amount
currency
method
status
paymentId
transactions {
id
transactionId
amount
currency
type
status
provider
timestamp
}
createdAt
}
}
Variables
{
"input": {
"orderId": 534,
"amount": 12850,
"currency": "EUR",
"method": "ideal",
"status": "OPEN",
"paymentId": "pay_9B7dCf3Hxk",
"addTransaction": {
"transactionId": "tr_WdjK5Nf4p2",
"paymentId": "pay_9B7dCf3Hxk",
"amount": 12850,
"currency": "EUR",
"type": "PAY",
"status": "OPEN",
"provider": "Mollie",
"timestamp": "2025-11-03T14:22:08Z"
}
}
}
Response
{
"data": {
"paymentCreate": {
"id": "87",
"orderId": 534,
"amount": 12850,
"currency": "EUR",
"method": "ideal",
"status": "OPEN",
"paymentId": "pay_9B7dCf3Hxk",
"transactions": [
{
"id": "201",
"transactionId": "tr_WdjK5Nf4p2",
"amount": 12850,
"currency": "EUR",
"type": "PAY",
"status": "OPEN",
"provider": "Mollie",
"timestamp": "2025-11-03T14:22:08Z"
}
],
"createdAt": "2025-11-03T14:22:09Z"
}
}
}
CreatePaymentInput fields
| Field | Type | Required | Description |
|---|---|---|---|
orderId | Int! | Yes | The order ID returned by cartProcess (cartOrderId) |
amount | Int! | Yes | Payment amount in cents |
currency | String! | Yes | ISO 4217 currency code (e.g. EUR, USD) |
method | String! | Yes | PSP payment method identifier (e.g. ideal, creditcard) |
status | PaymentStatuses! | Yes | Initial payment status (typically OPEN) |
paymentId | String | No | PSP payment identifier to store for later reference |
userId | Int | No | Logged-in user ID |
anonymousId | Int | No | Guest user ID |
addTransaction | CreateTransactionInput | No | Initial transaction to record |
CreateTransactionInput fields
| Field | Type | Required | Description |
|---|---|---|---|
transactionId | String! | Yes | PSP transaction identifier |
amount | Int! | Yes | Transaction amount in cents |
currency | String! | Yes | ISO 4217 currency code |
type | TransactionTypes! | Yes | Transaction type (PAY, AUTHORIZATION, REFUND, CANCEL_AUTHORIZATION, CHARGEBACK) |
status | TransactionStatuses! | Yes | Transaction status (OPEN, PENDING, FAILED, SUCCESS) |
paymentId | String | No | PSP payment identifier |
description | String | No | Description of the transaction |
timestamp | DateTime | No | When the transaction occurred at the PSP |
provider | String | No | PSP provider name (e.g. Mollie, Adyen) |
Updating a payment
When the PSP sends a callback confirming the payment result, update the payment record using paymentUpdate. The searchBy argument identifies which payment to update, and addTransaction appends a new transaction entry to the payment's history.
Mutation
mutation PaymentUpdate($searchBy: SearchByInput!, $input: UpdatePaymentInput!) {
paymentUpdate(searchBy: $searchBy, input: $input) {
id
orderId
amount
currency
method
status
paymentId
transactions {
id
transactionId
amount
currency
type
status
provider
timestamp
}
lastModifiedAt
}
}
Variables
{
"searchBy": {
"orderId": 534
},
"input": {
"status": "PAID",
"addTransaction": {
"transactionId": "tr_WdjK5Nf4p2",
"paymentId": "pay_9B7dCf3Hxk",
"amount": 12850,
"currency": "EUR",
"type": "PAY",
"status": "SUCCESS",
"provider": "Mollie",
"timestamp": "2025-11-03T14:23:41Z"
}
}
}
Response
{
"data": {
"paymentUpdate": {
"id": "87",
"orderId": 534,
"amount": 12850,
"currency": "EUR",
"method": "ideal",
"status": "PAID",
"paymentId": "pay_9B7dCf3Hxk",
"transactions": [
{
"id": "201",
"transactionId": "tr_WdjK5Nf4p2",
"amount": 12850,
"currency": "EUR",
"type": "PAY",
"status": "OPEN",
"provider": "Mollie",
"timestamp": "2025-11-03T14:22:08Z"
},
{
"id": "202",
"transactionId": "tr_WdjK5Nf4p2",
"amount": 12850,
"currency": "EUR",
"type": "PAY",
"status": "SUCCESS",
"provider": "Mollie",
"timestamp": "2025-11-03T14:23:41Z"
}
],
"lastModifiedAt": "2025-11-03T14:23:42Z"
}
}
}
Notice that the response now contains two transactions. Each addTransaction appends a new entry. This gives you a complete audit trail of the payment lifecycle.
SearchByInput fields
The searchBy argument lets you identify the payment by different identifiers. Provide exactly one.
| Field | Type | Description |
|---|---|---|
id | ID | Propeller's internal payment identifier |
paymentId | String | PSP payment identifier (as stored via paymentId on create) |
orderId | Float | The order ID the payment belongs to |
The
orderIdfield inSearchByInputis typed asFloatin the schema. Pass the order ID as a number (e.g.534).
UpdatePaymentInput fields
All fields are optional. Only the fields you provide will be updated.
| Field | Type | Description |
|---|---|---|
status | PaymentStatuses | Updated payment status |
amount | Int | Updated amount in cents |
currency | String | Updated currency code |
method | String | Updated payment method |
paymentId | String | Updated PSP payment identifier |
userId | Int | Updated user ID |
anonymousId | Int | Updated guest user ID |
addTransaction | CreateTransactionInput | New transaction to append |
Confirming the order
After a successful payment, transition the order from UNFINISHED to NEW (or another confirmed status) using orderSetStatus. This also sets the payment status on the order and can trigger a confirmation email.
Mutation
mutation OrderSetStatus($input: OrderSetStatusInput!) {
orderSetStatus(input: $input) {
id
status
paymentData {
status
statusDate
}
}
}
Variables
{
"input": {
"orderId": 534,
"status": "NEW",
"payStatus": "PAID",
"sendOrderConfirmationEmail": true,
"addPDFAttachment": true,
"deleteCart": true
}
}
Response
{
"data": {
"orderSetStatus": {
"id": 534,
"status": "NEW",
"paymentData": {
"status": "PAID",
"statusDate": "2025-11-03T14:23:45Z"
}
}
}
}
OrderSetStatusInput fields
| Field | Type | Required | Description |
|---|---|---|---|
orderId | Int! | Yes | The order to update |
status | String | No | New order status code (e.g. NEW) |
payStatus | String | No | Payment status to set on the order (e.g. PAID, OPEN, FAILED) |
sendOrderConfirmationEmail | Boolean | No | Send a confirmation email to the customer (default: false) |
addPDFAttachment | Boolean | No | Attach a PDF invoice to the confirmation email (default: false) |
triggerOrderSendConfirmEvent | Boolean | No | Trigger an ORDER_SEND_CONFIRMATION event via the Event Action Manager (default: false) |
deleteCart | Boolean | No | Delete the originating cart after confirmation (default: false) |
The
statustransition must be allowed by the configured status workflow. If you attempt a transition that is not configured, the API returns anORDER_STATUS_TRANSITION_NOT_ALLOWEDerror. See Understanding the order lifecycle for details on status transitions.
Handling failed payments
When the PSP reports that a payment failed (customer cancelled, card declined, timeout), update the payment record and the order status to reflect the failure.
Update the payment
mutation PaymentUpdate($searchBy: SearchByInput!, $input: UpdatePaymentInput!) {
paymentUpdate(searchBy: $searchBy, input: $input) {
id
orderId
status
transactions {
id
transactionId
type
status
timestamp
}
}
}
{
"searchBy": {
"orderId": 534
},
"input": {
"status": "CANCELLED",
"addTransaction": {
"transactionId": "tr_WdjK5Nf4p2",
"paymentId": "pay_9B7dCf3Hxk",
"amount": 12850,
"currency": "EUR",
"type": "PAY",
"status": "FAILED",
"provider": "Mollie",
"timestamp": "2025-11-03T14:25:12Z",
"description": "Payment cancelled by customer"
}
}
}
Expected response:
{
"data": {
"paymentUpdate": {
"id": "8",
"orderId": 534,
"status": "CANCELLED",
"transactions": [
{
"id": "12",
"transactionId": "tr_QrPm8Xk3v1",
"type": "PAY",
"status": "OPEN",
"timestamp": "2025-11-03T14:20:00.000Z"
},
{
"id": "13",
"transactionId": "tr_WdjK5Nf4p2",
"type": "PAY",
"status": "FAILED",
"timestamp": "2025-11-03T14:25:12.000Z"
}
]
}
}
}
Use the payment status that best matches the PSP result. For example, use CANCELLED when the customer cancelled, EXPIRED when the payment window timed out and FAILED for a declined card.
Update the order status
mutation OrderSetStatus($input: OrderSetStatusInput!) {
orderSetStatus(input: $input) {
id
status
paymentData {
status
}
}
}
{
"input": {
"orderId": 534,
"status": "NEW",
"payStatus": "FAILED"
}
}
Expected response:
{
"data": {
"orderSetStatus": {
"id": 534,
"status": "NEW",
"paymentData": {
"status": "FAILED"
}
}
}
}
Retrying a payment
To let the customer retry payment after a failure, create a new payment record for the same order using paymentCreate. The previous payment and its transactions remain in the history as an audit trail. Redirect the customer to the PSP again and follow the same flow.
Refunds
To record a refund, update the payment with a REFUND transaction and set the payment status to REFUNDED.
Mutation
mutation PaymentUpdate($searchBy: SearchByInput!, $input: UpdatePaymentInput!) {
paymentUpdate(searchBy: $searchBy, input: $input) {
id
orderId
status
transactions {
id
transactionId
amount
type
status
timestamp
}
}
}
Variables
{
"searchBy": {
"orderId": 534
},
"input": {
"status": "REFUNDED",
"addTransaction": {
"transactionId": "tr_Rf8mN2Kp7v",
"paymentId": "pay_9B7dCf3Hxk",
"amount": 12850,
"currency": "EUR",
"type": "REFUND",
"status": "SUCCESS",
"provider": "Mollie",
"timestamp": "2025-11-05T09:15:30Z",
"description": "Full refund for order 534"
}
}
}
Response
{
"data": {
"paymentUpdate": {
"id": "87",
"orderId": 534,
"status": "REFUNDED",
"transactions": [
{
"id": "201",
"transactionId": "tr_WdjK5Nf4p2",
"amount": 12850,
"type": "PAY",
"status": "OPEN",
"timestamp": "2025-11-03T14:22:08Z"
},
{
"id": "202",
"transactionId": "tr_WdjK5Nf4p2",
"amount": 12850,
"type": "PAY",
"status": "SUCCESS",
"timestamp": "2025-11-03T14:23:41Z"
},
{
"id": "203",
"transactionId": "tr_Rf8mN2Kp7v",
"amount": 12850,
"type": "REFUND",
"status": "SUCCESS",
"timestamp": "2025-11-05T09:15:30Z"
}
]
}
}
}
For partial refunds, set the transaction amount to the refunded amount in cents rather than the full payment amount.
Pay-on-account flow
For B2B customers who pay on account, no PSP integration is needed. The order is created with a confirmed status directly and no payment records are required.
Use cartProcess with status NEW instead of UNFINISHED:
mutation CartProcess($id: String!, $input: CartProcessInput!) {
cartProcess(id: $id, input: $input) {
cart {
cartId
}
order {
id
status
}
cartOrderId
}
}
{
"id": "018dcc9a-f965-7434-8fad-369aa9a8c276",
"input": {
"orderStatus": "NEW"
}
}
Expected response:
{
"data": {
"cartProcess": {
"cart": {
"cartId": "018dcc9a-f965-7434-8fad-369aa9a8c276"
},
"order": {
"id": 534,
"status": "NEW"
},
"cartOrderId": 534
}
}
}
Then confirm the order and trigger the confirmation email:
mutation OrderSetStatus($input: OrderSetStatusInput!) {
orderSetStatus(input: $input) {
id
status
paymentData {
status
}
}
}
{
"input": {
"orderId": 534,
"status": "NEW",
"payStatus": "OPEN",
"sendOrderConfirmationEmail": true,
"addPDFAttachment": true,
"deleteCart": true
}
}
Expected response:
{
"data": {
"orderSetStatus": {
"id": 534,
"status": "NEW",
"paymentData": {
"status": "OPEN"
}
}
}
}
No paymentCreate or paymentUpdate calls are needed. The order is confirmed with payment status OPEN, indicating that payment will be handled outside the checkout flow (e.g. via invoice).
Fetching payment records
Fetch a single payment
Use the payment query to retrieve a specific payment record. The searchBy argument accepts the same fields as paymentUpdate (Propeller id, PSP paymentId or orderId).
Query
query Payment($searchBy: SearchByInput!) {
payment(searchBy: $searchBy) {
id
orderId
amount
currency
method
status
paymentId
userId
anonymousId
transactions {
id
transactionId
orderId
amount
currency
type
status
paymentId
description
provider
timestamp
}
createdAt
createdBy
lastModifiedAt
lastModifiedBy
}
}
Variables
{
"searchBy": {
"orderId": 534
}
}
Response
{
"data": {
"payment": {
"id": "87",
"orderId": 534,
"amount": 12850,
"currency": "EUR",
"method": "ideal",
"status": "PAID",
"paymentId": "pay_9B7dCf3Hxk",
"userId": null,
"anonymousId": null,
"transactions": [
{
"id": "201",
"transactionId": "tr_WdjK5Nf4p2",
"orderId": 534,
"amount": 12850,
"currency": "EUR",
"type": "PAY",
"status": "OPEN",
"paymentId": "pay_9B7dCf3Hxk",
"description": null,
"provider": "Mollie",
"timestamp": "2025-11-03T14:22:08Z"
},
{
"id": "202",
"transactionId": "tr_WdjK5Nf4p2",
"orderId": 534,
"amount": 12850,
"currency": "EUR",
"type": "PAY",
"status": "SUCCESS",
"paymentId": "pay_9B7dCf3Hxk",
"description": null,
"provider": "Mollie",
"timestamp": "2025-11-03T14:23:41Z"
}
],
"createdAt": "2025-11-03T14:22:09Z",
"createdBy": "system",
"lastModifiedAt": "2025-11-03T14:23:42Z",
"lastModifiedBy": "system"
}
}
}
The payment query returns null when no payment is found for the given search criteria.
List payments
Use the payments query to list payment records with pagination.
Query
query Payments($input: PaymentsSearchInput) {
payments(input: $input) {
items {
id
orderId
amount
currency
method
status
paymentId
createdAt
}
itemsFound
page
pages
offset
start
end
}
}
Variables
{
"input": {
"page": 1,
"offset": 12
}
}
Response
{
"data": {
"payments": {
"items": [
{
"id": "87",
"orderId": 534,
"amount": 12850,
"currency": "EUR",
"method": "ideal",
"status": "PAID",
"paymentId": "pay_9B7dCf3Hxk",
"createdAt": "2025-11-03T14:22:09Z"
},
{
"id": "86",
"orderId": 521,
"amount": 4500,
"currency": "EUR",
"method": "banktransfer",
"status": "PAID",
"paymentId": "pay_3Hk7mWn9Rp",
"createdAt": "2025-10-28T10:05:22Z"
}
],
"itemsFound": 2,
"page": 1,
"pages": 1,
"offset": 12,
"start": 0,
"end": 2
}
}
}
Deleting a payment record
Use paymentDelete to remove a payment record. The searchBy argument works the same way as in paymentUpdate.
Mutation
mutation PaymentDelete($searchBy: SearchByInput!) {
paymentDelete(searchBy: $searchBy) {
id
orderId
status
}
}
Variables
{
"searchBy": {
"id": "87"
}
}
Response
{
"data": {
"paymentDelete": {
"id": "87",
"orderId": 534,
"status": "PAID"
}
}
}
The mutation returns the deleted payment record.
Payment flow summary
Online payment (iDEAL, credit card, etc.)
cartProcess (UNFINISHED)
→ redirect to PSP
→ paymentCreate (OPEN)
→ PSP callback (success)
→ paymentUpdate (PAID)
→ orderSetStatus (NEW / PAID)
Pay on account
cartProcess (NEW)
→ orderSetStatus (NEW / OPEN)
Failed payment
cartProcess (UNFINISHED)
→ redirect to PSP
→ paymentCreate (OPEN)
→ PSP callback (failed)
→ paymentUpdate (CANCELLED or FAILED)
→ orderSetStatus (UNSUCCESSFUL / FAILED)
→ optionally retry with a new paymentCreate
Next steps
- Checkout flow for setting addresses, shipping and payment on the cart before order creation
- Understanding the order lifecycle for order statuses, transitions and the quote-to-order lifecycle
- Order history for displaying order history to customers