Essentials

Order Lifecycle

Understand payment order states and transitions in DVPay

Understanding the order lifecycle helps you build robust payment integrations. This guide covers all order states, state transitions, and best practices for handling each stage.

Order States

DVPay orders progress through the following states:

pending
string
Order created, awaiting payment. QR code generated and ready for customer to scan.
processing
string
Payment initiated by customer. Funds are being verified and processed.
paid
string
Payment successfully completed. Funds received in merchant account.
completed
string
Order fulfilled and finalized. No further actions possible.
failed
string
Payment attempt failed. Customer can retry or order can be cancelled.
cancelled
string
Order cancelled before payment completion. No funds exchanged.
refunded
string
Payment refunded to customer. Funds returned from merchant account.
partially_refunded
string
Partial refund issued. Some funds returned, order remains paid.
expired
string
Payment window expired (default 30 minutes). Order no longer payable.

State Diagram

stateDiagram-v2
    [*] --> pending: Create Order
    pending --> processing: Customer Scans QR
    pending --> cancelled: Cancel Order
    pending --> expired: 30min Timeout

    processing --> paid: Payment Success
    processing --> failed: Payment Failed

    failed --> processing: Retry Payment
    failed --> cancelled: Cancel Order

    paid --> completed: Fulfill Order
    paid --> refunded: Full Refund
    paid --> partially_refunded: Partial Refund

    partially_refunded --> refunded: Refund Remaining

    completed --> [*]
    cancelled --> [*]
    refunded --> [*]
    expired --> [*]

State Transitions

Creating an Order (→ pending)

When you create an order, it starts in pending state:

order, err := createOrder(CreateOrderRequest{
    StoreID:  "store-123",
    Amount:   100.00,
    Currency: "USD",
})

if err != nil {
    log.Fatal(err)
}

fmt.Printf("Order created: %s\n", order.OrderID)
fmt.Printf("Status: %s\n", order.Status) // "pending"
fmt.Printf("QR Code: %s\n", order.QRCode)
fmt.Printf("Expires at: %s\n", order.ExpiresAt)

Next possible states:

  • processing - Customer scans QR code
  • cancelled - Merchant cancels order
  • expired - Payment window expires

Payment Processing (pending → processing)

When customer scans the QR code:

{
  "event": "payment.processing",
  "order_id": "ord_abc123",
  "status": "processing",
  "timestamp": 1696435200
}
You'll receive a webhook notification when payment enters processing state.

Actions to take:

  • Update UI to show "Processing payment..."
  • Lock inventory if applicable
  • Do not fulfill order yet

Next possible states:

  • paid - Payment succeeds
  • failed - Payment fails

Payment Success (processing → paid)

Payment completed successfully:

{
  "event": "payment.success",
  "order_id": "ord_abc123",
  "status": "paid",
  "amount": 100.00,
  "currency": "USD",
  "payment_method": "khqr",
  "timestamp": 1696435205
}
func handlePaymentSuccess(webhook PaymentWebhook) {
    // Update order status
    updateOrderStatus(webhook.OrderID, "paid")

    // Process fulfillment
    fulfillOrder(webhook.OrderID)

    // Send confirmation
    sendConfirmationEmail(webhook.OrderID)

    // Log transaction
    logTransaction(webhook.OrderID, webhook.Amount, webhook.Currency)
}

Actions to take:

  • Update order status in database
  • Begin order fulfillment
  • Send customer confirmation
  • Update inventory

Next possible states:

  • completed - Order fulfilled
  • refunded - Full refund issued
  • partially_refunded - Partial refund issued

Payment Failure (processing → failed)

Payment attempt failed:

{
  "event": "payment.failed",
  "order_id": "ord_abc123",
  "status": "failed",
  "failure_reason": "insufficient_funds",
  "timestamp": 1696435205
}
func handlePaymentFailure(webhook PaymentWebhook) {
    // Update order status
    updateOrderStatus(webhook.OrderID, "failed")

    // Release locked inventory
    releaseInventory(webhook.OrderID)

    // Notify customer
    notifyPaymentFailed(webhook.OrderID, webhook.FailureReason)

    // Log failure
    logFailure(webhook.OrderID, webhook.FailureReason)
}

Common failure reasons:

  • insufficient_funds - Customer wallet balance too low
  • payment_declined - Payment rejected by provider
  • timeout - Customer didn't complete payment in time

Next possible states:

  • processing - Customer retries payment
  • cancelled - Order cancelled

Order Completion (paid → completed)

Mark order as fulfilled:

POST /api/v1/payment-gateway/order/complete
{
  "order_id": "ord_abc123"
}
Once completed, orders cannot be refunded. Only mark as completed after successful delivery.

Actions to take:

  • Confirm delivery/service provided
  • Archive order records
  • Update analytics

Final state - No further transitions possible

Cancellation (pending/failed → cancelled)

Cancel an unpaid order:

curl -X POST https://merchant.dv.vai247.pro/api/v1/payment-gateway/order/cancel \
  -H "X-App-Id: your-app-id" \
  -H "X-Api-Key: your-api-key" \
  -H "X-Timestamp: $(date +%s)" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": "ord_abc123",
    "reason": "Customer requested cancellation"
  }'

