Skip to main content

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:

  1. cartProcess creates the order with status UNFINISHED
  2. Your application redirects the customer to the PSP
  3. The PSP processes the payment
  4. The PSP sends a callback to your backend
  5. paymentCreate records the payment in Propeller
  6. paymentUpdate updates the payment when the PSP confirms the final result
  7. orderSetStatus confirms 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.

StatusDescription
OPENPayment created, awaiting processing
PENDINGPayment is being processed by the PSP
AUTHORIZEDPayment authorized but not yet captured
CANCELLEDPayment cancelled by the customer or merchant
EXPIREDPayment expired before completion
FAILEDPayment attempt failed
PAIDPayment successfully completed
REFUNDEDPayment has been refunded
CHARGEBACKPayment reversed by the bank (chargeback)

Transaction types

The TransactionTypes enum categorizes individual transactions within a payment.

TypeDescription
AUTHORIZATIONFunds reserved on the customer's account
CANCEL_AUTHORIZATIONPreviously authorized funds released
PAYFunds captured or paid
REFUNDFunds returned to the customer
CHARGEBACKFunds reversed by the bank

Transaction statuses

The TransactionStatuses enum tracks the outcome of an individual transaction.

StatusDescription
OPENTransaction initiated, awaiting result
PENDINGTransaction is being processed
FAILEDTransaction failed
SUCCESSTransaction 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 amount field is an integer representing the amount in the smallest currency unit. For example, €128.50 is 12850.

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

FieldTypeRequiredDescription
orderIdInt!YesThe order ID returned by cartProcess (cartOrderId)
amountInt!YesPayment amount in cents
currencyString!YesISO 4217 currency code (e.g. EUR, USD)
methodString!YesPSP payment method identifier (e.g. ideal, creditcard)
statusPaymentStatuses!YesInitial payment status (typically OPEN)
paymentIdStringNoPSP payment identifier to store for later reference
userIdIntNoLogged-in user ID
anonymousIdIntNoGuest user ID
addTransactionCreateTransactionInputNoInitial transaction to record

CreateTransactionInput fields

FieldTypeRequiredDescription
transactionIdString!YesPSP transaction identifier
amountInt!YesTransaction amount in cents
currencyString!YesISO 4217 currency code
typeTransactionTypes!YesTransaction type (PAY, AUTHORIZATION, REFUND, CANCEL_AUTHORIZATION, CHARGEBACK)
statusTransactionStatuses!YesTransaction status (OPEN, PENDING, FAILED, SUCCESS)
paymentIdStringNoPSP payment identifier
descriptionStringNoDescription of the transaction
timestampDateTimeNoWhen the transaction occurred at the PSP
providerStringNoPSP 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.

FieldTypeDescription
idIDPropeller's internal payment identifier
paymentIdStringPSP payment identifier (as stored via paymentId on create)
orderIdFloatThe order ID the payment belongs to

The orderId field in SearchByInput is typed as Float in 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.

FieldTypeDescription
statusPaymentStatusesUpdated payment status
amountIntUpdated amount in cents
currencyStringUpdated currency code
methodStringUpdated payment method
paymentIdStringUpdated PSP payment identifier
userIdIntUpdated user ID
anonymousIdIntUpdated guest user ID
addTransactionCreateTransactionInputNew 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

FieldTypeRequiredDescription
orderIdInt!YesThe order to update
statusStringNoNew order status code (e.g. NEW)
payStatusStringNoPayment status to set on the order (e.g. PAID, OPEN, FAILED)
sendOrderConfirmationEmailBooleanNoSend a confirmation email to the customer (default: false)
addPDFAttachmentBooleanNoAttach a PDF invoice to the confirmation email (default: false)
triggerOrderSendConfirmEventBooleanNoTrigger an ORDER_SEND_CONFIRMATION event via the Event Action Manager (default: false)
deleteCartBooleanNoDelete the originating cart after confirmation (default: false)

The status transition must be allowed by the configured status workflow. If you attempt a transition that is not configured, the API returns an ORDER_STATUS_TRANSITION_NOT_ALLOWED error. 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