status field in synchronous responses! Synchronous responses may not reflect the final payment state, especially for delayed payment methods like bank transfers or BNPL services.When you receive a payment through Onerway Checkout (including Payment Links), you need to provide your customer with what they paid for. This process is known as fulfillment. Common fulfillment scenarios include:
You have two ways to handle fulfillment:
The manual option works for low transaction volumes or pilot operations, but for most situations we recommend automating fulfillment. The rest of this guide shows you how to build an automatic fulfillment system.
The automatic fulfillment system uses a dual-trigger mechanism combining webhooks (server-to-server notifications) and return URL redirects (landing page after payment):
You must use webhooks to ensure fulfillment happens for every payment. Landing page triggers optimize the experience by providing instant access when customers complete checkout.
Create a function on your server to fulfill successful payments. Webhooks trigger this function, and it's also called when customers land on your return URL after completing checkout. This guide refers to this function as fulfill_checkout, but you can name it whatever you wish.
Perform fulfillment only once per payment. Because of how webhooks and internet connections work, your fulfill_checkout function might be called multiple times, possibly concurrently, for the same transaction. Handling this correctly ensures you don't duplicate fulfillment.
Your fulfill_checkout function must handle the following:
Correctly handle being called multiple times, possibly concurrently, with the same merchantTxnId. Use database transactions or distributed locks to prevent race conditions.
Use merchantTxnId as the input parameter for transaction correlation and idempotency checks.
Query transaction details and payment information through the Transaction Order Query APIPayments API.
Check status in combination with txnType to determine if fulfillment is required. Only fulfill when status = S and txnType = SALE.
Provision benefits, ship products, or trigger your fulfillment workflows based on your business needs.
Persist fulfillment records in your database to prevent duplicate processing and enable auditing.
Use the code below as a starting point for your fulfill_checkout function. The TODO comments indicate functionality you must implement.
async function fulfill_checkout(merchantTxnId) {
// TODO: Check if already fulfilled (idempotency)
const existing = await db.getFulfillment(merchantTxnId);
if (existing) {
return existing;
}
// Retrieve the transaction from the Transaction Order Query API
const { content } = await onerway.queryTransactions({
merchantTxnIds: merchantTxnId
});
const txn = content?.[0];
if (!txn) {
return { success: false, reason: 'Transaction not found' };
}
// Verify transaction type and status
if (txn.txnType !== 'SALE') {
return { success: false, reason: 'Invalid transaction type' };
}
// Only fulfill when payment is successful
if (txn.status !== 'S') {
return { success: false, reason: 'Payment not completed' };
}
// TODO: Perform fulfillment
await provisionService(txn);
// TODO: Record fulfillment in database
const fulfillment = await db.saveFulfillment({
merchantTxnId,
status: 'fulfilled',
timestamp: new Date()
});
return { success: true, merchantTxnId, fulfillment };
}
Depending on your business needs, your fulfill_checkout function might need to:
Create a webhook event handler to listen for payment events and trigger your fulfill_checkout function.
When someone pays you, Onerway sends a webhook notification. Set up an endpoint on your server to accept, process, and confirm receipt of these events.
Listen for the following event to trigger fulfillment:
notifyType = TXN + txnType = SALE + status = S: Payment completed successfullynotifyType = TXN + txnType = SALE + status = F: Payment failed or was declinedS = Success (fulfill the order)F = Failed/Declined/Rejected (do not fulfill)// Webhook endpoint handler
app.post('/webhook', async (req, res) => {
const payload = req.body;
// TODO: Verify the webhook signature
// See: https://docs.onerway.com/apis/en/v0.6/sign
if (!verifySignature(payload, process.env.MERCHANT_SECRET_KEY)) {
return res.status(401).send('Invalid signature');
}
// Return only the transactionId value (not a JSON object)
res.status(200).send(payload.transactionId);
// Process fulfillment asynchronously to avoid timeout
if (payload.notifyType === 'TXN' &&
payload.txnType === 'SALE' &&
(payload.status === 'S' || payload.status === 'F')) {
setImmediate(async () => {
try {
await fulfill_checkout(payload.merchantTxnId);
} catch (error) {
console.error(`Fulfillment error: ${error.message}`);
// TODO: Implement error handling and alerting
}
});
}
});
transactionId value (not a JSON object). Any other response triggers retriestransactionId to deduplicate webhook deliveries and merchantTxnId to prevent duplicate fulfillmentThe quickest way to test your webhook handler is to simulate webhook events locally:
localhost:3000)# Using ngrok to expose local server
ngrok http 3000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
notifyUrl to your public webhook endpoint (for example https://abc123.ngrok.io/webhook)fulfill_checkout was called with correct merchantTxnIdAfter completing a test payment, verify:
When creating a Checkout session, include both notifyUrl and returnUrl in the txnOrderMsg parameter:
{
"merchantNo": "800209",
"merchantTxnId": "e868e769-afe5-41f7-9882-04835122e0b3",
"orderAmount": "100.00",
"orderCurrency": "USD",
"txnType": "SALE",
// ... other required parameters
"txnOrderMsg": "{
\"notifyUrl\": \"https://your-domain.com/webhook\",
\"returnUrl\": \"https://your-domain.com/order-complete?merchantTxnId=e868e769-afe5-41f7-9882-04835122e0b3\",
\"appId\": \"your-app-id\",
\"products\": \"[...]\",
// ... other txnOrderMsg fields
}"
}
Parameter details:
| Parameter | Description |
|---|---|
notifyUrl | Your webhook endpoint URL to receive payment event notifications |
returnUrl | Your landing page URL where customers are redirected after payment |
merchantTxnId to your returnUrl so you can query the final payment status when customers land on the page. For example: https://your-domain.com/order-complete?merchantTxnId=YOUR_TXN_ID.Use the Transaction Order Query API to confirm final status, reconcile delayed payments, and safely resume fulfillment if your webhook handler was down. Query by merchantTxnId and only fulfill when the status is final and successful.
Webhooks are required to ensure fulfillment for every payment, but webhooks can sometimes be delayed. To optimize your payment flow and provide immediate confirmation when your customer is present, you can trigger a status check from your landing page.
When your customer completes payment and is redirected to your returnUrl, confirm the payment status and display the correct next step:
// Landing page handler
app.get('/order-complete', async (req, res) => {
// 1. Extract merchantTxnId from URL
const merchantTxnId = req.query.merchantTxnId;
if (!merchantTxnId) {
return res.status(400).send('Missing transaction ID');
}
// 2. Confirm payment status before fulfillment
const result = await fulfill_checkout(merchantTxnId);
// 3. Render the landing page based on the final status
res.render('order-complete', {
status: result.success ? 'fulfilled' : 'pending',
merchantTxnId
});
});
When you render your landing page, you can display:
Some payment methods have asynchronous settlement or confirmation characteristics. When a checkout session ends, it doesn't necessarily mean funds are finalized or the payment is ultimately successful.
Examples of delayed payment methods:
| Payment Method | Type | Typical Delay |
|---|---|---|
| Trustly | Bank transfer | 2-5 days |
| Konbini | Convenience store | Up to 14 days |
| OXXO | Convenience store | Up to 14 days |
For delayed payment methods, the transaction can remain pending until payment either succeeds or fails. Onerway sends the same webhook event (notifyType = TXN, txnType = SALE) when payment eventually completes with the final status.
Fulfillment rules for delayed payments:
status = S before fulfillingstatus = F (failed)S) before fulfilling. Fulfilling orders prematurely can lead to financial losses if the payment fails or is disputed.You might also want to listen for payment failures and notify customers when a delayed payment doesn't succeed.