Tracking & Webhooks

Monitor payment status in real-time via the Lookup API and receive webhook notifications when payment status changes.

Payment Lookup API

Query payment status by payment ID or your external ID.

Endpoint

Lookup endpointtext
GET /api/lookup/{id}

Authentication

Include your API key in the x-api-key header:

Requestbash
curl -X GET "https://surstrom.io/api/lookup/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "x-api-key: your-api-key"

Lookup Methods

Lookup TypeFormatDescription
Payment UUIDa1b2c3d4-e5f6-...Surstrom payment ID
External IDorder-123Your order reference passed during payment creation

External ID lookup only works if you provided an externalId when creating the payment via POST /api/payment/init.

Response

Lookup Responsejson
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "PAYMENT",
"createdAt": "2025-01-15T10:30:00.000Z",
"updatedAt": "2025-01-15T10:35:00.000Z",
"status": "COMPLETED",
"providerId": "provider-uuid",
"relayId": "relay-user-uuid",
"clientId": "your-user-uuid",
"externalId": "order-123",
"relayPage": "https://relay-site.com/checkout?id=a1b2c3d4...",
"suffix": "MYSHOP",
"suffixClient": "ORDER123",
"amount": 100.00,
"currency": "usd",
"fee": 7,
"send": 93.00,
"relayWallet": "RelayWalletPublicKey...",
"clientWallet": "YourWalletPublicKey...",
"txHash": "5UxG7jK2mN...",
"deliveryTime": "2025-01-15T10:35:00.000Z",
"card": "4242",
"cardholder": "John Doe",
"country": "US",
"bin": "424242",
"brand": "visa",
"cardFingerprint": "fp_abc123...",
"fraudScore": 12,
"ip": "192.168.1.1",
"theme": "light",
"sent": "TRUE"
}

Response Fields

FieldTypeDescription
idstringPayment UUID
typestringAlways "PAYMENT"
statusstringPayment status (see lifecycle below)
externalIdstringYour order reference (if provided)
providerIdstringProvider UUID
relayIdstringRelay user UUID
clientIdstringYour user UUID
amountnumberGross payment amount in major units
currencystringCurrency code (usd, eur, etc.)
feenumberTotal fee percentage (includes referral fees if applicable)
sendnumberAmount you receive after fees
relayPagestringCheckout page URL with payment ID
suffixstringStatement descriptor suffix
suffixClientstringYour statement descriptor suffix
relayWalletstringRelay's Solana wallet address
clientWalletstringYour Solana wallet address
txHashstringSolana transaction signature (when settled)
txErrorstringSettlement error message (if failed)
deliveryTimestringSettlement timestamp (ISO 8601)
cardstringLast 4 digits of card
cardholderstringName on card
countrystringCard issuing country (ISO code)
binstringCard BIN (first 6 digits)
brandstringCard brand (visa, mastercard, amex)
cardFingerprintstringUnique card fingerprint for deduplication
fraudScorenumberFraud risk score (0-100)
ipstringCustomer IP address
declinestringDecline reason from provider
sentstringSettlement status: TRUE, FALSE, or FAILED
errorDescstringError message if payment failed
exchangeRouteobjectCross-currency conversion details
createdAtstringPayment creation timestamp (ISO 8601)
updatedAtstringLast update timestamp (ISO 8601)
themestringPayment form theme (light or dark)

Client responses do not include provider, providerPaymentId, description, rawLog, or netAmount. These fields are only available to Relays.

Fee Adjustments

If your account has referral fees configured, the fee and send fields are adjusted:

  • fee = Relay fee + your referral fee percentage
  • send = Amount after Relay fee minus referral amount

This ensures the values reflect what you actually receive.

Error Responses

Not Foundjson
{
"error": "Payment not found"
}
Unauthorizedjson
{
"error": "API key required"
}

Webhooks

Receive HTTP POST notifications when payment status changes.

Configuration

Configure your webhook URL in the Settings page of your dashboard.

Webhook URL Requirements

The URL must:

  • Be publicly accessible (HTTPS recommended)
  • Return HTTP 200 to confirm receipt
  • Respond within 10 seconds during setup test

Private IP ranges (localhost, 10.x, 192.168.x, 172.16-31.x) are blocked for security.

Webhook Delivery

PropertyValue
MethodPOST
Content-Typeapplication/json
HeaderX-Payment-Event: payment.event
HeaderX-Signature: <HMAC-SHA256 of body using your API key>
RetriesNone (fire-and-forget)

