Embedding TakiFlo

Put TakiFlo's workflows, jobs and editors inside your own application as authenticated iframes. Your users stay in your product; the work happens in theirs.

How it works

A TakiFlo session cannot travel into a frame on your domain. Browsers partition localStorage by top-level site, and third-party cookies are blocked in Safari and Firefox and being removed in Chrome. Somebody signed into TakiFlo in another tab is not signed in inside your embed, and no amount of code changes that.

So authentication is handed in. Your backend, which already knows who is looking at the page, asks TakiFlo for a short-lived token for that specific person. The frame trades it for a session it holds in memory.

  your backend  ──POST /api/embed/session (API key + externalId)──►  TakiFlo
                ◄────────── single-use token, 60 second TTL ───────────

  your page     ──►  <iframe src="https://app.takiflo.com/embed/jobs?t=…&tenant=…">

  the frame     ──POST /api/embed/exchange──►  session JWT, held in memory only

  the frame     ──postMessage: takiflo:session-expiring──►  your page
  your page     ──postMessage: takiflo:session ────────────►  the frame

Your API key stays on your server. The token that reaches the browser is single-use and lives about a minute, because it travels in a URL, the least private place a credential can sit.

Before you start

Create an API key. In TakiFlo, go to API keys and create one. Store it wherever you keep server-side secrets.

Register the origin you will embed from. TakiFlo refuses to be framed by anything you have not listed. An origin is a scheme, host and port, like https://app.example.com. Wildcards and paths are rejected.

curl -X POST https://app.takiflo.com/api/embed/origins \
  -H "Authorization: Bearer <your TakiFlo admin token>" \
  -H "Content-Type: application/json" \
  -d '{"origin": "https://app.example.com"}'

Add every origin you embed from, including staging and local development such as http://localhost:3000.

1. Broker a session from your backend

Add one route to your own application. It takes whoever is signed into your product and asks TakiFlo for a token for them. This is the whole integration.

app.get("/api/takiflo-session", async (req, res) => {
  const user = req.user; // however you identify people

  const r = await fetch("https://app.takiflo.com/api/embed/session", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-TakiFlo-Api-Key": process.env.TAKIFLO_API_KEY,
    },
    body: JSON.stringify({
      externalId: user.id, // YOUR id for this person, stable forever
      email: user.email,
      displayName: user.name, // optional
      role: "User", // 'User' or 'Admin'
    }),
  });

  const body = await r.json();
  if (!r.ok) return res.status(r.status).json(body);

  // Only these two reach the browser.
  res.json({ token: body.token, tenantId: body.tenantId });
});
@app.get("/api/takiflo-session")
def takiflo_session():
    user = current_user()

    r = requests.post(
        "https://app.takiflo.com/api/embed/session",
        headers={"X-TakiFlo-Api-Key": os.environ["TAKIFLO_API_KEY"]},
        json={
            "externalId": user.id,          # YOUR id for this person, stable forever
            "email": user.email,
            "displayName": user.name,       # optional
            "role": "User",                 # 'User' or 'Admin'
        },
        timeout=10,
    )
    body = r.json()
    if not r.ok:
        return body, r.status_code

    return {"token": body["token"], "tenantId": body["tenantId"]}
[HttpGet("/api/takiflo-session")]
public async Task<IActionResult> TakiFloSession()
{
    var user = CurrentUser();

    using var request = new HttpRequestMessage(
        HttpMethod.Post, "https://app.takiflo.com/api/embed/session");
    request.Headers.Add("X-TakiFlo-Api-Key", _config["TakiFlo:ApiKey"]);
    request.Content = JsonContent.Create(new
    {
        externalId = user.Id,       // YOUR id for this person, stable forever
        email = user.Email,
        displayName = user.Name,    // optional
        role = "User"               // "User" or "Admin"
    });

    var response = await _http.SendAsync(request);
    var body = await response.Content.ReadFromJsonAsync<JsonElement>();
    if (!response.IsSuccessStatusCode) return StatusCode((int)response.StatusCode, body);

    return Ok(new
    {
        token = body.GetProperty("token").GetString(),
        tenantId = body.GetProperty("tenantId").GetString()
    });
}

2. Render the iframe

Fetch a token from your own route, then point an iframe at TakiFlo. The token goes in t, and tenant tells TakiFlo whose origin list to check before it agrees to be framed at all.

<iframe id="takiflo" title="TakiFlo" style="width:100%;border:0;height:400px"></iframe>

