Partial payments
Let customers pay a single order across multiple payment methods.
Partial payments let a customer cover a single order using more than one payment method — for example, a gift card for the balance on file and a credit card for the remainder, or two cards split at checkout. Each method is charged only for its portion; the sum of all captured amounts should equal the order total. Common use cases include gift card redemption at checkout, mixed-method POS tender, and deposit-then-balance payment flows.
How it worksAsk
VINR does not have a native "split order" object. Instead, each portion is a standard payment created independently and tied to the same order by a shared reference in metadata. Your backend owns the order total check: VINR processes and settles each payment on its own rail, and your application decides when the collection of payments fully covers the order.
Every partial payment:
- Has its own
pay_…identifier and independent lifecycle. - Carries
metadata.orderId(and any other fields you choose) so you can group them server-side. - Emits its own lifecycle events (
payment.completed,payment.failed, etc.). - Can be refunded independently up to its own captured amount.
The recommended pattern is to store the order total in your own system, create payment objects one at a time, and accumulate amountCaptured across all payments sharing the same orderId until the sum reaches the order total.
Create a partial paymentAsk
Charge the first method (e.g. gift card)
Create a payment for exactly the amount the first method can cover. Set metadata.orderId to your order identifier and metadata.portion to 'partial' so your backend can query grouped payments later.
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
const giftCardPayment = await vinr.payments.create({
amount: 2000,
currency: 'EUR',
paymentMethod: giftCardPaymentMethodId,
description: 'Order #9142 — gift card portion',
returnUrl: 'https://yoursite.com/orders/9142/confirm',
metadata: {
orderId: '9142',
portion: 'partial',
},
});
// giftCardPayment.id → "pay_4Gc7w1b..."
// giftCardPayment.amount → 2000 (€20.00)Charge the second method for the remainder
Once the first payment reaches completed, create a second payment for the outstanding balance. Reuse the same orderId and set portion to 'final' to mark it as the settling charge.
const ORDER_TOTAL = 5500; // €55.00 in minor units
const firstCaptured = giftCardPayment.amountCaptured; // 2000
const cardPayment = await vinr.payments.create({
amount: ORDER_TOTAL - firstCaptured, // 3500 → €35.00
currency: 'EUR',
paymentMethod: cardPaymentMethodId,
description: 'Order #9142 — card remainder',
returnUrl: 'https://yoursite.com/orders/9142/confirm',
metadata: {
orderId: '9142',
portion: 'final',
},
});
// cardPayment.id → "pay_7Hm2n9d..."
// cardPayment.amount → 3500 (€35.00)Confirm the customer flow
Each payment that requires customer interaction (3DS, hosted redirect, etc.) has its own returnUrl. Redirect the customer to the first payment's hosted URL, wait for payment.completed, then present the second payment flow. The returnUrl for each can be the same page — use metadata.portion to track which step the customer just completed.
const remainder = await vinr.payments.create({
amount: 3500,
currency: 'EUR',
paymentMethod: cardPaymentMethodId,
metadata: { orderId: '9142', portion: 'final' },
});curl https://api.vinr.com/v1/payments \
-H "X-Api-Key: $VINR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 3500,
"currency": "EUR",
"paymentMethod": "pm_...",
"metadata": { "orderId": "9142", "portion": "final" }
}'Tracking order completionAsk
Listen for payment.completed on each partial payment. In your webhook handler, sum amountCaptured across all payments that share the same orderId and compare the total to the order amount stored in your system.
const event = vinr.webhooks.verify(rawBody, req.headers['x-vinr-signature']);
if (event.type === 'payment.completed') {
const payment = event.data;
const orderId = payment.metadata?.orderId;
if (orderId) {
await recordPartialCapture(orderId, payment.id, payment.amountCaptured);
const totalCaptured = await sumCapturedForOrder(orderId);
const orderTotal = await getOrderTotal(orderId);
if (totalCaptured >= orderTotal) {
await markOrderPaid(orderId);
}
}
}VINR does not auto-link partial payments or track whether they collectively satisfy an order total. Your application owns the order completion check. If the second payment fails, your backend must decide whether to retry, request a new method, or cancel and refund the first payment.
Refunds on partial ordersAsk
Each partial payment is refunded independently via its own pay_… ID, up to its own captured amount. A full order refund means issuing a separate refund for each payment that contributed to the order.
async function refundOrder(orderId: string): Promise<void> {
const payments = await getPaymentsForOrder(orderId);
for (const payment of payments) {
if (payment.amountCaptured > 0 && payment.amountRefunded < payment.amountCaptured) {
const refundable = payment.amountCaptured - payment.amountRefunded;
await vinr.refunds.create({
payment: payment.id,
amount: refundable,
reason: 'requested_by_customer',
metadata: { orderId },
});
}
}
}To refund only a portion of the order (e.g. one line item), determine which payment covered that item and issue a targeted refund against that payment's ID. See Refunds for status lifecycle and fee behaviour.
A refund can never exceed a single payment's captured amount. If a line item spans two payments — which is unusual but possible in open-ended split flows — you will need to issue two separate refunds.
Partial payment fieldsAsk
Prop
Type
AdvancedAsk
Next stepsAsk
Partial authorization
Capture less than the authorized amount and release the rest.
Refunds
Reverse captured funds on any individual payment object.
Payment lifecycle
Every payment status and the events that fire on each transition.
Last updated on