Integration
Embed the Relay's payment page in an iframe on your site and listen for payment completion via postMessage.
Payment Flow
- Your backend calls
POST /api/payment/initto create a payment - The response includes a
relayPageURL (e.g.,https://relay-site.com/checkout?id=xxx) - Embed this URL in an iframe on your site
- The Relay's page has the surstrom SDK installed
- The SDK mounts a payment form
- Customer fills in card details and pays
- On success, the SDK sends a
postMessageto the parent window with a proof token
Creating a Payment
curl -X POST https://surstrom.io/api/payment/init \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"amount": 50.00,
"maxFee": 10,
"currency": "usd",
"externalId": "order-123"
}'The response includes a relayPage URL that you embed in an iframe.
Embedding the iframe
The payment form loads inside the iframe. Since the form takes a moment to initialize (loading Stripe.js, mounting card fields), start the iframe hidden and reveal it when the form is ready. This prevents the customer from seeing a blank frame or loading flicker.
<!-- Start hidden. The SDK will notify us when the form is ready -->
<iframe
id="payment-frame"
src="RELAY_PAGE_URL_FROM_RESPONSE"
style="width: 100%; height: 600px; border: none; opacity: 0; transition: opacity 0.3s ease;"
allow="payment"
></iframe>You can show your own spinner or skeleton next to the iframe while it loads, then hide it together with the opacity reveal.
postMessage Events
The SDK inside the iframe communicates with your parent window via window.postMessage. There are three events:
payment-ready
Fired once when the payment form has fully mounted and is ready for customer input. The card number, expiry, and CVC fields are visible and interactive.
"payment-ready"Use this event to reveal the iframe and hide any loading indicators:
window.addEventListener('message', function(event) {
if (event.data === 'payment-ready') {
// Form is mounted — reveal the iframe
document.getElementById('payment-frame').style.opacity = '1';
// Hide your own loading spinner if you have one
var loader = document.getElementById('payment-loader');
if (loader) loader.style.display = 'none';
}
});payment-success
Fired when the payment is confirmed by the provider. Contains a proof hash and ts timestamp for client-side verification.
{
"type": "payment-success",
"proof": "a1b2c3d4e5f6...64-char-sha256-hex",
"ts": 1705312500000
}| Field | Type | Description |
|---|---|---|
proof | string | SHA-256 hex hash for client-side verification |
ts | number | Unix timestamp (ms) when the proof was generated |
payment-error
Fired when a payment attempt fails (declined card, insufficient funds, etc.). The customer can retry — the form stays active.
{
"type": "payment-error",
"message": "Your card was declined."
}Error events are not final. The payment remains active and the customer can retry with a different card. Do not close the iframe on error.
Full Example
Complete integration with iframe, loading state, all three events, and proof validation:
<div id="payment-container" style="position: relative; max-width: 400px;">
<!-- Loading indicator shown while form mounts -->
<div id="payment-loader" style="text-align: center; padding: 40px;">
Loading payment form...
</div>
<!-- iframe starts invisible -->
<iframe
id="payment-frame"
src="RELAY_PAGE_URL"
style="width: 100%; height: 500px; border: none; opacity: 0; transition: opacity 0.3s ease;"
allow="payment"
></iframe>
</div>
<script>
var PROVIDER_ID = 'YOUR_PROVIDER_ID'; // from /api/payment/init response
var EXTERNAL_ID = 'order-123'; // your order reference
window.addEventListener('message', function(event) {
var data = event.data;
// Form is ready — reveal iframe, hide loader
if (data === 'payment-ready') {
document.getElementById('payment-frame').style.opacity = '1';
document.getElementById('payment-loader').style.display = 'none';
return;
}
if (!data || !data.type) return;
// Payment succeeded — verify proof and fulfill order
if (data.type === 'payment-success') {
verifyProof(data.proof, data.ts).then(function(valid) {
if (valid) {
// Proof verified — payment is confirmed
// Redirect to thank-you page, update UI, etc.
window.location.href = '/order-complete?id=' + EXTERNAL_ID;
}
});
}
// Payment failed — customer can retry, just log or show message
if (data.type === 'payment-error') {
console.log('Payment error:', data.message);
// Do NOT close the iframe — customer can retry
}
});
// Verify the proof token client-side
async function verifyProof(proof, ts) {
var input = PROVIDER_ID + ':' + EXTERNAL_ID + ':' + ts;
var buf = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(input)
);
var hash = Array.from(new Uint8Array(buf))
.map(function(b) { return b.toString(16).padStart(2, '0'); })
.join('');
return hash === proof;
}
</script>Proof Token Validation
The proof token is a SHA-256 hash that you can verify client-side. The hash is computed from:
SHA256(providerId + ":" + externalId + ":" + ts)providerId— returned in thePOST /api/payment/initresponseexternalId— your order reference that you passed when creating the paymentts— timestamp from thepayment-successevent
What Proof Validation Guarantees
The proof token is only returned after the backend verifies with the payment provider that the payment succeeded. This means:
- Backend confirms with the payment provider that the charge went through
- Only then the proof token is generated and sent via postMessage
- If proof validation passes on your frontend, the payment is confirmed successful
Recommended Flow
- Receive
payment-successpostMessage withproofandts - Verify the proof client-side using the formula above
- If valid: The payment succeeded — you can immediately update your UI and fulfill the order
- Settlement (USDC transfer) happens asynchronously after payment confirmation
A validated proof token guarantees the payment provider confirmed the transaction. You can safely fulfill orders immediately after successful proof validation. Settlement to your wallet happens automatically in the background.
The API key is used for server-to-server communication. Never expose it in frontend code.