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:
- Email lookup — check existing verification status
- Identity verification — Didit KYC (ID scan + liveness)
- Accreditation — investor self-certifies their basis (income, net worth, etc.)
- Holdings display — shows the investor their positions across all ioniqx offerings they hold
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.
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
- Sign in to the ioniqx portal and complete company KYC if you haven't already.
- Open the Portal menu in the top navigation bar and click Developers.
- 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:
pk_live_…— production key issued on the live ioniqx platformpk_test_…— non-production key, used in development and staging environments
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.
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.
pk_live_… or pk_test_…).true, the widget overlay opens immediately after init().
Default: false.
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.
GET /embed/lookup./embed/callback when done.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.
WalletWhitelistJob
to register the wallet on-chain.
Step 2 — Accreditation
Accreditation is required when both of the following are true:
- The investor's KYC is approved
- The API customer is linked to an investment company and the investor does not
yet have an active
AccreditedCertificationfor that company
The investor selects the basis that applies to them:
| Basis | Description |
|---|---|
income_200k | Individual income over $200k in each of the past two years |
income_300k_joint | Joint income over $300k in each of the past two years |
net_worth_1m | Individual or joint net worth over $1M (excluding primary residence) |
licensed_broker | Holds a Series 7, 65, or 82 license in good standing |
entity_5m | Entity with assets over $5M not formed for this investment |
The self-certification is recorded as an AccreditedCertification with:
- Status:
approvedimmediately (investor is their own certifier under Reg D 506(c) self-certification) - Expiry: 12 months from the attestation date
/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:
- Shows a success card ("Verified!")
- Posts
{ type: "SAAS_COMPLETE", token: "…" }to the parent window viapostMessage - 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.
| Field | Type | Description |
|---|---|---|
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_received | string | Sum of all paid USDC distribution allocations |
acquired_at | ISO 8601 | Date 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"
}
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.
| Event | Trigger |
|---|---|
investor.kyc_approved | KYC verification approved by Didit |
investor.kyc_rejected | KYC verification rejected |
investor.accreditation_granted | Accredited certification created |
investor.distribution_announced | Distribution 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.type | When fired | Payload |
|---|---|---|
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:
- Invalid API key — the key is malformed or belongs to an inactive customer. The embed returns HTTP 401.
- Origin not in allowlist — your page's origin is not listed in the customer's allowed domains. The embed returns HTTP 403.
- Script not loaded —
SaasWidgetis undefined. Ensure the<script>tag loads before theinit()call, or use thedeferattribute and wrap your init call in aDOMContentLoadedlistener.
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.