Skip to content

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-onlyLinked accountConnect
Email at mintRefusedRequiredOptional
First screenThe assistantsPhone numberThe grid: assistants and apps
Phone number and codeNeverFirstAfter the grid
Configure accountNoYesYes, after the code

What the user sees ​

One page, hosted by Configure, branded with your name:

  1. 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 every required entry. A user who already has a Configure account taps Sign in under Continue ("Already use Configure? Sign in") and proves their phone first.
  2. Phone number.
  3. Code. Six digits, sent by text message.
  4. 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 keysk_..., 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 enabledConfigure sets flows on your installation. Ask for connect.
Registered return hostsOnly if you send return_url. Configure registers them for you.
Your own user idYou 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"
  }'
FieldRequiredNotes
flowyesconnect. Omit it and you get the import-only flow.
partner_refyesYour 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.
emailnoThe user's email, if you have one. Configure attaches it, unverified, when the link opens.
email_verifiednotrue only if you verified the email. Needs email. With it, Configure also emails the user a receipt when they finish.
sourcesnoAny of chatgpt, claude, gemini, grok. Defaults to all four. The grid shows these assistants.
requirednoWhat the user must import or connect before Continue. See Require an app or an assistant.
return_urlnoWhere 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, and gmail, outlook, calendar, drive, notion. Anything else is a 400 that names what it did not accept.
  • | inside an entry means any one of them. An inner array means the same: [["gmail", "outlook"], "chatgpt"].
  • email is short for gmail|outlook.
  • The link carries it: url ends with &required=....
  • It shapes the screen. It is not a security control. When you poll, read what landed in imports and connections.

2. The user does the flow ​

Nothing to build. The page runs these steps in order:

  1. The first tap opens the link. The status turns opened, and expires_at moves to 60 minutes after that tap. A user who loads the page and leaves without a tap leaves the session created.
  2. 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.
  3. 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_ref is linked to it, and your agent is approved for it.
  4. If you sent email with email_verified: true, the code records the email as verified. The exception: another account already proved that address. Your email never makes an OpenID Connect email_verified claim true.
  5. 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.

statusMeaning
createdMinted. Nobody has tapped the link yet.
openedThe user tapped the link. expires_at is now 60 minutes after that tap.
importingA pasted export is being processed.
readyThe user finished the receipt, after the code. profile_ready is true.
expiredNobody tapped the link before expires_at.
abandonedThe 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.

FieldWhat it holdsWhen 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_connectorsThe 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.linkedtrue 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_idThe 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_atThe code passed, and when.At the code.
account.phoneThe number they proved, in E.164 format.Once the phone is proven and linked.
profile_readytrue 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 403 with approval_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 seeMeaning
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 pollThe id is wrong, or the session was minted with the other key. Test and live keys see only their own sessions.
status: expiredNobody tapped the link within 15 minutes. Mint a new one on the next click.
status: abandoned, account.linked: falseThe user left before the code. What they imported is still readable with your id.
status: abandoned, account.phone_verified: trueThe 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 readyYour partner_ref was linked to a different Configure account earlier. Contact Configure.
The user seesWhy
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 offYou required an app or an assistant that they have not brought in yet.

Limits ​

Handoff link15 minutes, single use.
Session after the first tap60 minutes.
Mint and poll, per developer account120 a minute on the free plan, 300 on pro, 800 on scale, 2000 on enterprise.
Code attempts5 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.

Personalization infrastructure for agents