Skip to main content
Understanding the transaction lifecycle helps you build robust payment flows, handle edge cases, and provide accurate status updates to your customers.

Outgoing Transaction Flow

Your customer/platform sends funds to an external recipient.
1

Create Quote

Lock in exchange rate and fees:
POST /quotes

{
  "source": {"sourceType": "ACCOUNT", "accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},
  "destination": {"destinationType": "ACCOUNT", "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123"},
  "lockedCurrencySide": "SENDING",
  "lockedCurrencyAmount": 100000
}
Response:
  • Quote ID
  • Locked exchange rate
  • Expiration time (typically ~5 minutes or greater, depending on corridor)
2

Execute Quote

Initiate the payment:
POST /quotes/{quoteId}/execute
Result:
  • Transaction created with status PENDING
  • Source account debited immediately
  • OUTGOING_PAYMENT.PENDING webhook sent
3

Processing

Grid handles:
  • Currency conversion (if applicable)
  • Routing to appropriate payment rail
  • Settlement with destination bank/wallet
Status: PROCESSING
4

Completion or Failure

Success Path:
  • Funds delivered to recipient
  • Status: COMPLETED
  • settledAt timestamp populated
  • OUTGOING_PAYMENT.COMPLETED webhook sent
Failure Path:
  • Delivery failed (invalid account, etc.)
  • Status: FAILED
  • failureReason populated
  • OUTGOING_PAYMENT.FAILED webhook sent
  • Refund initiated automatically — track via the refund object and OUTGOING_PAYMENT.REFUND_* webhooks
Most transactions on Grid are completed in seconds.

Same-Currency Transfers

For same-currency transfers without quotes:

Transfer-Out (Internal → External)

POST /transfer-out

{
  "source": {"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},
  "destination": {"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", "currency": "USD"},
  "amount": 100000
}
Response:
{
  "id": "Transaction:...",
  "status": "PENDING",
  "type": "OUTGOING"
}
Follows same lifecycle as quote-based outgoing transactions.

Transfer-In (External → Internal)

POST /transfer-in

{
  "source": {"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", "currency": "USD"},
  "destination": {"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},
  "amount": 100000
}
Only works for “pullable” external accounts (e.g., debit cards).

Outgoing Payment Status

A single status field represents whether the transaction reached its destination:
StatusDescription
PENDINGQuote is pending confirmation
EXPIREDQuote wasn’t executed before the expiry window
PROCESSINGExecuting the quote after receiving funds (checking internal balances, or push/pull to/from external account)
COMPLETEDPayout successfully reached the destination account
FAILEDSomething went wrong — accompanied by a failureReason
EXPIRED and FAILED are terminal states, but COMPLETED is not always final — a bank can return a payment after it was marked COMPLETED, moving it to FAILED. Always continue processing webhook events for transactions even after they reach COMPLETED.

State Diagram

Refund Object

When a payment fails or is cancelled, refunds are tracked in a dedicated object on the transaction, decoupled from the payment status:
FieldDescription
referenceRefund reference ID
initiatedAtTimestamp when refund was initiated
settledAtTimestamp when refund settled
statusPENDING, COMPLETED, or FAILED
reasonWhy the refund occurred — TRANSACTION_FAILED, USER_CANCELLATION, or TIMEOUT
{
  "id": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
  "status": "FAILED",
  "type": "OUTGOING",
  "failureReason": "QUOTE_EXECUTION_FAILED",
  "refund": {
    "reference": "UMA-Q12345-REFUND",
    "initiatedAt": "2025-10-03T15:10:00Z",
    "settledAt": "2025-10-03T15:15:00Z",
    "status": "COMPLETED",
    "reason": "TRANSACTION_FAILED"
  }
}

Webhooks

Outgoing payment webhooks use the format OUTGOING_PAYMENT.<STATUS>. The webhook request body contains the full transaction resource.

Event Types

EventDescription
OUTGOING_PAYMENT.PENDINGTransaction created, quote pending confirmation
OUTGOING_PAYMENT.PROCESSINGQuote confirmed, payout in progress
OUTGOING_PAYMENT.COMPLETEDPayout reached destination
OUTGOING_PAYMENT.FAILEDPayout failed
OUTGOING_PAYMENT.EXPIREDQuote expired before execution
OUTGOING_PAYMENT.REFUND_PENDINGRefund initiated
OUTGOING_PAYMENT.REFUND_COMPLETEDRefund settled
OUTGOING_PAYMENT.REFUND_FAILEDRefund failed

Example Payloads

{
  "type": "OUTGOING_PAYMENT.PENDING",
  "data": {
    "id": "Transaction:...",
    "status": "PENDING",
    "type": "OUTGOING",
    "sentAmount": {"amount": 100000, "currency": {"code": "USD"}},
    "receivedAmount": {"amount": 92000, "currency": {"code": "EUR"}},
    "createdAt": "2025-10-03T15:00:00Z"
  }
}

Handling Webhooks

app.post('/webhooks/grid', async (req, res) => {
  const { data, type } = req.body;

  switch (type) {
    case 'OUTGOING_PAYMENT.COMPLETED':
      await notifyCustomer(data.customerId, 'Payment delivered!');
      break;

    case 'OUTGOING_PAYMENT.FAILED':
      await notifyCustomer(data.customerId, `Payment failed: ${data.failureReason}`);
      break;

    case 'OUTGOING_PAYMENT.REFUND_COMPLETED':
      await notifyCustomer(data.customerId, 'Refund completed.');
      break;

    case 'OUTGOING_PAYMENT.REFUND_FAILED':
      await notifyCustomer(data.customerId, 'Refund failed. Contact support.');
      break;
  }

  await updateTransactionStatus(data.id, type);
  res.status(200).json({ received: true });
});

Scenarios

The standard successful payment flow:
  1. OUTGOING_PAYMENT.PENDING
  2. OUTGOING_PAYMENT.PROCESSING
  3. OUTGOING_PAYMENT.COMPLETED

Listing Transactions

Query all transactions for a customer or date range:
GET /transactions?customerId=Customer:abc123&startDate=2025-10-01T00:00:00Z&limit=50
Response:
{
  "data": [
    {
      "id": "Transaction:...",
      "status": "COMPLETED",
      "type": "OUTGOING",
      "sentAmount": {"amount": 100000, "currency": {"code": "USD"}},
      "receivedAmount": {"amount": 92000, "currency": {"code": "EUR"}},
      "settledAt": "2025-10-03T15:05:00Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
Use for reconciliation and reporting.

Failure Handling

Common Failure Reasons

Failure ReasonDescriptionRecovery
QUOTE_EXPIREDQuote expired before executionCreate new quote
QUOTE_EXECUTION_FAILEDError executing the quoteCreate new quote
INSUFFICIENT_BALANCESource account lacks fundsFund account, retry
LIGHTNING_PAYMENT_FAILEDLightning network payment could not be routedRetry or use alternative rail
FUNDING_AMOUNT_MISMATCHFunding amount doesn’t match expected amountVerify amounts and retry
COUNTERPARTY_POST_TX_FAILEDPost-transaction processing at counterparty failedContact support
When a transaction fails, a refund is initiated automatically. Track the refund via the refund object on the transaction and OUTGOING_PAYMENT.REFUND_* webhook events. See Refund Object above.

Best Practices

Save transaction IDs to your database:
const transaction = await executeQuote(quoteId);
await db.transactions.insert({
  gridTransactionId: transaction.id,
  internalPaymentId: paymentId,
  status: transaction.status,
  createdAt: new Date()
});
Use idempotency keys for safe retries:
const idempotencyKey = `payment-${userId}-${Date.now()}`;
await createQuote({...params, idempotencyKey});
Translate technical statuses to user-friendly messages:
function getUserMessage(webhookType, data) {
  switch (webhookType) {
    case 'OUTGOING_PAYMENT.PENDING':
      return 'Payment processing...';
    case 'OUTGOING_PAYMENT.PROCESSING':
      return 'Payment in progress...';
    case 'OUTGOING_PAYMENT.COMPLETED':
      return 'Payment delivered!';
    case 'OUTGOING_PAYMENT.FAILED':
      return 'Payment failed. Please try again or contact support.';
    case 'OUTGOING_PAYMENT.REFUND_PENDING':
      return 'Refund in progress...';
    case 'OUTGOING_PAYMENT.REFUND_COMPLETED':
      return 'Refund completed. Funds returned to your account.';
    case 'OUTGOING_PAYMENT.REFUND_FAILED':
      return 'Refund failed. Please contact support.';
    default:
      return 'Payment status updated.';
  }
}