Skip to main content

Overview

If you are building your own product on Junis, your users should each get their own Junis account — so their conversations, history, and memory stay separate — without anyone having to sign up for Junis. User Provisioning does exactly that: you create an account for each of your users, run everything on their behalf, and pay for all of it from one place. You never share your API key with your users, and they never see Junis.

The lifecycle

1

Create an account for each of your users

One call with their email. Idempotent, so you can call it every login.
2

Connect the accounts they need

Push API keys straight in, or hand the person a connect link for anything that needs their sign-in (Gmail, Slack). Your agents then act as that user.
3

Run chats as that user

Add on_behalf_of to your normal calls. Sessions and memory belong to them.
4

Show them their history

The same parameter works on the session read endpoints.
5

Delete their data when they leave

Purge their conversations permanently, so you can honour your own deletion promise.
Billing never moves. Subscription checks and credit charges always stay with the user behind your API key, no matter whose name a call runs under. Your users do not need a Junis subscription — or a Junis account they ever log into.
Provisioned accounts do not receive the signup credit bonus and have no memory profile until the person actually signs in with Google themselves. If they ever do, their Google account links to the one you created and their membership activates — nothing is lost or duplicated.

Getting a key

Create the key from Dashboard → API Keys → New Key and tick “Connect users from my own app” before generating it. That checkbox is what turns an ordinary key into a provisioning key.
Scopes are frozen into a key when it is created. If you already have a provisioning key from before users:credentials existed, generate a new one to get it — existing keys are not upgraded.
Only organization owners and admins can open that page, which is also the requirement for using these scopes — so a key created there is always eligible. Provisioning keys are named provisioning-key-… and carry a User provisioning label in the key list, so you can tell them apart later.
This key can create accounts in your organization, run chats as its members, and delete conversations. Keep it on your server — never ship it in a browser, a mobile app, or a public repository.

Endpoint

Authentication

Include your API key in the X-API-Key header:

Request Parameters

Example Request

Response

When the Email Belongs to an Existing Junis User

If the email already belongs to a real Junis account (someone who has signed in themselves), the account is not attached to your organization and cannot be delegated:
Only the email is echoed back — no user ID or membership information is returned. Handle this case in your integration: invite the user to your organization from the dashboard instead, or skip Junis-linked features for that user.

Behavior

1

Create-or-get user

If a user with the email already exists, it is returned unchanged (created: false). Otherwise a new account is created with google_id = NULL — no signup bonus, no memory profile (both happen on the real first login).
2

Ensure pending membership

If the organization has no membership row for this email, a pending invite with the member role is created — identical to inviting the user from the dashboard.
3

First Google login links everything

When the user signs in with Google using that email, the account is linked to their Google identity, the pending membership is activated, and the invited organization becomes their current organization.
The endpoint is idempotent — calling it repeatedly for the same email returns the same user and membership state.

Errors


Storing API Credentials for a User

Most useful automations need to call something on the person’s behalf — their Notion, their Google Sheets, their store. Those calls need their key, not yours. Push the credential into their account once, and every agent that runs for them uses it automatically. They never paste a key into a chat, and the key never reaches the model.

Endpoints

Requires the users:credentials scope and an owner/admin key.

Store or rotate

PUT is idempotent — the same call creates a credential the first time and rotates it every time after.

Check what they have

Returns names only — never values. Use it to work out what is still missing and prompt for it in your own onboarding UI.

Delete

Do this when the person leaves, alongside deleting their conversations.
Stored values can never be read back. There is no endpoint, on any key, that returns a credential’s value — listing gives you names only. If your key leaks, the credentials already stored with it cannot be pulled out.
Only accounts you provisioned. If the email already belonged to a real Junis user (delegable: false), this returns 403. Nobody — not even an organization owner — can write into a real person’s private vault. That rule is what makes the vault trustworthy, so it has no exceptions.

Rules

Errors


Listing Your Provisioned Users

Reconcile against your own database, or build an internal admin view. Only accounts your organization provisioned appear — never real Junis users.
Requires users:provision.
An API key you can push in directly (§ above). Gmail, Slack, Google Calendar and other OAuth apps you cannot — those credentials only exist after the person clicks “Allow” on the provider’s own consent screen. There is no key to inject. A connect link solves both cases with the same flow: you request a link, open it in a webview, and the person connects everything on one page. Junis handles OAuth redirects and key entry for you — your app needs no callback page and no OAuth code.
1

Ask for a link

One call with the toolkits you want connected.
2

Open it in a webview

The person connects each item. OAuth apps redirect out and come back automatically.
3

They return to your app

Via your return_url, if you set one.

Discovering what you can connect

