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.
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.provisioning-key-… and carry a User provisioning label in the key list,
so you can tell them apart later.
Endpoint
Authentication
Include your API key in theX-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: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.
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
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.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.users:provision.
Connect Links — for anything that needs the person to sign in
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.Request a link
Requires
users:credentials, and the target must be an account you provisioned.
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.
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
- 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 carryingagents:manage. Read the current
instruction, append your block, and write the whole thing back — instruction is
replaced wholesale, not appended to.
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.
Running as a Provisioned User
Take theuser_id from the provisioning response and pass it as on_behalf_of. It
works on both invocation endpoints and needs the users:delegate scope:
Keeping one-off calls out of their history
Both endpoints acceptsession_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:
include_hidden=false.
Showing a User Their History
The sameon_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.
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 thesessions:delete scope, granted by the same checkbox.
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.
