Skip to content

Payments Endpoints

Payment endpoints handle the creation, submission, and confirmation of Stellar blockchain payments.

Payment Flow

Create Payment Intent

Generate an unsigned payment transaction for the client to sign.

Endpoint: POST /api/payments/:invoiceId/pay-intent

Authentication: Not required (public endpoint)

Parameters:

ParameterTypeLocationRequiredDescription
invoiceIdstringPathYesInvoice ID to pay
senderPublicKeystringBodyNoSender's Stellar address (desktop flow)
networkPassphrasestringBodyYesStellar network passphrase

Request Body:

json
{
  "senderPublicKey": "GDPYEQVXKP7VVXV6XJZXJQVXQVXQVXQVXQVXQVXQVXQVXQVXQVXQVXQV",
  "networkPassphrase": "Test SDF Network ; September 2015"
}

Success Response (200):

json
{
  "invoiceId": "cm123abc456def",
  "transactionXdr": "AAAAAgAAAABk7F...",
  "sep7Uri": "web+stellar:pay?destination=GABC...&amount=100&asset_code=USDC&memo=INV-0001",
  "amount": "100.00",
  "asset": {
    "code": "USDC",
    "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
  },
  "memo": "INV-0001",
  "networkPassphrase": "Test SDF Network ; September 2015",
  "timeout": 300
}

Response Fields:

FieldTypeDescription
invoiceIdstringInvoice being paid
transactionXdrstring | nullUnsigned transaction XDR (desktop flow)
sep7UristringSEP-7 payment URI (mobile flow)
amountstringPayment amount
asset.codestringAsset code (XLM, USDC, EURC)
asset.issuerstring | nullAsset issuer address (null for XLM)
memostringInvoice number used as memo
networkPassphrasestringNetwork the payment must use
timeoutnumberSeconds until transaction expires (300s)

Error Responses:

StatusErrorDescription
400Cannot pay your own invoiceSender matches invoice owner
400Invoice cannot be paidInvoice not in payable status
400Selected wallet network does not matchNetwork mismatch
404Invoice not foundInvalid invoice ID

Example:

bash
curl -X POST https://api.link2pay.dev/api/payments/cm123abc456def/pay-intent \
  -H "Content-Type: application/json" \
  -d '{
    "senderPublicKey": "GDPYEQVXKP7V...",
    "networkPassphrase": "Test SDF Network ; September 2015"
  }'

Usage Notes:

  • Invoice status changes to PROCESSING after successful pay-intent creation
  • transactionXdr is only returned if senderPublicKey is provided
  • sep7Uri is always returned for mobile wallet compatibility
  • Desktop flow: sign transactionXdr with Freighter → submit to /payments/submit
  • Mobile flow: open sep7Uri in wallet app (auto-signs and submits)

Submit Payment

Submit a signed payment transaction to the Stellar network.

Endpoint: POST /api/payments/submit

Authentication: Not required (public endpoint)

Request Body:

json
{
  "invoiceId": "cm123abc456def",
  "signedTransactionXdr": "AAAAAgAAAABk7F..."
}

Request Schema:

FieldTypeRequiredDescription
invoiceIdstringYesInvoice being paid
signedTransactionXdrstringYesSigned transaction XDR from wallet

Success Response (200):

json
{
  "success": true,
  "transactionHash": "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
  "ledger": 123456
}

Already Paid Response (200):

json
{
  "success": true,
  "alreadyPaid": true
}

Error Responses:

StatusErrorDescription
400Network mismatchTransaction network doesn't match invoice
400Insufficient balanceWallet doesn't have enough funds
400Your wallet does not have a trustlineMissing trustline for asset
400Recipient wallet is not activatedRecipient needs funding
404Invoice not foundInvalid invoice ID
429Network is busyHorizon rate limit hit
500Payment processing failedGeneric submission error
503Stellar network is temporarily unavailableHorizon down

Example:

bash
curl -X POST https://api.link2pay.dev/api/payments/submit \
  -H "Content-Type: application/json" \
  -d '{
    "invoiceId": "cm123abc456def",
    "signedTransactionXdr": "AAAAAgAAAABk7F..."
  }'

Usage Notes:

  • Transaction is validated against invoice network passphrase
  • If successful, invoice status becomes PAID
  • Idempotent: submitting an already-paid invoice returns success: true, alreadyPaid: true
  • Transaction hash can be viewed on Stellar Expert or StellarChain

Confirm Payment

Manually confirm a payment by providing a transaction hash.

Endpoint: POST /api/payments/confirm

Authentication: Not required (public endpoint)

Use Cases:

  • Mobile wallet flow (wallet submits directly to Stellar)
  • Retry after timeout
  • Manual verification

Request Body:

json
{
  "invoiceId": "cm123abc456def",
  "transactionHash": "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
}

Request Schema:

FieldTypeRequiredDescription
invoiceIdstringYesInvoice being confirmed
transactionHashstringYesStellar transaction hash

Success Response (200):

json
{
  "status": "confirmed",
  "transactionHash": "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
  "ledger": 123456,
  "paidAt": "2024-03-07T12:00:00.000Z"
}

Already Paid Response (200):

json
{
  "status": "already_paid",
  "transactionHash": "7a8b9c0d...",
  "paidAt": "2024-03-07T12:00:00.000Z"
}

Error Responses:

StatusErrorDescription
400Transaction was not successfulTransaction failed on-chain
400Transaction does not match invoicePayment details mismatch
400Underpayment: paid X, expected YAmount too low
404Invoice not foundInvalid invoice ID
404Transaction not found on networkInvalid hash or wrong network

