ioniqx / Embed Widget

Embed Widget

Drop investor KYC, accreditation, and holdings into any website with two lines of JavaScript.

Overview

The ioniqx embed widget is a hosted iframe that your site loads via a single <script> tag — modelled on Stripe.js. It handles the full investor verification lifecycle:

When the flow completes the widget posts a short-lived signed result token to your page via window.postMessage. You verify this token server-side to confirm the investor's identity and grant them access to your application.

ℹ️
The iframe is served from the ioniqx domain. Your site only ever receives a signed token — no raw KYC data or personal information crosses into your JavaScript.

Quick start

Three steps to get a working embed on your site:

1. Add the script tag

<script src="https://staging.ioniqx.io/widget.js"></script>

Place it anywhere in your HTML — the script is CDN-cached and has no dependencies.

2. Initialise the widget

SaasWidget.init({
  apiKey:    "pk_live_…",
  onSuccess: function (token) {
    // send `token` to your server to verify
    fetch("/verify-investor", {
      method:  "POST",
      headers: { "Content-Type": "application/json" },
      body:    JSON.stringify({ token })
    }).then(function () {
      // close the widget once your server has confirmed the investor
      window.SaasWidget.close();
    });
  }
});

3. Open the widget

// Attach to a button, or set autoOpen: true in init()
document.getElementById("verify-btn").addEventListener("click", function () {
  SaasWidget.open();
});

Get an API key

Every investment company on ioniqx gets a publishable API key that identifies them when loading the embed widget. Keys are self-served — any company admin or manager can find theirs in the portal under Portal → Developers (/portal/developers). No request to ioniqx support is required.

Finding your key

  1. Sign in to the ioniqx portal and complete company KYC if you haven't already.
  2. Open the Portal menu in the top navigation bar and click Developers.
  3. Your publishable API key is displayed at the top of the page. Click Copy to copy it to the clipboard.

Key formats

Keys are prefixed to indicate the environment they belong to:

A key is auto-provisioned the first time you visit the Developers page — you do not need to create one manually.

Regenerating a key

If a key is compromised or you need to rotate it, click Regenerate on the Developers page. The old key is invalidated immediately and all embeds using it will stop loading until updated. Only company admins and managers can regenerate keys.

Allowed embed domains

