Skip to main content

Best Practices

Follow these patterns to build a reliable, production-ready PayFlow integration.

Use idempotency keys​

Idempotency keys prevent duplicate charges if a request is retried after a network failure. Include a unique Idempotency-Key header on all payment requests:

const { v4: uuidv4 } = require('uuid');

const payment = await payflow.payments.create(
{ amount: 2500, currency: 'gbp', customer_id: 'cus_9KZFXWr' },
{ idempotencyKey: uuidv4() } // Generate once, store with your order
);

If you retry the request with the same key, PayFlow returns the original response rather than creating a duplicate payment.

Never trust client-side amounts​

Always calculate payment amounts on your server, never from data sent by the client:

// ❌ Don't do this – a user could manipulate the amount
const { amount } = req.body;

// ✅ Do this instead – look up the price server-side
const product = await db.products.findById(req.body.productId);
const amount = product.price;

Store payment IDs immediately​

Save the payment ID to your database before responding to the customer, not after. This prevents lost transactions if the response fails to deliver:

const payment = await payflow.payments.create({ ... });

// Save FIRST, then respond
await db.orders.update({ id: orderId }, { paymentId: payment.id, status: 'paid' });

res.json({ success: true });

Use webhooks for fulfilment​

Don't rely on the payment API response alone to fulfil orders. Use webhooks as your source of truth, since they handle asynchronous payment methods, retries, and bank delays:

payment.create() → immediate response (use for UX feedback)
webhook payment.succeeded → fulfil order (use for business logic)

Log everything​

Log payment IDs, error codes, and customer IDs for every transaction. This is essential for debugging disputes and customer support:

const payment = await payflow.payments.create({ ... });

logger.info({
event: 'payment_created',
payment_id: payment.id,
customer_id: customerId,
amount: payment.amount,
currency: payment.currency,
});

Test all error scenarios​

PayFlow provides test card numbers that trigger specific errors in test mode. Always test:

  • ✅ Successful payment
  • ❌ Card declined
  • ❌ Insufficient funds
  • ❌ Network timeout (use retry logic)
  • ❌ Duplicate request (confirm idempotency works)