Webhooks are best-effort delivery. Always use the Lookup API as the source of truth for payment status.

Webhook Signature Verification

Every webhook includes an X-Signature header containing the HMAC-SHA256 signature of the raw request body, using your API key as the secret. This lets you verify both the sender and that the body was not tampered with.

Verify Signature (Node.js)javascript
const crypto = require('crypto')

const API_KEY = process.env.SURSTROM_API_KEY

app.post('/webhook/surstrom', (req, res) => {
const signature = req.headers['x-signature']
const rawBody = JSON.stringify(req.body) // must match the raw bytes received

const expected = crypto
  .createHmac('sha256', API_KEY)
  .update(rawBody)
  .digest('hex')

if (signature !== expected) {
  return res.status(401).send('Invalid signature')
}

// Signature valid, process webhook...
res.status(200).send('OK')
})
Verify Signature (Python)python
import hmac
import hashlib

API_KEY = os.environ["SURSTROM_API_KEY"]

@app.route("/webhook/surstrom", methods=["POST"])
def webhook():
  signature = request.headers.get("X-Signature", "")
  raw_body = request.get_data(as_text=True)

  expected = hmac.new(
      API_KEY.encode(),
      raw_body.encode(),
      hashlib.sha256
  ).hexdigest()

  if not hmac.compare_digest(signature, expected):
      return "Invalid signature", 401

  # Signature valid, process webhook...
  return "OK", 200

Always verify the X-Signature header before processing webhooks to prevent spoofed requests. Use the raw request body (byte-for-byte, without reformatting) for signature verification.

Webhook Events

Webhooks are fired when:

  1. Payment created — Initial PENDING status
  2. Payment confirmed — Customer completed payment form, status changes to PROCESSING
  3. Settlement complete — USDC delivered to your wallet, status changes to COMPLETED
  4. Payment failed — Card declined or error, status changes to FAILED
  5. Payment canceled — Timeout or manual cancel, status changes to CANCELED
  6. Payment refunded — Refund processed, status changes to REFUNDED
  7. Payment disputed — Dispute opened, status changes to DISPUTED

Webhook Payload

Client Webhook Payloadjson
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "PAYMENT",
"createdAt": "2025-01-15T10:30:00.000Z",
"updatedAt": "2025-01-15T10:35:00.000Z",
"status": "COMPLETED",
"providerId": "provider-uuid",
"relayId": "relay-user-uuid",
"clientId": "your-user-uuid",
"externalId": "order-123",
"relayPage": "https://relay-site.com/checkout?id=a1b2c3d4...",
"suffix": "MYSHOP",
"suffixClient": "ORDER123",
"amount": 100.00,
"currency": "usd",
"fee": 7,
"send": 93.00,
"relayWallet": "RelayWalletPublicKey...",
"clientWallet": "YourWalletPublicKey...",
"txHash": "5UxG7jK2mN...",
"deliveryTime": "2025-01-15T10:35:00.000Z",
"card": "4242",
"cardholder": "John Doe",
"country": "US",
"bin": "424242",
"brand": "visa",
"cardFingerprint": "fp_abc123...",
"fraudScore": 12,
"ip": "192.168.1.1",
"theme": "light",
"sent": "TRUE"
}

Payload by Status

PENDING — Payment created, waiting for customer:

  • status: "PENDING"
  • sent: "FALSE"
  • No card details yet
  • Use relayPage URL to redirect customer to payment form

PROCESSING — Payment confirmed, awaiting settlement:

  • status: "PROCESSING"
  • sent: "FALSE"
  • Card details populated
  • USDC settlement in progress

COMPLETED — Settlement successful:

  • status: "COMPLETED"
  • sent: "TRUE"
  • txHash: Solana transaction signature
  • deliveryTime: Settlement timestamp
  • USDC has arrived in your wallet

FAILED — Payment failed:

  • status: "FAILED"
  • sent: "FALSE"
  • errorDesc: Failure reason (e.g., "Your card was declined")

CANCELED — Payment timed out or canceled:

  • status: "CANCELED"
  • sent: "FALSE"

REFUNDED — Payment was refunded:

  • status: "REFUNDED"
  • sent: "FALSE"
  • Original settlement may have been reversed

DISPUTED — Payment is under dispute:

  • status: "DISPUTED"
  • sent: varies (may be "TRUE" if settled before dispute)

CRASH — System error during processing:

  • status: "CRASH"
  • sent: "FALSE"
  • txError: Error details if settlement failed

Cross-Currency Payments

