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 — let the person connect what the squad needs
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. And even for plain API keys, it is better when the person’s key never passes through your servers. A connect link solves both with one flow: you request a link for a squad, open it in a webview, and the person connects (or disconnects) everything on one page. OAuth redirects, key entry and encryption are handled for you — your app needs no callback page and no OAuth code. The page carries no branding other than what the person is connecting to.1
Ask for a link
One call with the squad’s
agent_id. The page shows only what that squad needs.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 — even if they only connected some items.What the page shows
Pass the squad’s root agent (agent_id, from GET /management/agents). The page is built
from what is configured on that agent and its sub-agents:
If the squad changes later, existing links follow the new configuration.
Check what is needed — and what is done
Use the same endpoint before issuing a link (is there anything to connect?) and after (did they finish?). It is generated by the same code as the page, so what you see is what the person sees.skipped explains what is not on the page and why: provided_by_organization /
provided_by_system (already works without the person doing anything), or
no_credential_schema (the platform does not declare which values it needs — the squad’s
admin adds that in the platform settings and the item appears), or too_many_items (more
than 20 of one kind; trim the squad). An item with no declared fields stays hidden even
when already connected: the page cannot store that value, so it does not offer to remove
it either — that would be a one-way door. Items we provision automatically never appear at
all, since there is nothing for the person to enter. Stored values are never returned —
only connected and field names.Request a link
Requires
users:credentials, and the target must be an account you provisioned. If the
squad has nothing for the person to connect, the request returns 400 with the same
skipped list as connect-status.
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 shows All set when everything is connected, and a button back to your app
whenever you set
return_url — including when only some items are done, so the person can
come back and finish later.
Revoke links
When a person leaves your app, or you suspect a link leaked, invalidate every live link for that account at once. Idempotent.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 or disconnecting refreshes that user’s agent tools immediately.
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.
