Skip to content

API reference ​

Base URL: https://api.configure.dev. Every server-side request carries two headers:

HeaderValue
X-API-KeyYour secret key, sk_ prefix. Server only.
X-AgentYour agent handle.

A request about a specific user also identifies the user, in one of two ways:

  • Authorization: Bearer with the token from sign-in.
  • X-User-Id with your own ID.

X-User-Id works for a user who has not linked yet, and for a linked user who has approved your agent. For a linked user who has not approved it, requests return 403 approval_required. POST /v1/profile/connect and POST /v1/profile/mcp-session are the exceptions. They keep working so you can create the connection URL for authorization.

The SDK wraps every endpoint on this page. Use raw HTTP when neither the TypeScript SDK nor MCP fits your stack.

text
POST /v1/profile/connect

Use this endpoint to create a user-specific connection URL. Each URL expires. Do not construct connection URLs manually.

Every field is optional:

FieldWhat it does
appAsk for one app: gmail, calendar, drive, notion, or sheets. The flow opens on that app.
capabilityAsk for a capability, for example gmail:send.
purposeOne sentence shown to the user about why you are asking.
client_nameThe name the hosted page shows, if it differs from your agent name.
return_urlThe callback URL after the hosted flow.
user_idThe user, when you are not sending a bearer token.
emailThe user's email address from your app or authentication provider. For an unlinked user, Configure records it after approval as developer-supplied data. It does not authenticate the user or verify address ownership.
bash
curl -X POST https://api.configure.dev/v1/profile/connect \
  -H "X-API-Key: $CONFIGURE_API_KEY" \
  -H "X-Agent: $CONFIGURE_AGENT" \
  -H "X-User-Id: user_842" \
  -H "Content-Type: application/json" \
  -d '{ "purpose": "Use your preferences in replies" }'
json
{
  "status": "authorization_required",
  "connect_url": "https://mcp.configure.dev/connect/mcc_...",
  "expires_at": "2026-08-31T20:14:00Z",
  "instructions": "Show the link to the user so they can connect."
}

status is one of authorization_required, connect_app, or permission_needed. Send the user to connect_url verbatim and treat expires_at as authoritative. This endpoint does not take required. It drops the field without an error.

Connection URL creation uses the account rate limit. Requests above the limit return 429, rate_limited, and retry_after in seconds. Wait before retrying and reuse an existing URL while it remains valid.

text
POST /v1/profile/link

Need a verified email in the ID token? Ask for the email scope. When the account has no proven address, the hosted page asks for Gmail or Outlook after the code. email_verified is then true for that mailbox's address. To ask for the mailbox first, before the imports, add required=gmail|outlook (or required=email) to your /oauth/authorize request. Check email_verified in your callback anyway. The OpenID Connect page has the whole exchange.

After your relying party exchanges its Configure OIDC authorization code, send the returned access token with the external user ID you used for the profile:

bash
curl -X POST https://api.configure.dev/v1/profile/link \
  -H "X-API-Key: $CONFIGURE_API_KEY" \
  -H "X-Agent: $CONFIGURE_AGENT" \
  -H "X-User-Id: $CLERK_USER_ID" \
  -H "Content-Type: application/json" \
  -d '{ "access_token": "'"$CONFIGURE_ACCESS_TOKEN"'" }'

The OAuth client that issued the access token must belong to the same developer account as the API key. The access token is the user's fresh consent as one account, so the external ID follows it. If that external ID was linked to a different Configure account, the link moves. The response then reports relinked: true with previous_configure_user_id. No profile data moves between the two accounts. Repeated calls are safe. The response reports merged (first link, the unlinked profile folded into the account) and already_linked (nothing changed).

Read a profile ​

text
GET /v1/profile

Pass sections to scope the read. For prompt injection, use identity, preferences, and summary. An unscoped read returns everything the user has approved for your agent, and the response can be large.

The response carries linked, identity, preferences, summary, connections, integrations, imports, and agents. If a category exceeds the response limit, truncated and truncated_boxes identify the omitted content.

Commit a turn ​

text
POST /v1/profile/commit
FieldBounds
messagesUp to 20 messages, up to 16,000 characters each.
responseOptional assistant reply, appended as the trailing message.
memoriesUp to 20 explicit memories, up to 1,000 characters each.
syncDefault false: the commit may be queued.

The response carries status. It is processing when the commit was queued and completed when it ran inline. Once the commit has run, the response also has facts_written, user_summary, memories_written, and rejected_memories. With sync: true, the call always waits for them.

Committing also satisfies the read contract. Commit the turn to resolve a -32009 commit_required response.

Open an MCP session ​

text
POST /v1/profile/mcp-session

It returns linked and an mcp_servers block, ready to pass to a model provider:

