Skip to main content

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"
}
FieldTypeDescription
typestringAlways "tidely:ready".
versionstringProtocol 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..."
}
FieldTypeDescription
typestringAlways "tidely:auth".
initTokenstringThe 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."
}
FieldTypeDescription
typestringAlways "tidely:auth-error".
codestringStable error code — see table below.
messagestringHuman-readable description. Useful for debugging; do not parse for logic.
codeWhen
MISSING_TOKENParent sent tidely:auth without an initToken.
INVALID_TOKENToken signature invalid, malformed, or claims not for this frontend.
EXPIRED_TOKENToken is past its exp. Mint a new one.
TOKEN_ALREADY_USEDInit tokens are single-use; this one was already exchanged.
USER_INACTIVEUser became inactive between mint and exchange.
INTERNAL_ERRORUnexpected 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 targetOrigin to the exact Tidely origin (e.g. https://app.tidely.com). Never call postMessage(..., '*') — 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.origin on 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:auth from 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.