For payments converted from another currency:

Exchange Routejson
{
"exchangeRoute": {
  "originalValue": 100.00,
  "clientCurrency": "usd",
  "nativeCurrency": "eur",
  "rate": 0.92,
  "result": 92.00
}
}

Testing Webhooks

When you save a webhook URL, surstrom sends a test webhook to verify it works. The test payload:

Test Webhookjson
{
"event": "test",
"timestamp": "2025-01-15T10:30:00.000Z",
"userId": "your-user-uuid",
"message": "This is a test webhook from surstrom"
}

Your endpoint must return HTTP 200 for the webhook URL to be saved.

Handling Webhooks

Example webhook handler:

Webhook Handlerjavascript
app.post('/webhook/surstrom', (req, res) => {
const event = req.headers['x-payment-event']
const payload = req.body

// Test webhook
if (payload.event === 'test') {
  console.log('Test webhook received')
  return res.status(200).send('OK')
}

// Payment webhook
const { id, status, externalId, amount, currency, txHash, send } = payload

switch (status) {
  case 'PENDING':
    // Payment created - redirect customer to relayPage
    console.log(`Payment ${id} created for order ${externalId}`)
    break

  case 'PROCESSING':
    // Payment confirmed, settlement in progress
    console.log(`Payment ${id} confirmed for ${amount} ${currency}`)
    break

  case 'COMPLETED':
    // Settlement complete - fulfill the order
    console.log(`Payment ${id} settled. TX: ${txHash}`)
    console.log(`Received ${send} USDC`)
    await fulfillOrder(externalId)
    break

  case 'FAILED':
    console.log(`Payment ${id} failed: ${payload.errorDesc}`)
    await cancelOrder(externalId)
    break
}

res.status(200).send('OK')
})

Always return HTTP 200 quickly. Process webhooks asynchronously to avoid timeouts.

Payment Lifecycle

StatusDescriptionsentAction
PENDINGCreated, waiting for customerFALSERedirect to relayPage
PROCESSINGCustomer paid, awaiting settlementFALSEWait for settlement
COMPLETEDSettlement deliveredTRUEFulfill order
FAILEDPayment failedFALSEShow error, allow retry
CANCELEDTimed out or canceledFALSECancel order
CRASHSystem error (rare)FALSEContact support
REFUNDEDRefunded through providerFALSEProcess refund
DISPUTEDUnder disputevariesHandle dispute

Best Practices

Idempotency

You may receive the same webhook multiple times. Use the id and status fields to deduplicate:

Idempotent Handlerjavascript
const processedPayments = new Set()

app.post('/webhook/surstrom', (req, res) => {
const { id, status } = req.body
const key = `${id}:${status}`

if (processedPayments.has(key)) {
  return res.status(200).send('Already processed')
}

processedPayments.add(key)
// Process payment...

res.status(200).send('OK')
})

Verify with Lookup API

Verify Before Fulfillmentjavascript
app.post('/webhook/surstrom', async (req, res) => {
const { id, status, externalId } = req.body

// Acknowledge webhook immediately
res.status(200).send('OK')

// Verify status before fulfilling order
if (status === 'COMPLETED') {
  const payment = await fetch(`https://surstrom.io/api/lookup/${id}`, {
    headers: { 'x-api-key': API_KEY }
  }).then(r => r.json())

  if (payment.status === 'COMPLETED' && payment.sent === 'TRUE') {
    await fulfillOrder(externalId)
  }
}
})

Use External ID for Correlation

Always pass an externalId when creating payments to correlate webhooks with your orders:

Payment Creationjavascript
const payment = await fetch('https://surstrom.io/api/payment/init', {
method: 'POST',
headers: {
  'Content-Type': 'application/json',
  'x-api-key': API_KEY
},
body: JSON.stringify({
  amount: 99.99,
  maxFee: 10,
  currency: 'usd',
  externalId: 'order-12345'  // Your order ID
})
}).then(r => r.json())

// Later, look up by your order ID:
const status = await fetch('https://surstrom.io/api/lookup/order-12345', {
headers: { 'x-api-key': API_KEY }
}).then(r => r.json())

Async Processing

Process webhooks asynchronously to avoid timeouts:

Queue-Based Processingjavascript
app.post('/webhook/surstrom', (req, res) => {
// Respond immediately
res.status(200).send('OK')

// Queue for async processing
paymentQueue.add(req.body)
})

Rate Limits

EndpointLimit
Lookup API100 requests/minute
Webhook configurationSession authenticated