.. _checking_payment_status: Checking Payment Status ======================= After redirecting a customer to checkout, you need to monitor the payment status to update your order accordingly. Webhooks vs Polling ------------------- There are two ways to learn about a payment's outcome: - **Webhooks (recommended)** — MiraclePay pushes signed events (``payment.succeeded``, ``payment.expired``, ...) to your backend the moment they happen. See :ref:`webhooks`. - **Polling** — your backend periodically calls ``GET /external/v1/payments/:id``. Use webhooks as the primary signal and polling as a fallback or for reconciliation. Either way, fulfill orders only from a state you read from the API — never from redirect query parameters. Payment Status Values --------------------- .. list-table:: :widths: 20 15 65 :header-rows: 1 * - 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 — inspect the payment object's ``lastError`` for the reason * - ``expired`` - Yes - The prompt's ``expiresAt`` passed without payment * - ``cancelled`` - Yes - Payment was cancelled by customer or system The payment object also carries a ``sessionStatus`` describing the checkout session itself: .. list-table:: :widths: 20 80 :header-rows: 1 * - Session status - Description * - ``pending`` - The checkout session is open. * - ``completed`` - The customer completed checkout. * - ``expired`` - The session expired. * - ``cancelled`` - The session was cancelled. Polling for Status ------------------ **Endpoint:** ``GET /external/v1/payments/:id`` (scope ``payment_prompts:read``, signed — see :ref:`authentication`) The response is the full :ref:`payment object `: .. code-block:: json { "object": "payment_prompt", "id": "550e8400-e29b-41d4-a716-446655440000", "amount": 1099, "commissionAmount": 22, "commissionPayer": "merchant", "settlementModel": "split", "status": "successful", "sessionStatus": "completed", "blockchainId": "eth-usdc", "tokenAmount": "10.99", "transactionHash": "0xabc...", "paidAt": "2026-07-21T14:35:00.000Z", "expiresAt": "2026-07-21T15:00:00.000Z", "createdAt": "2026-07-21T14:30:00.000Z", "redirectUrl": "https://shop.example.com/order/12345/complete", "customerReferenceId": "order_12345", "cancellationReason": null, "lastError": null } Polling Implementation ---------------------- Node.js / TypeScript ~~~~~~~~~~~~~~~~~~~~ Using the ``miraclePayFetch`` signed-fetch helper from :ref:`authentication`: .. code-block:: typescript type PaymentStatusValue = | "pending" | "successful" | "unsuccessful" | "expired" | "cancelled"; interface Payment { object: "payment_prompt"; id: string; amount: number; // cents status: PaymentStatusValue; sessionStatus: "pending" | "cancelled" | "expired" | "completed" | null; blockchainId: string | null; tokenAmount: string | null; transactionHash: string | null; paidAt: string | null; expiresAt: string; createdAt: string; customerReferenceId: string | null; lastError: { code: string; message: string; occurredAt: string } | null; } async function getPayment(promptId: string): Promise { const response = await miraclePayFetch( "GET", `/external/v1/payments/${promptId}`, ); if (!response.ok) { const { error } = await response.json(); throw new Error(`MiraclePay ${error.errorCode}: ${error.message}`); } return response.json(); } async function waitForPayment( promptId: string, timeoutMs = 300000, // 5 minutes ): Promise { const startTime = Date.now(); const pollInterval = 5000; // 5 seconds while (Date.now() - startTime < timeoutMs) { const payment = await getPayment(promptId); // Return immediately for terminal states if (payment.status !== "pending") { return payment; } 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 payment = await waitForPayment(promptId); if (payment.status === "successful") { await Order.update(orderId, { status: "paid", paidAt: payment.paidAt, blockchainId: payment.blockchainId, transactionHash: payment.transactionHash, }); return { success: true, message: "Payment confirmed!" }; } else { await Order.update(orderId, { status: "payment_failed", failureReason: payment.lastError?.code ?? payment.status, }); return { success: false, message: `Payment ${payment.status}` }; } } catch (error) { // Handle timeout - payment may still complete return { success: false, message: "Payment pending - check back later" }; } } Python ~~~~~~ The signing scheme is language-agnostic — the same canonical string (see :ref:`authentication`) with ``hmac`` and ``hashlib``. Assuming a ``get_payment(prompt_id)`` helper that performs the signed ``GET``: .. code-block:: python import time 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: payment = get_payment(prompt_id) if payment['status'] != 'pending': return payment 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: payment = wait_for_payment(prompt_id) if payment['status'] == 'successful': Order.objects.filter(id=order_id).update( status='paid', paid_at=payment['paidAt'], blockchain_id=payment['blockchainId'] ) return {'success': True, 'message': 'Payment confirmed!'} else: Order.objects.filter(id=order_id).update( status='payment_failed', failure_reason=payment['status'] ) return {'success': False, 'message': f"Payment {payment['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:** .. code-block:: javascript // When customer initiates checkout const { prompt } = await createPayment(amountCents, ["eth-usdc"]); await Order.update(orderId, { paymentPromptId: prompt.id, status: 'awaiting_payment' }); // Schedule background job await jobQueue.add('check-payment', { orderId, promptId: prompt.id }); **2. Background job polls for status:** .. code-block:: javascript // Background worker jobQueue.process('check-payment', async (job) => { const { orderId, promptId } = job.data; const payment = await getPayment(promptId); if (payment.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: payment.status === 'successful' ? 'paid' : 'payment_failed', paymentStatus: payment.status, }); // Send notification to customer if (payment.status === 'successful') { await sendOrderConfirmation(orderId); } }); Status Transition Diagram ------------------------- .. code-block:: text ┌─────────────────┐ │ Payment Created │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ pending │ └────────┬────────┘ │ ┌───────────────┬───────────┼───────────┬───────────────┐ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌───────────────┐ ┌────────────┐ ┌─────────┐ ┌───────────┐ ┌───────────┐ │ successful │ │unsuccessful│ │ expired │ │ cancelled │ │ (stays │ │ (confirmed) │ │ (failed) │ │(timeout)│ │ (user) │ │ pending) │ └───────────────┘ └────────────┘ └─────────┘ └───────────┘ └───────────┘ Best Practices -------------- 1. **Prefer webhooks**: Use :ref:`webhooks` as the primary signal and polling for fallback/reconciliation — don't poll synchronously inside HTTP handlers. 2. **Set reasonable timeouts**: Poll until the prompt's ``expiresAt`` (plus a small buffer) rather than a fixed wall-clock duration — expiry is configured per network. 3. **Handle all terminal states**: Your code should handle ``successful``, ``unsuccessful``, ``expired``, and ``cancelled``. For ``unsuccessful``, log the payment's ``lastError``. 4. **Implement idempotency**: Ensure processing the same payment twice doesn't cause issues (e.g., double fulfillment). On the create side, use the ``Idempotency-Key`` header — see :ref:`external_payments_api`. 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. .. code-block:: javascript // Example: Idempotent order update async function markOrderPaid(orderId, promptId) { const order = await Order.findById(orderId); // Already processed - skip if (order.status === 'paid') { return order; } // Verify payment status const payment = await getPayment(promptId); if (payment.status !== 'successful') { throw new Error(`Payment not successful: ${payment.status}`); } // Update order atomically return Order.findOneAndUpdate( { _id: orderId, status: { $ne: 'paid' } }, { status: 'paid', paidAt: new Date() } ); }