Checking Payment Status

After redirecting a customer to checkout, you need to monitor the payment status to update your order accordingly.

Payment Status Values

Status

Terminal

Description

pending

No

Payment created, awaiting customer action on checkout page

successful

Yes

Payment confirmed on blockchain - fulfill the order

unsuccessful

Yes

Payment failed (e.g., transaction reverted)

expired

Yes

24-hour window passed without payment

cancelled

Yes

Payment was cancelled by customer or system

Polling for Status

Use the GET /external/payments/prompt/:id endpoint to check payment status.

Endpoint: GET /external/payments/prompt/:id

Example Request:

curl -X GET https://api.miraclecash.info/external/payments/prompt/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Basic $(echo -n 'your_client_id:your_api_key' | base64)"

Example Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "blockchainId": "eth",
  "status": "successful",
  "createdAt": "2025-01-28T14:30:00.000Z"
}

Polling Implementation

Node.js / TypeScript

interface PaymentStatus {
  id: string;
  blockchainId: string | null;
  status: 'pending' | 'successful' | 'unsuccessful' | 'expired' | 'cancelled';
  createdAt: string;
}

async function waitForPayment(
  promptId: string,
  timeoutMs: number = 300000 // 5 minutes
): Promise<PaymentStatus> {
  const startTime = Date.now();
  const pollInterval = 5000; // 5 seconds

  while (Date.now() - startTime < timeoutMs) {
    const status = await getPaymentStatus(promptId);

    // Return immediately for terminal states
    if (status.status !== 'pending') {
      return status;
    }

    // Wait before next poll
    await new Promise(resolve => setTimeout(resolve, pollInterval));
  }

  throw new Error('Payment polling timeout');
}

// Usage in your order flow
async function processOrder(orderId: string, promptId: string) {
  try {
    const result = await waitForPayment(promptId);

    if (result.status === 'successful') {
      await Order.update(orderId, {
        status: 'paid',
        paidAt: new Date(),
        blockchainId: result.blockchainId,
      });
      return { success: true, message: 'Payment confirmed!' };
    } else {
      await Order.update(orderId, {
        status: 'payment_failed',
        failureReason: result.status,
      });
      return { success: false, message: `Payment ${result.status}` };
    }
  } catch (error) {
    // Handle timeout - payment may still complete
    return { success: false, message: 'Payment pending - check back later' };
  }
}

Python

import time
from typing import Literal

def wait_for_payment(
    prompt_id: str,
    timeout_seconds: int = 300,
    poll_interval: int = 5
) -> dict:
    """Poll until payment reaches a terminal state."""
    start_time = time.time()

    while time.time() - start_time < timeout_seconds:
        status = get_payment_status(prompt_id)

        if status['status'] != 'pending':
            return status

        time.sleep(poll_interval)

    raise TimeoutError('Payment polling timeout')


def process_order(order_id: str, prompt_id: str) -> dict:
    """Process order after customer completes checkout."""
    try:
        result = wait_for_payment(prompt_id)

        if result['status'] == 'successful':
            Order.objects.filter(id=order_id).update(
                status='paid',
                paid_at=datetime.now(),
                blockchain_id=result['blockchainId']
            )
            return {'success': True, 'message': 'Payment confirmed!'}
        else:
            Order.objects.filter(id=order_id).update(
                status='payment_failed',
                failure_reason=result['status']
            )
            return {'success': False, 'message': f"Payment {result['status']}"}

    except TimeoutError:
        return {'success': False, 'message': 'Payment pending'}

Background Job Pattern

For production systems, use a background job instead of blocking requests:

1. Create payment and store prompt ID:

// When customer initiates checkout
const payment = await createPayment(amount, 'eth');
await Order.update(orderId, {
  paymentPromptId: payment.prompt.id,
  status: 'awaiting_payment'
});

// Schedule background job
await jobQueue.add('check-payment', {
  orderId,
  promptId: payment.prompt.id
});

2. Background job polls for status:

// Background worker
jobQueue.process('check-payment', async (job) => {
  const { orderId, promptId } = job.data;
  const status = await getPaymentStatus(promptId);

  if (status.status === 'pending') {
    // Re-queue job to check again in 30 seconds
    throw new Error('Still pending - retry');
  }

  // Update order based on final status
  await Order.update(orderId, {
    status: status.status === 'successful' ? 'paid' : 'payment_failed',
    paymentStatus: status.status,
  });

  // Send notification to customer
  if (status.status === 'successful') {
    await sendOrderConfirmation(orderId);
  }
});

Status Transition Diagram

                           ┌─────────────────┐
                           │ Payment Created │
                           └────────┬────────┘
                                    │
                                    ▼
                           ┌─────────────────┐
                           │     pending     │
                           └────────┬────────┘
                                    │
        ┌───────────────┬───────────┼───────────┬───────────────┐
        │               │           │           │               │
        ▼               ▼           ▼           ▼               ▼
┌───────────────┐ ┌───────────┐ ┌─────────┐ ┌───────────┐ ┌───────────┐
│  successful   │ │unsuccessful│ │ expired │ │ cancelled │ │  (stays   │
│  (confirmed)  │ │  (failed)  │ │(timeout)│ │  (user)   │ │  pending) │
└───────────────┘ └───────────┘ └─────────┘ └───────────┘ └───────────┘

Best Practices

  1. Don't block user requests: Use background jobs or webhooks instead of synchronous polling in HTTP handlers.

  2. Set reasonable timeouts: 5 minutes is usually enough for most blockchain confirmations.

  3. Handle all terminal states: Your code should handle successful, unsuccessful, expired, and cancelled.

  4. Implement idempotency: Ensure processing the same payment twice doesn't cause issues (e.g., double fulfillment).

  5. Log everything: Store the full response for debugging and audit trails.

  6. Graceful degradation: If polling times out, don't assume failure - the payment may still complete.

// Example: Idempotent order update
async function markOrderPaid(orderId: string, promptId: string) {
  const order = await Order.findById(orderId);

  // Already processed - skip
  if (order.status === 'paid') {
    return order;
  }

  // Verify payment status
  const status = await getPaymentStatus(promptId);
  if (status.status !== 'successful') {
    throw new Error(`Payment not successful: ${status.status}`);
  }

  // Update order atomically
  return Order.findOneAndUpdate(
    { _id: orderId, status: { $ne: 'paid' } },
    { status: 'paid', paidAt: new Date() }
  );
}