Integration

Embed the Relay's payment page in an iframe on your site and listen for payment completion via postMessage.

Payment Flow

  1. Your backend calls POST /api/payment/init to create a payment
  2. The response includes a relayPage URL (e.g., https://relay-site.com/checkout?id=xxx)
  3. Embed this URL in an iframe on your site
  4. The Relay's page has the surstrom SDK installed
  5. The SDK mounts a payment form
  6. Customer fills in card details and pays
  7. On success, the SDK sends a postMessage to the parent window with a proof token

Creating a Payment

POST /api/payment/initbash
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.

Iframe embedhtml
<!-- 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-readytext
"payment-ready"

Use this event to reveal the iframe and hide any loading indicators:

Listening for payment-readyjavascript
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.

payment-success payloadjson
{
"type": "payment-success",
"proof": "a1b2c3d4e5f6...64-char-sha256-hex",
"ts": 1705312500000
}
FieldTypeDescription
proofstringSHA-256 hex hash for client-side verification
tsnumberUnix 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.

payment-error payloadjson
{
"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:

Full integration examplehtml
<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:

Proof formulatext
SHA256(providerId + ":" + externalId + ":" + ts)
  • providerId — returned in the POST /api/payment/init response
  • externalId — your order reference that you passed when creating the payment
  • ts — timestamp from the payment-success event

What Proof Validation Guarantees

The proof token is only returned after the backend verifies with the payment provider that the payment succeeded. This means:

  1. Backend confirms with the payment provider that the charge went through
  2. Only then the proof token is generated and sent via postMessage
  3. If proof validation passes on your frontend, the payment is confirmed successful

Recommended Flow

  1. Receive payment-success postMessage with proof and ts
  2. Verify the proof client-side using the formula above
  3. If valid: The payment succeeded — you can immediately update your UI and fulfill the order
  4. 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.