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
GET /api/lookup/{id}Authentication
Include your API key in the x-api-key header:
curl -X GET "https://surstrom.io/api/lookup/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "x-api-key: your-api-key"Lookup Methods
| Lookup Type | Format | Description |
|---|---|---|
| Payment UUID | a1b2c3d4-e5f6-... | Surstrom payment ID |
| External ID | order-123 | Your 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
{
"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
| Field | Type | Description |
|---|---|---|
id | string | Payment UUID |
type | string | Always "PAYMENT" |
status | string | Payment status (see lifecycle below) |
externalId | string | Your order reference (if provided) |
providerId | string | Provider UUID |
relayId | string | Relay user UUID |
clientId | string | Your user UUID |
amount | number | Gross payment amount in major units |
currency | string | Currency code (usd, eur, etc.) |
fee | number | Total fee percentage (includes referral fees if applicable) |
send | number | Amount you receive after fees |
relayPage | string | Checkout page URL with payment ID |
suffix | string | Statement descriptor suffix |
suffixClient | string | Your statement descriptor suffix |
relayWallet | string | Relay's Solana wallet address |
clientWallet | string | Your Solana wallet address |
txHash | string | Solana transaction signature (when settled) |
txError | string | Settlement error message (if failed) |
deliveryTime | string | Settlement timestamp (ISO 8601) |
card | string | Last 4 digits of card |
cardholder | string | Name on card |
country | string | Card issuing country (ISO code) |
bin | string | Card BIN (first 6 digits) |
brand | string | Card brand (visa, mastercard, amex) |
cardFingerprint | string | Unique card fingerprint for deduplication |
fraudScore | number | Fraud risk score (0-100) |
ip | string | Customer IP address |
decline | string | Decline reason from provider |
sent | string | Settlement status: TRUE, FALSE, or FAILED |
errorDesc | string | Error message if payment failed |
exchangeRoute | object | Cross-currency conversion details |
createdAt | string | Payment creation timestamp (ISO 8601) |
updatedAt | string | Last update timestamp (ISO 8601) |
theme | string | Payment 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 percentagesend= Amount after Relay fee minus referral amount
This ensures the values reflect what you actually receive.
Error Responses
{
"error": "Payment not found"
}{
"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
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Header | X-Payment-Event: payment.event |
| Header | X-Signature: <HMAC-SHA256 of body using your API key> |
| Retries | None (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.
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')
})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", 200Always 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:
- Payment created — Initial
PENDINGstatus - Payment confirmed — Customer completed payment form, status changes to
PROCESSING - Settlement complete — USDC delivered to your wallet, status changes to
COMPLETED - Payment failed — Card declined or error, status changes to
FAILED - Payment canceled — Timeout or manual cancel, status changes to
CANCELED - Payment refunded — Refund processed, status changes to
REFUNDED - Payment disputed — Dispute opened, status changes to
DISPUTED
Webhook Payload
{
"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
relayPageURL 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 signaturedeliveryTime: 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:
{
"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:
{
"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:
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
| Status | Description | sent | Action |
|---|---|---|---|
PENDING | Created, waiting for customer | FALSE | Redirect to relayPage |
PROCESSING | Customer paid, awaiting settlement | FALSE | Wait for settlement |
COMPLETED | Settlement delivered | TRUE | Fulfill order |
FAILED | Payment failed | FALSE | Show error, allow retry |
CANCELED | Timed out or canceled | FALSE | Cancel order |
CRASH | System error (rare) | FALSE | Contact support |
REFUNDED | Refunded through provider | FALSE | Process refund |
DISPUTED | Under dispute | varies | Handle dispute |
Best Practices
Idempotency
You may receive the same webhook multiple times. Use the id and status fields to deduplicate:
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
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:
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:
app.post('/webhook/surstrom', (req, res) => {
// Respond immediately
res.status(200).send('OK')
// Queue for async processing
paymentQueue.add(req.body)
})Rate Limits
| Endpoint | Limit |
|---|---|
| Lookup API | 100 requests/minute |
| Webhook configuration | Session authenticated |