Connect a user's assistants and apps from your app
Invite-only
Configure enables this flow per partner. A secret key on its own does not reach it. An uninvited key gets 404 from the mint endpoint. An invited partner whose installation does not list connect gets a 400 that says so. If you have not been given credentials for it directly, talk to Configure first.
On one Configure page, your user imports from their assistants and connects Gmail, Google Calendar, Google Drive, Notion or Outlook. Then they prove a phone number. You keep your own user id, and the page links it to their Configure account. Your server needs three calls:
ts
const API = "https://api.configure.dev";
// sk_..., server only. Add "X-Agent": "<your agent>" if your account has more than one agent.
const KEY = { "X-API-Key": process.env.CONFIGURE_SECRET_KEY ?? "" };
type FlowSession = {
id: string;
status: "created" | "opened" | "importing" | "ready" | "expired" | "abandoned";
imports: { source: string; status: string; memory_count: number }[];
requested_connectors: string[];
connections: { app: string; connected: boolean }[];
account: { linked: boolean; configure_user_id: string | null; phone: string | null };
url?: string;
};
// 1. In the click handler: mint a session, keep its id, and redirect the user to `url`.
export async function startConnect(yourUserId: string): Promise<{ sessionId: string; url: string }> {
const res = await fetch(`${API}/v1/flows/sessions`, {
method: "POST",
headers: { ...KEY, "Content-Type": "application/json" },
body: JSON.stringify({
flow: "connect",
partner_ref: yourUserId, // your own id, never an email or a phone number
return_url: "https://yourapp.example/welcome", // https, on a host registered for your account
}),
});
if (res.status !== 201) throw new Error(`Mint failed: ${res.status} ${await res.text()}`);
const session = (await res.json()) as FlowSession;
return { sessionId: session.id, url: session.url ?? "" };
}
// 2. The user imports and connects on Configure's page, then proves a phone number. Nothing to build.
// 3. When the user is back on your return_url, read the session. The redirect is not proof.
export async function checkConnect(sessionId: string) {
const res = await fetch(`${API}/v1/flows/sessions/${sessionId}`, { headers: KEY });
if (!res.ok) throw new Error(`Poll failed: ${res.status} ${await res.text()}`);
const session = (await res.json()) as FlowSession;
return {
finished: session.status === "ready",
over: ["ready", "expired", "abandoned"].includes(session.status),
imports: session.imports,
connections: session.connections,
// Store this id only once linked is true. Before that, it is a provisional id.
configureUserId: session.account.linked ? session.account.configure_user_id : null,
};
}
// 4. Read the profile with your own user id.
export async function readProfile(yourUserId: string) {
const res = await fetch(`${API}/v1/profile`, { headers: { ...KEY, "X-User-Id": yourUserId } });
if (!res.ok) throw new Error(`Profile read failed: ${res.status} ${await res.text()}`);
return res.json();
}The SDK has no method for this flow. The file above calls the REST endpoints with fetch.
The partner flows differ in what they ask for and in what order:
| Import-only | Linked account | Connect | |
|---|---|---|---|
| Email at mint | Refused | Required | Optional |
| First screen | The assistants | Phone number | The grid: assistants and apps |
| Phone number and code | Never | First | After the grid |
| Configure account | No | Yes | Yes, after the code |
What the user sees
One page, hosted by Configure, branded with your name:
- The grid. The assistants in your
sources, then Gmail, Google Calendar, Google Drive, Notion and Outlook. The user imports from an assistant or connects an app. Continue stays off until they do one of these and meet everyrequiredentry. A user who already has a Configure account taps Sign in under Continue ("Already use Configure? Sign in") and proves their phone first. - Phone number.
- Code. Six digits, sent by text message.
- Receipt. "You're all set", with a Return to button that names your app. It returns on its own after about 5.8 seconds, or about 8.8 seconds while an app they chose is still unconnected. Without a
return_url, the button is Close.
There is no email step and no Gmail step. What the hosted flow asks for shows these screens next to every other way in.
What you need
| A secret key | sk_..., bound to your agent. Your agent is the handle that names your app on every request. Configure gave it to you with the key. If your account has more than one agent, send X-Agent too. |
| The flow enabled | Configure sets flows on your installation. Ask for connect. |
| Registered return hosts | Only if you send return_url. Configure registers them for you. |
| Your own user id | You send it as partner_ref. The email is optional. |
1. Mint a session when the user clicks
bash
curl -X POST https://api.configure.dev/v1/flows/sessions \
-H "X-API-Key: $CONFIGURE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"flow": "connect",
"partner_ref": "your-own-user-id",
"return_url": "https://yourapp.example/welcome"
}'| Field | Required | Notes |
|---|---|---|
flow | yes | connect. Omit it and you get the import-only flow. |
partner_ref | yes | Your opaque id for this user: 1 to 200 characters of letters, digits and . _ : @ -. Never an email or a phone number. One that looks like either is refused. |
email | no | The user's email, if you have one. Configure attaches it, unverified, when the link opens. |
email_verified | no | true only if you verified the email. Needs email. With it, Configure also emails the user a receipt when they finish. |
sources | no | Any of chatgpt, claude, gemini, grok. Defaults to all four. The grid shows these assistants. |
required | no | What the user must import or connect before Continue. See Require an app or an assistant. |
return_url | no | Where the receipt sends the user. https, on a host registered for your account. |
The response is 201:
json
{
"id": "5b0c1c0e-7d1c-4f63-9f55-2a7f4f0d3a10",
"status": "created",
"livemode": true,
"partner_ref": "your-own-user-id",
"flow": "connect",
"steps": ["imports", "phone", "otp", "done"],
"sources": ["chatgpt", "claude", "gemini", "grok"],
"imports": [],
"requested_connectors": [],
"connections": [],
"return_url": "https://yourapp.example/welcome",
"profile_ready": false,
"failure": null,
"created_at": "2026-09-25T09:00:00.000Z",
"opened_at": null,
"completed_at": null,
"expires_at": "2026-09-25T09:15:00.000Z",
"account": {
"phone_verified": false,
"linked": false,
"configure_user_id": null,
"phone_verified_at": null,
"phone": null
},
"url": "https://accounts.configure.dev/embed/auth?flow_handoff=cfgfh_...&agent=your-agent&flow=connect"
}Send the user to url. Nothing else from this response belongs in a browser. The link lasts 15 minutes and works once, so mint it inside the click handler. steps is the screen list for this session: the grid, the phone number, the code, the receipt.
Require an app or an assistant
If your product needs something the user has not brought in, require it. Pass required when you mint:
json
{
"flow": "connect",
"partner_ref": "your-own-user-id",
"required": ["gmail|outlook", "chatgpt"]
}Each required entry moves to the top of the grid and reads Required in red. For an entry with alternatives, the line above Continue says that one is enough, for example "Connect Gmail or Outlook to continue." Continue stays off until every entry is met. A required mailbox stays on the grid. A partner link never opens on the separate mailbox page.
The rules:
- Values: any of your
sources, andgmail,outlook,calendar,drive,notion. Anything else is a400that names what it did not accept. |inside an entry means any one of them. An inner array means the same:[["gmail", "outlook"], "chatgpt"].emailis short forgmail|outlook.- The link carries it:
urlends with&required=.... - It shapes the screen. It is not a security control. When you poll, read what landed in
importsandconnections.
2. The user does the flow
Nothing to build. The page runs these steps in order:
- The first tap opens the link. The status turns
opened, andexpires_atmoves to 60 minutes after that tap. A user who loads the page and leaves without a tap leaves the sessioncreated. - Imports and connects that happen before the code run under your
partner_ref. If the user leaves now, you can still read what they brought in. - At the code, the phone number resolves to a Configure account: theirs if they have one, a new one if not. What they brought in moves to that account. Your
partner_refis linked to it, and your agent is approved for it. - If you sent
emailwithemail_verified: true, the code records the email as verified. The exception: another account already proved that address. Your email never makes an OpenID Connectemail_verifiedclaimtrue. - The user finishes the receipt, and the status turns
ready.
One case is refused: your partner_ref is already linked to a Configure account, and the phone number resolves to a different one. The code step stops there, and the session never reaches ready.
3. Poll the session
bash
curl https://api.configure.dev/v1/flows/sessions/$SESSION_ID \
-H "X-API-Key: $CONFIGURE_SECRET_KEY"The response is the mint response without url. After the receipt:
json
{
"id": "5b0c1c0e-7d1c-4f63-9f55-2a7f4f0d3a10",
"status": "ready",
"livemode": true,
"partner_ref": "your-own-user-id",
"flow": "connect",
"steps": ["imports", "phone", "otp", "done"],
"sources": ["chatgpt", "claude", "gemini", "grok"],
"imports": [{ "source": "chatgpt", "status": "completed", "memory_count": 5 }],
"requested_connectors": ["gmail", "notion"],
"connections": [
{ "app": "gmail", "connected": true },
{ "app": "notion", "connected": false }
],
"return_url": "https://yourapp.example/welcome",
"profile_ready": true,
"failure": null,
"created_at": "2026-09-25T09:00:00.000Z",
"opened_at": "2026-09-25T09:00:40.000Z",
"completed_at": "2026-09-25T09:03:12.000Z",
"expires_at": "2026-09-25T10:00:40.000Z",
"account": {
"phone_verified": true,
"linked": true,
"configure_user_id": "efc419f4-797e-4e19-9615-70c7e2945444",
"phone_verified_at": "2026-09-25T09:02:58.000Z",
"phone": "+14155550100"
}
}Poll when the user comes back to your return_url, or while your own page waits for them. The redirect is not proof. A user who closes the tab never comes back. Stop at ready, expired or abandoned.
status | Meaning |
|---|---|
created | Minted. Nobody has tapped the link yet. |
opened | The user tapped the link. expires_at is now 60 minutes after that tap. |
importing | A pasted export is being processed. |
ready | The user finished the receipt, after the code. profile_ready is true. |
expired | Nobody tapped the link before expires_at. |
abandoned | The user tapped the link but did not finish before expires_at. |
Configure sets expired and abandoned when you read a session after its expires_at. This flow never sets failed.
| Field | What it holds | When it fills |
|---|---|---|
imports | [{ source, status, memory_count }]. status is importing, completed, empty, duplicate or failed. | As each paste runs. At the finish, Configure computes it again, so imports the account already had show too. |
requested_connectors | The apps the user picked on the grid: what they meant to connect. | As they tap. |
connections | [{ app, connected }] for those apps: what the account holds. | Only at the finish. It is [] before. |
account.linked | true when your partner_ref is linked to a Configure account. | At the code. Right after the first tap if this partner_ref was linked before. |
account.configure_user_id | The id Configure knows this user by. | null before the first tap. A provisional id while linked is false. The account's id after the code. |
account.phone_verified, account.phone_verified_at | The code passed, and when. | At the code. |
account.phone | The number they proved, in E.164 format. | Once the phone is proven and linked. |
profile_ready | true when status is ready. | At the finish. |
Store configure_user_id next to your own user row only when linked is true. Before that, it is a provisional id, and it changes at the code.
4. Read the profile with your own id
bash
curl https://api.configure.dev/v1/profile \
-H "X-API-Key: $CONFIGURE_SECRET_KEY" \
-H "X-User-Id: your-own-user-id"- A user who left before the code: the read works, with
linked: false. - A user who passed the code: the read works, with
linked: true. The code approved your agent. - A linked user who has not approved your agent gets
403withapproval_required.
Once linked is true, the user has a Configure account bound to your partner_ref. The linked-account guide shows how to offer one more app from your own screen with a connect link.
What can go wrong
A refused mint or poll answers with this envelope:
json
{
"error": {
"type": "invalid_request_error",
"code": "invalid_format",
"message": "return_url must be an absolute https URL.",
"param": "return_url",
"retryable": false
},
"request_id": "..."
}| You see | Meaning |
|---|---|
404 resource_missing, "Route not found." | Your account is not invited to the partner flows. Talk to Configure. |
400 "The connect flow is not enabled for your installation. Email partners@configure.dev to enable it." | Your installation does not list connect. |
400 "flow must be import_only, linked_account or connect." | flow has a typo. |
400 "partner_ref must be an opaque reference, not an email address or phone number." | Send your own user id instead. |
400 "email must be a deliverable address." | Fix the email, or leave it out. |
400 "email_verified attests an email; pass the email with it." | Send email with email_verified. |
400 "sources must be a non-empty list drawn from chatgpt, claude, gemini, grok." | Fix sources, or leave it out. |
400 "return_url must be an absolute https URL." | Use an https URL. |
400 "return_url must be on a host registered for your account. Email partners@configure.dev to add one." | Ask Configure to register the host. |
400 "required must be drawn from ..." | The message names the values it did not accept. |
404 "Flow session not found." on a poll | The id is wrong, or the session was minted with the other key. Test and live keys see only their own sessions. |
status: expired | Nobody tapped the link within 15 minutes. Mint a new one on the next click. |
status: abandoned, account.linked: false | The user left before the code. What they imported is still readable with your id. |
status: abandoned, account.phone_verified: true | The user passed the code and left before the receipt. The account and the link are real, and the profile read works. |
The user reports that the code step would not finish, and the session never reaches ready | Your partner_ref was linked to a different Configure account earlier. Contact Configure. |
| The user sees | Why |
|---|---|
| This link has already been used. | The link was opened in a different browser. Mint a new one. |
| This link has expired. Ask the app to send a new one. | More than 15 minutes passed before the first tap. Mint a new one. |
| Required in red, and Continue stays off | You required an app or an assistant that they have not brought in yet. |
Limits
| Handoff link | 15 minutes, single use. |
| Session after the first tap | 60 minutes. |
| Mint and poll, per developer account | 120 a minute on the free plan, 300 on pro, 800 on scale, 2000 on enterprise. |
| Code attempts | 5 per phone number and 30 per IP address, in 10 minutes. |
Test mode
Mint with your sk_test_ key. The session reads livemode: false. Poll it with the same test key: a live key does not find it.
If you want Configure as a sign-in option instead, with no invitation, see OpenID Connect.