The ids you pass come from two catalog endpoints. Call them once at build time and hard-code the ids you care about — they are stable.
Only integrations with connection_type: "personal" can be connected for an individual user — organization-wide ones are rejected with 400. You do not need to branch on auth_scheme: the connect page handles OAuth and API-key toolkits on its own.
Requires users:credentials, and the target must be an account you provisioned.
The token in url is shown once. Only its hash is stored, so the link cannot be looked up again — request a new one instead. Treat the URL like a password: it is the only thing standing between a stranger and that user’s connect page.

Open it in a webview

Fetch the link at the moment the person taps your “Connect” button, then open it. Do not pre-generate links — a 30 minute lifetime is what keeps a leaked URL harmless.
The page detects each type on its own: When everything is done the page shows All set, plus a button back to your app if you set return_url.

What the page can and cannot do

The link grants exactly one thing: creating connections for that one user, limited to the toolkits you listed. It cannot read data, cannot reach other users, cannot touch toolkits you did not include, and cannot reveal any stored value. The page never shows your organization, the user’s id, or their email.
  • Expired, revoked and non-existent links all return the same response — a link cannot be probed for validity.
  • Reusable until it expires, because an OAuth round trip has to come back to it.
  • Connecting refreshes that user’s agent tools immediately.

Checking the result

You do not have to poll. Composio pushes status changes to Junis, and the connection is live as soon as the person finishes. To display state in your own app:

Errors


Using a Stored Credential in an Agent

Reference the credential by name in the agent’s instruction. Junis substitutes the calling user’s own value at request time and sends it as an HTTP header — it never enters the prompt, the model’s context, or the conversation log.
The two never substitute for each other. If a user has no credential with that name, the call is not sent — the tool returns an error naming what is missing, instead of quietly falling back to an organization key.
The config is read from the instruction of the agent that owns the custom_api_call tool — not from the orchestrator. If a sub-agent makes the call, the block belongs in that sub-agent’s instruction. Giving one dedicated agent the tool and the config keeps it out of your orchestrator’s context on every turn.

Writing it into an agent

Owner/admin only, with a user-level key carrying agents:manage. Read the current instruction, append your block, and write the whole thing back — instruction is replaced wholesale, not appended to.
instruction and tools are both replaced in full. Fetch, merge, then write — and have only one process doing it, or two concurrent writers will overwrite each other.
An organization-level provisioning key cannot do this — it does not carry agents:manage. Use a user-level key created by an owner or admin from Settings → API Keys. The two keys have different jobs: the provisioning key manages accounts and their credentials, the user-level key manages agents.

Users Managing Their Own Credentials

If the person signs into Junis themselves, they manage their own credentials — you do not need to push anything.
  • In the app: Team → MCP & Keys → My Keys. Open to every member, including those with the MEMBER role.
  • Over the API: with their own user-level key carrying secrets:manage.
These endpoints accept user-level keys only. An organization-level key is rejected with 403, because such a key resolves to whoever created it — accepting it would let every holder of that key edit that person’s private vault.
Same rules as above: names only on read, 8 KB per value, 200 per organization, and no way to read a value back.

Running as a Provisioned User

Take the user_id from the provisioning response and pass it as on_behalf_of. It works on both invocation endpoints and needs the users:delegate scope:
Sessions, messages, and memory belong to that user. Subscription checks and credit charges stay with your API key’s own user. Delegation works before their first login, so you can start using an account the moment you create it.

Keeping one-off calls out of their history

Both endpoints accept session_hidden: true. Use it for calls that are part of your product’s machinery rather than a conversation the user should see later — scoring, classification, background analysis:
Hidden sessions still belong to the user and still bill to you; they are simply left out when you list a user’s conversations with include_hidden=false.

Showing a User Their History

The same on_behalf_of parameter works on the session read endpoints, so you can build a history view inside your own product:
include_hidden=false leaves out the one-off calls you marked with session_hidden, so the list matches what your user would expect to see. It defaults to true. Omit on_behalf_of entirely and you get your own key’s sessions, exactly as before.
Reading is narrower than running. On these endpoints on_behalf_of only accepts accounts your organization provisioned. Chat completions also let you run as a real member of your organization, but reading or deleting their conversations never works — a provisioning key can’t pull a real person’s chat history.

Deleting a User’s Data

When someone deletes their account in your product, delete their Junis conversations too. This is what lets you honour a “we delete everything” promise. Requires the sessions:delete scope, granted by the same checkbox.
The session, its messages, and its stored state are removed permanently — there is no recovery. To purge a user completely, list their sessions with include_hidden=true (the default, so you catch the hidden ones too) and delete each one. Every deletion is recorded in your organization’s audit log with who deleted it, whose session it was, and whether the call was delegated.

Delegation errors