The widget will refuse to load on any origin that is not listed in your allowed domains. Configure the domains on the Developers page — one per line, without a protocol prefix (e.g. example.com, not https://example.com). Subdomains are automatically permitted: adding example.com also allows app.example.com.

⚠️
The pk_… key is safe to embed in client-side JavaScript because it is a publishable key — it only identifies your customer account and does not grant server-side access. Keep your server-side credentials (webhook secret, API credentials) out of the browser.

Installation

The widget script is served from /widget.js on the ioniqx app domain. It sets up the global window.SaasWidget object and registers a message listener for the SAAS_COMPLETE postMessage event.

<!-- Place before closing </body> or in <head> -->
<script src="https://staging.ioniqx.io/widget.js" defer></script>

The script is HTTP-cached for one hour. It has no external dependencies and does not read cookies or local storage.

SaasWidget.init(options)

Creates the overlay and iframe. Must be called before open(). Call it once on page load — calling init() a second time is a no-op.

apiKey
string
required
Your publishable API key (pk_live_… or pk_test_…).
onSuccess
function(token)
required
Called when the investor completes the full verification flow. Receives a short-lived signed result token (5-minute TTL). Send this token to your server for verification before granting access.
autoOpen
boolean
optional
If true, the widget overlay opens immediately after init(). Default: false.
closeOnOverlay
boolean
optional
If false, clicking the dimmed overlay backdrop will not close the widget. Default: true.

Returns the SaasWidget object (chainable).

SaasWidget.open()

Shows the overlay. The iframe is already mounted — this just sets display:flex.

SaasWidget.open();

SaasWidget.close()

Hides the overlay without destroying the iframe. The iframe retains its state if the user re-opens the widget in the same page session.

SaasWidget.close();

The widget does not close automatically after verification completes — call window.SaasWidget.close() from your onSuccess callback once you are ready to dismiss it (e.g. after your server confirms the token). If you want the investor to stay in the widget to review their holdings, simply omit the call.

Verification flow overview

Every investor goes through up to three steps. The widget detects existing state and skips steps the investor has already completed.

1
Email lookup
Investor enters their email. The widget checks their current verification status via GET /embed/lookup.
2
KYC
If not yet verified, the iframe navigates to Didit's hosted flow (ID scan + selfie liveness). Didit redirects back to /embed/callback when done.
if needed
Accreditation
If the customer is linked to an investment company and the investor is not yet accredited for it, they self-certify their basis (income, net worth, etc.).
3
Complete
SAAS_COMPLETE fires via postMessage. The investor's holdings are displayed. Your onSuccess callback receives the signed token.

Step 1 — KYC

When the email lookup returns needs_kyc, the widget shows a "Start Verification" button. Clicking it navigates the iframe to Didit's hosted identity verification flow. The investor scans their government-issued ID and completes a liveness check without leaving the overlay.

Didit redirects the iframe back to /embed/callback when the session ends. The widget then polls GET /embed/poll every 3 seconds until the ioniqx webhook processor receives the Didit result and updates the investor's kyc_status.

ℹ️
KYC approval triggers an automated email to the investor (Your identity verification is complete) and, if the investor has a primary wallet registered, queues a WalletWhitelistJob to register the wallet on-chain.

Step 2 — Accreditation

Accreditation is required when both of the following are true:

The investor selects the basis that applies to them:

BasisDescription
income_200kIndividual income over $200k in each of the past two years
income_300k_jointJoint income over $300k in each of the past two years
net_worth_1mIndividual or joint net worth over $1M (excluding primary residence)
licensed_brokerHolds a Series 7, 65, or 82 license in good standing
entity_5mEntity with assets over $5M not formed for this investment

The self-certification is recorded as an AccreditedCertification with:

⚠️
Self-certification meets the standard for Reg D 506(c) investor verification via questionnaire. For offerings where third-party verification is required, a portal employee must confirm the certification via the admin panel at /portal/investors/:id/accreditation.

On successful accreditation the widget fires an email to the investor (Accredited investor status confirmed) and proceeds to the Complete step.

Step 3 — Complete

When the investor is fully verified the widget:

  1. Shows a success card ("Verified!")
  2. Posts { type: "SAAS_COMPLETE", token: "…" } to the parent window via postMessage
  3. Fetches and renders the investor's holdings (see below)

Your onSuccess callback fires with the signed result token. The widget does not automatically close — the investor can review their holdings before dismissing it.

Holdings display

After the verification completes, the widget fetches GET /embed/holdings?token=… and renders a summary table if the investor has any cap table positions.

Holdings are shown across all ioniqx investment companies — not scoped to the company linked to the current API key. This lets an investor see their full portfolio in any embed they use.

FieldTypeDescription
project_name string Title of the investment project
token_amount integer Number of tokens held
ownership_pct string Ownership percentage (up to 6 decimal places)
cost_basis string Total cost basis in USDC
distributions_receivedstring Sum of all paid USDC distribution allocations
acquired_at ISO 8601Date the initial position was opened

The holdings fetch is non-blocking — SAAS_COMPLETE fires before the fetch resolves. If the fetch fails, the complete card still shows; the table is simply omitted.

Token verification (server side)

The result token is a signed Rails message verifier payload — it is not a JWT. Verify it on your server by calling the ioniqx token verification endpoint.

Request

POST https://staging.ioniqx.io/embed/verify_token
Authorization: Bearer <your_server_secret>
Content-Type:  application/json

{ "token": "<result_token_from_postMessage>" }

Response

{
  "valid":    true,
  "user_id":  42,
  "email":    "investor@example.com",
  "kyc_status": "approved",
  "expires_at": "2026-06-18T14:30:00Z"
}
⚠️
Tokens expire after 5 minutes. Always verify server-side immediately after receiving the SAAS_COMPLETE event. Never store the raw token — store the user_id or your own session identifier instead.

Webhooks

ioniqx sends webhook events to your configured endpoint when investor status changes. All events are signed with HMAC-SHA256 using your webhook secret.

EventTrigger
investor.kyc_approvedKYC verification approved by Didit
investor.kyc_rejectedKYC verification rejected
investor.accreditation_grantedAccredited certification created
investor.distribution_announcedDistribution allocation created for investor

Signature verification

# Ruby example
expected = OpenSSL::HMAC.hexdigest(
  "SHA256",
  ENV["IONIQX_WEBHOOK_SECRET"],
  request.body.read
)
raise "Invalid signature" unless
  ActiveSupport::SecurityUtils.secure_compare(
    expected,
    request.headers["X-Ioniqx-Signature"]
  )

postMessage events

The widget communicates with the host page exclusively via window.postMessage. Always verify event.origin matches the ioniqx app domain before acting on any message.

event.data.typeWhen firedPayload
SAAS_COMPLETE Investor has completed the full verification flow { type, token }
ℹ️
SaasWidget.init() already installs the message listener and calls your onSuccess callback for you. Only set up your own listener if you need to handle the event before the widget's built-in handler runs.

CSP & iframe security

The ioniqx embed uses a strict frame-ancestors Content Security Policy. The widget iframe will only render on origins that are explicitly listed in the customer's allowed-domains list. Any other origin receives an HTTP 403 response.

If your site uses a Content-Security-Policy header, you must allow the ioniqx app domain as an iframe source:

Content-Security-Policy: frame-src https://staging.ioniqx.io

The widget iframe requests the identity-credentials-get permission policy, which is required for some Didit verification flows. No other permissions are requested.

Troubleshooting

Widget does not open

Check the browser console for errors. The most common causes are:

Investor email returns "not found"

The investor must have an account in ioniqx before using the embed. Accounts are created when a portal employee adds the investor to a company and sends them a KYC verification link via POST /portal/investors/:id/kyc.

Accreditation step not appearing

The accreditation step only appears when the API customer is associated with an investment company. Your API key is always scoped to your company — confirm at Portal → Developers that the Developers page loads without error (which confirms the company association is in place). If no company is linked to your customer record, contact ioniqx support. If a company is linked but the step still does not appear, the investor may already have an active accreditation for that company — the embed skips accreditation and goes directly to Complete in that case.

onSuccess token is expired

Result tokens have a 5-minute TTL. If there is a delay between the user completing verification and your server receiving the token (e.g. the user left the page open), the token will be rejected. Ask the investor to open the widget again — since they are already fully verified, the embed will complete immediately and issue a fresh token.

Holdings table not showing

Holdings only appear if the investor has at least one CapTableEntry record in ioniqx. A newly onboarded investor will see no holdings until a company employee issues tokens to them via the cap table management interface.