<script>
  const TAKIFLO = "https://app.takiflo.com";

  async function loadTakiFlo(screen = "jobs", recordId) {
    const r = await fetch("/api/takiflo-session");
    const { token, tenantId } = await r.json();

    document.getElementById("takiflo").src =
      `${TAKIFLO}/embed/${screen}?t=${encodeURIComponent(token)}` +
      `&tenant=${encodeURIComponent(tenantId)}` +
      (recordId ? `&id=${encodeURIComponent(recordId)}` : "");
  }

  loadTakiFlo();
</script>

Fetch the token immediately before setting src. It expires in 60 seconds, so one fetched at page load and used after the customer reads a paragraph will already be dead.

3. Handle messages

Two messages need answering: the frame telling you how tall it wants to be, and the frame telling you its session is about to run out.

const TAKIFLO_ORIGIN = new URL(TAKIFLO).origin;
const frame = document.getElementById("takiflo");

window.addEventListener("message", async (event) => {
  // Check the origin FIRST, before looking at the message at all.
  if (event.origin !== TAKIFLO_ORIGIN) return;

  const message = event.data;
  if (!message || typeof message.type !== "string") return;

  switch (message.type) {
    case "takiflo:ready":
      // Mounted and rendered.
      break;

    case "takiflo:height":
      frame.style.height = message.px + "px";
      break;

    case "takiflo:session-expiring": {
      // Fetch another token from YOUR backend and hand it in.
      const r = await fetch("/api/takiflo-session");
      const { token } = await r.json();
      frame.contentWindow.postMessage(
        { type: "takiflo:session", token },
        TAKIFLO_ORIGIN, // never "*"
      );
      break;
    }

    case "takiflo:error":
      console.error("TakiFlo:", message.code, message.message);
      break;
  }
});

Embeddable screens

PathShowsNeeds
/embed/jobsEvery job in the account. Selecting one opens its editor.Jobs: read
/embed/workflowsEvery workflow. Selecting one opens its editor.Workflows: read
/embed/job-editor?id=…One job: its stages, their state, and moving them along.Jobs: read/write to change anything
/embed/workflow-editor?id=…One workflow template, its stages and stage events.Workflows: read/write to change anything

A user without write access sees the screen and cannot save. That is a permission, not an error. Set role: "Admin" when brokering the session if the person should be able to change things.

Message reference

Every message carries protocol: "takiflo.v1". The name changes if the shape ever does, so a page built against v1 keeps working.

From the frame to you

MessagePayloadWhen
takiflo:readySession live, first render done.
takiflo:height{ px }Content height changed. Size the iframe to it.
takiflo:session-expiringAbout a minute left. Fetch a new token and post it back.
takiflo:error{ code, message }The session was refused. See below.

From you to the frame

MessagePayloadWhen
takiflo:session{ token }A fresh token from your backend, usually answering session-expiring.

Security rules

When it goes wrong

What you seeCauseFix
Blank frame, console says the page refused to connect or was blocked by CSPYour origin is not registered, or tenant is missing from the URLRegister the exact origin. Include &tenant= from the session response.
token_already_usedThe token was exchanged once already, often a frame mounted twice or a page reload reusing the same URLFetch a fresh token for every frame load.
invalid_tokenThe token does not exist, usually truncated or altered in transitPass it through encodeURIComponent and do not trim it.
token_expiredMore than 60 seconds passed between brokering and loadingFetch immediately before setting src.
invalid_api_keyThe key is wrong, inactive, or absentCheck the X-TakiFlo-Api-Key header on your server call.
role_not_permittedThe session asked for a role above AdminSend User or Admin.
email_in_useThat address already belongs to a different person in the accountAddresses are unique. Reassigning one needs the old account renamed or removed first.
"This session has ended"session-expiring went unansweredHandle that message and post a fresh token back.
A screen loads but saving failsThe mapped user has read-only access to that areaBroker the session with role: "Admin".

How users are mapped

An embedded session acts as a real person, not one shared identity for your whole account. Every stage completion, publish and reversal is recorded against a name, which is what makes the audit trail worth having.

  • externalId is the identity. Send your own stable identifier for the person, a database id rather than an email address. It must never be reused for a different human being.
  • First sight provisions an account. No invitation email is sent; nobody is being invited anywhere.
  • Your record stays authoritative. Change somebody's email or role on your side and the next session updates TakiFlo to match.
  • These accounts cannot sign in directly. They have no password, and password reset is refused for them. The only way in is a token you brokered.
  • Removing someone is your decision. Stop brokering tokens for them and they can no longer get in. The account remains so its history stays intact.
  • There is no seat charge. Provisioned users do not count against a limit. TakiFlo bills on jobs and notification credits.

Provisioned people appear in Users marked as coming from an embed, so an administrator can tell them from people who were invited.

Need Help?

We're here to support you every step of the way.

Documentation: Browse our guides or contact support.