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 frameYour 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
| Path | Shows | Needs |
|---|---|---|
/embed/jobs | Every job in the account. Selecting one opens its editor. | Jobs: read |
/embed/workflows | Every 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
| Message | Payload | When |
|---|---|---|
takiflo:ready | — | Session live, first render done. |
takiflo:height | { px } | Content height changed. Size the iframe to it. |
takiflo:session-expiring | — | About 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
| Message | Payload | When |
|---|---|---|
takiflo:session | { token } | A fresh token from your backend, usually answering session-expiring. |
Security rules
When it goes wrong
| What you see | Cause | Fix |
|---|---|---|
| Blank frame, console says the page refused to connect or was blocked by CSP | Your origin is not registered, or tenant is missing from the URL | Register the exact origin. Include &tenant= from the session response. |
token_already_used | The token was exchanged once already, often a frame mounted twice or a page reload reusing the same URL | Fetch a fresh token for every frame load. |
invalid_token | The token does not exist, usually truncated or altered in transit | Pass it through encodeURIComponent and do not trim it. |
token_expired | More than 60 seconds passed between brokering and loading | Fetch immediately before setting src. |
invalid_api_key | The key is wrong, inactive, or absent | Check the X-TakiFlo-Api-Key header on your server call. |
role_not_permitted | The session asked for a role above Admin | Send User or Admin. |
email_in_use | That address already belongs to a different person in the account | Addresses are unique. Reassigning one needs the old account renamed or removed first. |
| "This session has ended" | session-expiring went unanswered | Handle that message and post a fresh token back. |
| A screen loads but saving fails | The mapped user has read-only access to that area | Broker 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.
externalIdis 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.