Frontend Messaging
On the client side, the embedded integration consists of a small set of structured
postMessage
events exchanged between the partner's parent page and the embedded Tidely iframe. These
messages are how the init token reaches Tidely's frontend and how the
iframe reports back whether sign-in succeeded.
All messages are JSON objects with a type field prefixed by tidely:. Future versions
may add more fields; treat unknown fields as opaque and unknown type values as no-ops.
Message Flow
Messages
1. tidely:ready — iframe → parent
Sent by the iframe once after the Tidely frontend has loaded and is ready to receive
an init token. The parent should wait for this message before posting tidely:auth —
sending the token earlier means the iframe may not yet have its message listener
attached.
{
"type": "tidely:ready",
"version": "1"
}
| Field | Type | Description |
|---|---|---|
type | string | Always "tidely:ready". |
version | string | Protocol version. Currently "1". New versions are additive. |
2. tidely:auth — parent → iframe
The parent sends this message to deliver the init token to the iframe. Always set
targetOrigin to the exact iframe origin — see Security.
{
"type": "tidely:auth",
"initToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
| Field | Type | Description |
|---|---|---|
type | string | Always "tidely:auth". |
initToken | string | The init token returned by POST /partner/authentication/sso/init. |
The iframe will then exchange the init token for an access + refresh token pair on Tidely's backend. The init token is consumed in the process and cannot be reused.
3. tidely:auth-success — iframe → parent
Sent when the init token has been successfully exchanged and the user is fully logged in inside the iframe. From this point on, the iframe operates as a normal authenticated Tidely session — refresh tokens, route changes, and API calls happen entirely inside the iframe.
{
"type": "tidely:auth-success"
}
4. tidely:auth-error — iframe → parent
Sent when the iframe could not complete sign-in. The most common cause is an init token that was already consumed, expired, or that did not validate.
{
"type": "tidely:auth-error",
"code": "INVALID_TOKEN",
"message": "The init token is invalid or has expired."
}
| Field | Type | Description |
|---|---|---|
type | string | Always "tidely:auth-error". |
code | string | Stable error code — see table below. |
message | string | Human-readable description. Useful for debugging; do not parse for logic. |
code | When |
|---|---|
MISSING_TOKEN | Parent sent tidely:auth without an initToken. |
INVALID_TOKEN | Token signature invalid, malformed, or claims not for this frontend. |
EXPIRED_TOKEN | Token is past its exp. Mint a new one. |
TOKEN_ALREADY_USED | Init tokens are single-use; this one was already exchanged. |
USER_INACTIVE | User became inactive between mint and exchange. |
INTERNAL_ERROR | Unexpected error inside the iframe / Tidely backend during exchange. |
The iframe will not retry on its own. The parent should respond by minting a fresh init
token and reloading the iframe (or sending a new tidely:auth message after a
tidely:ready).
New codes may be added in the future. Treat unknown code values as a generic failure.
Putting It Together — Parent Page Example
<iframe
id="tidely-iframe"
src="https://app.tidely.com/"
style="width: 100%; height: 800px; border: 0;"
allow="clipboard-read; clipboard-write"
></iframe>
<script>
const TIDELY_ORIGIN = 'https://app.tidely.com';
const iframe = document.getElementById('tidely-iframe');
// 1. Fetch an init token from your own backend.
// Your backend calls POST /partner/authentication/sso/init server-side.
async function fetchInitToken() {
const res = await fetch('/api/tidely/init-token', { method: 'POST' });
if (!res.ok) throw new Error('Could not mint init token');
const { initToken } = await res.json();
return initToken;
}
// 2. Listen for messages from the iframe.
window.addEventListener('message', async (event) => {
// Always check the origin first.
if (event.origin !== TIDELY_ORIGIN) return;
const msg = event.data;
if (!msg || typeof msg !== 'object') return;
switch (msg.type) {
case 'tidely:ready': {
const initToken = await fetchInitToken();
iframe.contentWindow.postMessage(
{ type: 'tidely:auth', initToken },
TIDELY_ORIGIN, // pin the targetOrigin — never use '*'
);
break;
}
case 'tidely:auth-success':
console.log('Tidely sign-in succeeded');
break;
case 'tidely:auth-error':
console.error('Tidely sign-in failed:', msg.code, msg.message);
// Optionally re-mint and retry by reloading the iframe.
break;
}
});
</script>
Security
The browser's postMessage API is intentionally permissive: any frame can send messages
to any other frame. To keep the init token safe, both sides apply the following rules.
On the parent
- Always set
targetOriginto the exact Tidely origin (e.g.https://app.tidely.com). Never callpostMessage(..., '*')— that posts the init token to whichever origin currently occupies the iframe, which may not be Tidely if a navigation has happened. - Always check
event.originon incoming messages. Drop anything that doesn't come from the Tidely origin. - Do not log or persist init tokens anywhere on the client. Treat them as bearer tokens.
On the Tidely side
- The iframe only accepts
tidely:authfrom origins that have been allow-listed for your partner. If your parent page is served from an origin that has not been registered, the message is dropped silently. Send your origin(s) to partnerships@tidely.com. - The iframe only emits messages to its parent's origin. Nested iframes or unrelated windows do not receive Tidely messages.
- The init token is validated server-side by Tidely's backend on exchange — signature, audience, expiry, and one-time-use are all enforced there, not in the iframe.
Content Security Policy
If your parent page sets a Content Security Policy, make sure it allows framing the Tidely app:
frame-src 'self' https://app.tidely.com https://app.sam.tidely.com;
Tidely sets frame-ancestors headers that restrict who can embed app.tidely.com. If
your origin is not allow-listed, the browser will refuse to render the iframe — contact
partnerships@tidely.com to register it.
Versioning
The tidely:ready message carries a version field. The current protocol version is
"1". Future revisions will:
- never rename or remove existing message types or fields,
- add new fields and new message types as needed.
So an integration written against version 1 will continue to work as the protocol
evolves; you can opt into newer features by handling the new message types when you are
ready.