Example:

bash
curl -X POST https://api.link2pay.dev/api/payments/confirm \
  -H "Content-Type: application/json" \
  -d '{
    "invoiceId": "cm123abc456def",
    "transactionHash": "7a8b9c0d..."
  }'

Verification Steps:

  1. Fetches transaction from Stellar Horizon
  2. Verifies transaction succeeded
  3. Validates payment recipient matches invoice
  4. Validates payment asset matches invoice currency
  5. Validates payment amount ≥ invoice total
  6. Marks invoice as PAID if all checks pass

Get Payment Status

Check the payment status of an invoice.

Endpoint: GET /api/payments/:invoiceId/status

Authentication: Not required (public endpoint)

Parameters:

ParameterTypeLocationRequiredDescription
invoiceIdstringPathYesInvoice ID

Success Response (200):

json
{
  "invoiceId": "cm123abc456def",
  "status": "PAID",
  "transactionHash": "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
  "ledgerNumber": 123456,
  "paidAt": "2024-03-07T12:00:00.000Z",
  "payerWallet": "GDPYEQVXKP7V..."
}

Response Fields:

FieldTypeDescription
invoiceIdstringInvoice ID
statusstringInvoice status (DRAFT, PENDING, PROCESSING, PAID, etc.)
transactionHashstring | nullStellar transaction hash (if paid)
ledgerNumbernumber | nullLedger number (if paid)
paidAtstring | nullISO timestamp of payment
payerWalletstring | nullPayer's wallet address

Error Responses:

StatusErrorDescription
404Invoice not foundInvalid invoice ID
500Failed to fetch payment statusServer error

Example:

bash
curl https://api.link2pay.dev/api/payments/cm123abc456def/status

Usage Notes:

  • Use for polling payment status
  • Recommended polling interval: 5 seconds
  • Status transitions: PENDINGPROCESSINGPAID
  • Alternative: Use webhooks (coming soon) instead of polling

Verify Transaction

Verify a Stellar transaction hash and get payment details.

Endpoint: POST /api/payments/verify-tx

Authentication: Not required (public endpoint)

Request Body:

json
{
  "transactionHash": "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
}

Success Response (200):

json
{
  "successful": true,
  "hash": "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
  "ledger": 123456,
  "createdAt": "2024-03-07T12:00:00.000Z",
  "payments": [
    {
      "from": "GDPYEQVXKP7V...",
      "to": "GABC123...",
      "amount": "100.00",
      "assetCode": "USDC",
      "assetIssuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
    }
  ]
}

Response Fields:

FieldTypeDescription
successfulbooleanWhether transaction succeeded
hashstringTransaction hash
ledgernumberLedger number
createdAtstringISO timestamp
paymentsarrayList of payment operations
payments[].fromstringSender wallet
payments[].tostringRecipient wallet
payments[].amountstringPayment amount
payments[].assetCodestringAsset code
payments[].assetIssuerstring | undefinedAsset issuer

Error Responses:

StatusErrorDescription
400transactionHash requiredMissing hash in request body
404Transaction not foundInvalid or non-existent hash
500Failed to verify transactionServer error

Example:

bash
curl -X POST https://api.link2pay.dev/api/payments/verify-tx \
  -H "Content-Type: application/json" \
  -d '{
    "transactionHash": "7a8b9c0d..."
  }'

Usage Notes:

  • Independent verification endpoint
  • Not tied to invoices
  • Useful for debugging and reconciliation
  • Returns all payment operations in the transaction

Payment Lifecycle

Status Flow

DRAFT → PENDING → PROCESSING → PAID
  ↓        ↓           ↓
CANCELLED  EXPIRED    FAILED

Status Definitions:

StatusDescriptionCan Pay?
DRAFTInvoice created, not sentYes
PENDINGInvoice sent to clientYes
PROCESSINGPayment intent created, awaiting signatureYes (retry)
PAIDPayment confirmed on-chainNo
EXPIREDPassed due date without paymentNo
FAILEDPayment failedNo
CANCELLEDInvoice cancelled by ownerNo

Complete Payment Flow

typescript
// 1. Create payment intent
const intent = await fetch('/api/payments/invoice123/pay-intent', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    senderPublicKey: walletAddress,
    networkPassphrase: 'Test SDF Network ; September 2015'
  })
}).then(r => r.json());

// 2. Sign with Freighter
const signedXdr = await signTransaction(intent.transactionXdr, {
  networkPassphrase: intent.networkPassphrase
});

// 3. Submit payment
const result = await fetch('/api/payments/submit', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    invoiceId: 'invoice123',
    signedTransactionXdr: signedXdr
  })
}).then(r => r.json());

// 4. Poll for confirmation (or use watcher)
let status;
do {
  await new Promise(resolve => setTimeout(resolve, 5000));
  status = await fetch(`/api/payments/invoice123/status`).then(r => r.json());
} while (status.status === 'PROCESSING');

console.log('Payment complete:', status.transactionHash);

Error Handling

See API Errors for detailed error codes and handling strategies.

Common Payment Errors:

  • Network mismatch: Switch wallet to correct network
  • Insufficient balance: Add funds to wallet
  • No trustline: Add trustline for asset in wallet
  • Account not activated: Recipient needs 1 XLM to activate
  • Transaction timeout: Create new payment intent

Next Steps

Built on Stellar blockchain