json
{
  "linked": true,
  "mcp_servers": [
    {
      "type": "url",
      "url": "https://api.configure.dev/mcp",
      "name": "configure",
      "authorization_token": "..."
    }
  ]
}

Sessions are short-lived, about 15 minutes. Create one per conversation. A session is created for any user you name. To learn whether they have connected, read connected on the connect endpoint or linked on the profile read, not this response.

Write to the profile ​

There are five SDK methods. Each one is a POST under /v1/profile with the same name:

MethodInputReturns
remember(fact, { box?, kind?, reply_to?, resolves? })One durable fact, filed to a box{ saved, memory, source, livemode }
forget({ id } | { match } | { import_id } | { scope }, { reason? })Exactly one selector. id deletes on the spot, because you already named one memory. match, import_id, and scope preview what they would delete. Send a second call with confirm: true to delete{ deleted, deleted_count, matched }
import(text, { box?, kind? })Text to process into durable context, such as a pasted transcript{ saved, box, distilled, memory, source, livemode }
search({ query?, box?, source?, from?, to?, limit? })A lookup across what this user approved for you{ results, visibleSources, sourceCounts, guidance, livemode }
link({ accessToken })Bind an external user ID to the Configure account from your OIDC access-token exchange{ linked, merged, already_linked, relinked, previous_configure_user_id?, configure_user_id }

An agent can forget only what it saved itself. The message and character limits listed above apply to commit.

Provision the sandbox ​

text
POST /v1/sandbox/provision

This endpoint requires no authentication and applies a per-IP rate limit. It returns sk_test_ and pk_test_ keys and a linked synthetic user. Use these credentials to test sign-in, initial context injection, and conversation commits before accessing production profiles. The test user's verification code is always 424242.

With the sk_test_ key, POST /v1/sandbox/reset restores the synthetic user to her seeded state. POST /v1/sandbox/simulate with { "scenario": "gmail_token_expired" } makes her Gmail connection fail like a revoked token until the next reset.

Exchange the sign-in code ​

text
POST /v1/auth/sign-in/exchange

This is the sign-in code exchange behind /callback. Call it with your secret key. The SDK method configure.auth.exchangeSignInCode(code) wraps it and returns { token, userId }. Use the SDK here unless you cannot. The OpenID Connect code exchange is a different endpoint, POST /oauth/token, covered on the OpenID Connect page.

The card ​

The card is the MCP app a host renders for configure_connect. It has a heading, the assistant marks, Connect, and Not now. If you already mount it in your own chat with the host script and the unchanged connect() result, it keeps working. Every resource ID serves the current card.

Value
Host scripthttps://api.configure.dev/mcp-apps/configure-mcp-host.js
Current resourceui://configure/connect/v8.html
Status of a linkGET <connect_url>/status. It answers { "status": "pending" }, approved, or expired

Get profile updates ​

text
PUT /v1/webhooks

Register an HTTPS endpoint and Configure POSTs a signed profile.updated event when what your agent can read about a user changes: a new or grown category, a regenerated document, an identity field, a finished import, a connected app. Re-read the profile when one arrives. A weekly read is enough as reconciliation.

ts
const hook = await configure.webhooks.set({ url: 'https://example.com/hooks/configure' });
// hook.secret is returned once. Store it as CONFIGURE_WEBHOOK_SECRET.

Changes are batched per user, about a minute after the last write and at most one event every five minutes, so an import arrives as one event. A sk_test_ key manages a separate endpoint that only receives test users.

The event names what changed and never carries profile content:

json
{
  "id": "evt_2f0c…",
  "type": "profile.updated",
  "created_at": "2026-09-29T02:38:21.126Z",
  "livemode": true,
  "agent": "your-agent",
  "data": {
    "user": { "external_id": "user_42", "configure_user_id": null, "linked": false },
    "baseline": false,
    "sections": ["categories", "documents"],
    "changes": {
      "categories": [{ "id": "work", "title": "Work", "change": "updated", "count": 5, "previous_count": 3, "updated_on": "2026-09-28" }],
      "sources": [],
      "documents": ["summary"],
      "identity": [],
      "integrations": { "connected": [], "disconnected": [] }
    }
  }
}

data.user.external_id is the ID you pass as X-User-Id. documents lists regenerated documents: summary, soul, preferences, context. baseline: true means Configure had no earlier snapshot of this user for your endpoint, so everything present is listed as added. Users who already exist when you register are recorded silently; a user created later, or one who changes before that first recording, arrives as a baseline.

Receive events with one handler. It verifies the signature, answers the test ping, and calls your function for each profile.updated event. If your function throws, the handler answers 500 and Configure retries the same event.

ts
import { Configure } from 'configure';