Cancellable states:

  • pending - Before payment
  • failed - After failed payment

Cannot cancel:

  • paid - Use refund instead
  • completed - Already finalized
  • cancelled - Already cancelled

Refunds (paid → refunded/partially_refunded)

Issue full or partial refunds:

// Full refund
func refundOrder(orderID string) error {
    order, _ := getOrderDetails(orderID)

    return processRefund(RefundRequest{
        OrderID:  orderID,
        Amount:   order.Amount, // Full amount
        Currency: order.Currency,
        Reason:   "Customer requested refund",
    })
}

// Partial refund
func partialRefund(orderID string, refundAmount float64) error {
    order, _ := getOrderDetails(orderID)

    if refundAmount >= order.Amount {
        return errors.New("partial refund must be less than order amount")
    }

    return processRefund(RefundRequest{
        OrderID:  orderID,
        Amount:   refundAmount,
        Currency: order.Currency,
        Reason:   "Partial refund requested",
    })
}

Refund rules:

  • Only paid orders can be refunded
  • Refunds processed in original currency
  • Partial refunds can be issued multiple times
  • Total refunds cannot exceed original amount

Expiration (pending → expired)

Orders expire after 30 minutes (default):

{
  "event": "payment.expired",
  "order_id": "ord_abc123",
  "status": "expired",
  "created_at": 1696435200,
  "expired_at": 1696437000,
  "timestamp": 1696437000
}

Actions to take:

  • Release locked inventory
  • Update order status
  • Notify customer (optional)

Final state - No further transitions possible

Checking Order Status

Query Order Status

curl -X GET https://merchant.dv.vai247.pro/api/v1/payment-gateway/order/ord_abc123 \
  -H "X-App-Id: your-app-id" \
  -H "X-Api-Key: your-api-key" \
  -H "X-Timestamp: $(date +%s)"

Poll for Status Updates

For systems without webhooks:

async function pollOrderStatus(orderId, maxAttempts = 60) {
  for (let i = 0; i < maxAttempts; i++) {
    const status = await getOrderStatus(orderId);

    if (['paid', 'failed', 'cancelled', 'expired'].includes(status)) {
      return status; // Final state reached
    }

    // Wait 5 seconds before next check
    await new Promise(resolve => setTimeout(resolve, 5000));
  }

  throw new Error('Order status check timeout');
}

// Usage
const finalStatus = await pollOrderStatus('ord_abc123');
Polling is less efficient than webhooks. Use webhooks for production systems.

State-Specific Actions

Actions by State

StateCan CancelCan RefundCan CompleteShould Fulfill
pending
processing
paid
completedN/A
failed
cancelled
refunded
partially_refunded
expired

State Validation

Always validate state before performing actions:

function canCancel(orderStatus) {
  return ['pending', 'failed'].includes(orderStatus);
}

function canRefund(orderStatus) {
  return ['paid', 'partially_refunded'].includes(orderStatus);
}

function canComplete(orderStatus) {
  return ['paid', 'partially_refunded'].includes(orderStatus);
}

// Usage
const order = await getOrderDetails(orderId);

if (canRefund(order.status)) {
  await refundOrder(orderId);
} else {
  throw new Error(`Cannot refund order in ${order.status} state`);
}

Best Practices

1. Handle Webhooks Idempotently

Process each webhook event only once:

const processedEvents = new Set();

function processWebhook(webhook) {
  const eventKey = `${webhook.order_id}_${webhook.event}_${webhook.timestamp}`;

  if (processedEvents.has(eventKey)) {
    console.log('Event already processed');
    return;
  }

  processedEvents.add(eventKey);

  // Process the event
  switch (webhook.event) {
    case 'payment.success':
      handlePaymentSuccess(webhook);
      break;
    // ... other events
  }
}

2. Log State Transitions

Track all state changes for debugging:

async function updateOrderStatus(orderId, newStatus, reason = null) {
  const order = await getOrderDetails(orderId);
  const oldStatus = order.status;

  await db.orders.update(orderId, { status: newStatus });

  await db.statusHistory.insert({
    order_id: orderId,
    old_status: oldStatus,
    new_status: newStatus,
    reason: reason,
    timestamp: Date.now()
  });

  console.log(`Order ${orderId}: ${oldStatus}${newStatus}`);
}

3. Set Appropriate Timeouts

Configure order expiration times:

const ORDER_TIMEOUT_MINUTES = 30; // Default

const order = await createOrder({
  store_id: storeId,
  amount: amount,
  currency: currency,
  expires_in: ORDER_TIMEOUT_MINUTES * 60 // seconds
});

4. Handle Concurrent Updates

Prevent race conditions with optimistic locking:

async function updateOrderWithLock(orderId, updates) {
  const order = await db.orders.findOne({ id: orderId });

  // Check version hasn't changed
  const result = await db.orders.updateOne(
    { id: orderId, version: order.version },
    { ...updates, version: order.version + 1 }
  );

  if (result.modifiedCount === 0) {
    throw new Error('Order was modified by another process');
  }
}

Next Steps

Webhook Configuration

Set up real-time order status notifications

Create Order API

Learn how to create payment orders