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 |
|---|---|---|
|
No |
Payment created, awaiting customer action on checkout page |
|
Yes |
Payment confirmed on blockchain - fulfill the order |
|
Yes |
Payment failed (e.g., transaction reverted) |
|
Yes |
24-hour window passed without payment |
|
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
Don't block user requests: Use background jobs or webhooks instead of synchronous polling in HTTP handlers.
Set reasonable timeouts: 5 minutes is usually enough for most blockchain confirmations.
Handle all terminal states: Your code should handle
successful,unsuccessful,expired, andcancelled.Implement idempotency: Ensure processing the same payment twice doesn't cause issues (e.g., double fulfillment).
Log everything: Store the full response for debugging and audit trails.
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() }
);
}