const configure = new Configure({ apiKey: process.env.CONFIGURE_API_KEY!, agent: 'your-agent' });

export const POST = configure.webhooks.handler(process.env.CONFIGURE_WEBHOOK_SECRET!, async (event) => {
  await refreshProfile(event.data.user.external_id);
});

The same handler works in Express. Mount it before any JSON body parser so it can verify the raw body:

ts
app.post('/hooks/configure', configure.webhooks.handler(process.env.CONFIGURE_WEBHOOK_SECRET!, async (event) => {
  await refreshProfile(event.data.user.external_id);
}));

To verify by hand, call verifyWebhook(rawBody, headers, secret). It returns the event or throws.

Deliveries follow Standard Webhooks: webhook-id, webhook-timestamp and webhook-signature headers, HMAC-SHA256 over id.timestamp.body. Any 2xx counts as delivered. Anything else is retried with the same webhook-id and the same body for about three days, so deduplicate on webhook-id.

An event fires when anything your agent can read changes: new or edited memories, regenerated documents, identity fields, connected tools, an account link, or the user approving your agent. Your own writes do not trigger one: a memory your agent saves, and the categories and documents regenerated in the same window, are not sent back to you. Anything else in that window still is, including a user editing or deleting one of your memories. A document regenerated after that window arrives as its own documents event. Configure also rechecks every registered user once a day and sends an event only if something changed, so a missed change surfaces within a day. Deliveries that still fail after three days are dropped; if your endpoint can be down that long, keep a weekly profile read as reconciliation.

RequestDoes
PUT /v1/webhooksRegister or change the URL. { "rotate_secret": true } creates a new secret; the old one keeps signing for 24 hours.
GET /v1/webhooksThe endpoint and its delivery counts.
GET /v1/webhooks/secretThe signing secret, plus previous_secret for 24 hours after a rotation. Your server can read it at startup with its API key instead of storing it.
POST /v1/webhooks/testSends a signed ping and returns your endpoint's status.
DELETE /v1/webhooksStops deliveries.

The Python SDK has the same calls: client.webhooks.set(url), client.webhooks.handler(secret, on_update) and verify_webhook(body, headers, secret). The handler returns (status, body) for your framework:

python
hook = client.webhooks.handler(os.environ["CONFIGURE_WEBHOOK_SECRET"], lambda event: refresh_profile(event["data"]["user"]["external_id"]))

@app.post("/hooks/configure")
def handle_webhook():
    status, body = hook(request.get_data(), request.headers)
    return body, status

When a call fails ​

Every SDK method rejects with ConfigureError. An error built from an API response carries code, statusCode, type, retryable, suggestedAction, docUrl, retryAfter and requestId. Include requestId when reporting an error to Configure.

code is one of thirteen values, exported as ErrorCode:

CodeWhat it meansWhat to do
API_KEY_MISSINGNo key reached the callCheck the server's environment
AUTH_REQUIRED401: your key is missing, wrong, or revokedCheck the key. Every user is affected
ACCESS_DENIED403. With suggestedAction: "complete_approval", this user linked a Configure account and has not approved your agent. Otherwise the tool is outside the set you declaredSend the user through profile.connect(), or widen tools()
TOOL_NOT_CONNECTEDThe user has not connected that app, or its access expiredCreate a link for that app
COMMIT_REQUIREDA profile read requires a conversation commit for the current turnCommit the turn, then retry
PAYMENT_REQUIREDYour account is over a quotaResolve your account quota; reconnecting the user does not help
RATE_LIMITED429Back off using retryAfter
INVALID_INPUTThe SDK rejected the arguments before sendingFix the call
NOT_FOUND404Check the ID
TOOL_ERRORThe connector itself failedSurface it, do not offer a link
TIMEOUTThe request exceeded your client timeoutAnswer without it
NETWORK_ERRORThe request never completedAnswer without it
SERVER_ERROR500 and aboveRetry as appropriate; continue the chat without unavailable context

WARNING

401 and 403 are different problems. 401 (AUTH_REQUIRED) means your key is wrong or revoked, which affects every user at once. 403 (ACCESS_DENIED, with suggestedAction: "complete_approval") means this one user has not approved your agent. If your code treats both as "not connected" and caches the answer, it marks every reader unconnected as soon as a key rotates. The server's own code, such as approval_required, is not on the error. Match on statusCode and suggestedAction.

If a profile read, commit, or tool call fails, handle the error and continue the response without unavailable context. Do not interpret an authorization failure as permission to proceed with a restricted action.

Everything else ​

The rest of the profile surface (search, remember, forget, and import) mirrors the SDK methods of the same names. The TypeScript SDK is the reference implementation. Use the SDK implementation to verify behavior if this reference is inconsistent.

Infrastructure to connect agents, apps, and chats