Store an API key once
Any acting member, a teammate, co-owner or Owner, person or machine, can connect a new tool without changing Mainmind's code. Use an api-key/v1 account when the provider accepts an API key in an HTTP header. Mainmind stores the key encrypted and injects it into calls to the installed API destination. Subsequent bots use the alias; the key is absent from their permits, local environments and account receipts.
When a human holds the secret
An agent usually does not hold the key, and should not carry it through a chat. Add the account without it: call install_provider_connection with alias and profile and no credential. The account is written pending and the receipt returns a single-use placement_url, valid seven days, that the agent sends to an Owner or co-owner of the space.
That person opens the link in a browser signed in with GitHub. The page, titled Connect followed by the provider or alias and the space, shows where these details are sent, and who asked when an actor name was recorded — an install that recorded only a seat names nobody rather than presenting that seat as a person. Where they are sent is the exact origin, path and header for an API key; for a sign-in the token endpoint as well, and any header bound to a slot. All of it is fixed when the account was added and unchangeable afterwards. An account of keys for a tool (secret-slots/v1) takes values the same way. The page says no tool receives them and nobody can read them back. To use those values from a tool, remove the account and add it again as a driver a tool can call, as Move an account off secret-slots shows. Then one write-only input per field, each with a plain label and a one-line hint and never a prefilled value. When they press Connect, what they typed is stored encrypted on the server, the account becomes active, and the link stops working. Nothing typed is returned to the agent, logged, or written to a run receipt. A teammate, a member of another space, or anyone with a used, expired or invented link sees a page that says only that the link is not valid.
list_provider_connections lists a pending account under pending with who asked and when the link expires, and lists it as active once the values are placed; an Owner's or co-owner's start carries the same under open_placements, including a pending account of keys for a tool. Neither carries the link. An Owner or co-owner signed in to the dashboard can also place the values by alias with POST /api/tool-connections/<alias>/place, without a link.
The app's Accounts screen lists this space's own accounts and nothing else: one card per alias with its state, who added it, and each value name shown as saved or needed, never a value. One that is not connected yet carries Connect, including keys a tool keeps by name. An active api-key/v1 one carries Replace key. An active sign-in, app login, or named-keys account carries Replace values. Named keys say that no tool receives them and nobody can read them back. Any of them carries Remove behind a confirmation. Add an account adds one from the app through the same routes an agent uses. The built-in provider adapters (Amazon Selling Partner, Zoho, Shopify) appear there once connected, and under Add an account until then; Amazon signs in instead (Sign in with Amazon). A placed one carries Replace values: type only what changed, such as a client secret you rotated at the provider, and leave the rest blank to keep what is stored. It stays the same provider account, agents use the new values from their next call, and nothing about the account has to be removed first. Over HTTP it is PUT /api/tool-credentials with the generation the status route lists.
Add an API key from Accounts
Choose Add an account; API key is already chosen. Fill in:
- Name: what you call it, such as
Internal billing API. Tools call it by the name shown under it, hereinternal-billing-api. - Website: the API's address, such as
api.example.com. An https address works too. The key is sent only there. A path, a port other than 443, plain http or an IP address is refused. - Header: starts as
Authorization. Change it to the header the service reads, such asX-Api-Key, and clear Prefix for a header like that. The form clears it for you; type a prefix again if the service wants one. - Prefix: starts as
Bearer, which sendsAuthorization: Bearer <key>. Leave it empty when the service wants the key alone. - Key: the key itself.
Press Connect. Tools send the whole path on each call, such as /v1/invoices; the account's API root is /. One key covers one website: for a second website, add a second account.
If the service answers a call with 401, the card says Key refused: the service refused this key on that date, and Replace key takes a new one. Mainmind keeps only the date and the status, never the answer. The mark goes away after the next call that works, or once the key is replaced.
To remove an account, an Owner or co-owner calls revoke_provider_connection from their AI app, or chooses Remove on the account's card; over HTTP, DELETE /api/tool-connections/<alias> takes the machine bearer or an Owner's or co-owner's signed-in dashboard session. The alias is free again and the row stays as the audit record. Remove an account that is not connected yet when its destination is not the service you expect, rather than entering anything on it.
For example, this profile contains only configuration, so it can live with the tool's instructions:
{
"schema": "mainmind.connection-profile/v1",
"id": "new-api",
"driver": "api-key/v1",
"api_origin": "https://api.example.com",
"api_root": "/v1",
"authorization_header": "x-api-key",
"request_headers": ["content-type"],
"response_headers": ["content-type"]
}Replace the example origin and root with the provider's API address. For an API that requires Authorization: Bearer, set authorization_header to authorization and add authorization_scheme: "Bearer". Keep the credential out of this file. The credential object is exactly { "api_key": "…" }; the key must contain 8–4096 printable non-space ASCII characters.
When the agent itself holds the key outside a chat, install with install_provider_connection, passing an alias, the profile and the credential. This is write-only: the result returns field names and a non-secret credential_generation, never the key. An existing alias returns HTTP 409. Mainmind does not put the credential payload in its run ledger, but a chat host may retain the input. To keep the initial key out of a bot conversation, install without a credential and let a person type it on the placement page (When a human holds the secret), or send it directly over HTTPS to POST /api/tool-connections from a program that reads the key from a hidden prompt. Neither the Owner machine bearer nor the provider key belongs in a command argument or environment variable.
Then any acting member's bot calls list_provider_connections and uses the returned identifier:
{
"provider": "connection:new-api",
"method": "GET",
"path": "/items"
}Pass that object to call_provider. The request reaches https://api.example.com/v1/items with the key injected by Mainmind. Local tools can instead obtain issue_tool_permit and use the returned MAINMIND_CONNECTIONS_URL plus /new-api/items, authenticating with MAINMIND_CONNECTIONS_KEY. That value is a Mainmind permit, not the provider key. The tool must support this gateway URL; Mainmind does not host its executable code.
A request body must state its own type. call_provider forwards your body unchanged and adds no content-type of its own, so send one in headers — {"content-type": "application/json"} for JSON, or whatever the provider documents for that route. A body with no type is refused before the provider is contacted. This is not pedantry: a provider that will not parse an untyped body rarely says so, and at least one answers 200 having applied none of it, which is a write you are told succeeded and did not.
Replace a key with replace_provider_connection_credential, passing the alias, expected_credential_generation from the install receipt or fresh discovery, and the new {api_key} credential. Direct HTTPS uses PUT /api/tool-connections. To keep the new key out of a chat, an Owner or co-owner uses Replace key on the account's card on the Accounts screen instead.
Replacement never reads back the old key or changes the API destination. A stale generation returns 409. Old permits cannot use the replacement; local tools get a fresh permit. For MCP, inspect any previous operation receipt and start a fresh call without run_id; do not replay an uncertain provider write. Requests already sent cannot be recalled. If a replacement response is lost, check the current generation before retrying.
An Owner or co-owner can replace an oauth2-refresh/v1 account's values the same way, with the same generation check. A teammate cannot: those values sign in to the provider for the whole space, so a teammate asks one of them instead. The best route is Replace values on the app's Accounts screen, which keeps the refresh token out of a chat. Over MCP or HTTPS, send the whole credential, exactly as an install requires: client_id, client_secret, the new refresh_token, tenant_id when the profile binds one, and every declared slot. Or send {refresh_token} alone: the installed client, tenant and slots are kept, and never returned. A bound tenant_id must be the one the account was added with; to move to another tenant, revoke and install again. The alias, profile and grants stay as they are. The next call mints its access token from the new refresh token; the one cached from the old grant is not used again. An account added with secret-slots/v1 keeps its values with Mainmind. An Owner or co-owner places them with Connect while it is pending, or Replace values once it is connected. The values go into the store and are not shown back, and no tool receives them. To use it from a tool, revoke it and add the account again as an app login (sign-in/v1), an API key (api-key/v1) or a sign-in (oauth2-refresh/v1) with lease_env, as Move an account off secret-slots shows.
Install only the provider destination you intend to trust with its key. Mainmind does not follow redirects and refuses responses that reflect the key verbatim, but cannot protect against a malicious provider encoding a key into its own response. API-key accounts support one authentication header. OAuth refresh uses oauth2-refresh/v1. No tool permit carries an account's values: inject_access_token is refused for new accounts. secret-slots/v1 may be added, and its values placed or replaced; a permit still names it, and an account with inject_access_token, in connections_omitted and does not receive the values. When a refresh answers invalid_grant, the provider has revoked or expired the grant and retrying will not help. For Zoho, an owner or co-owner presses Sign in with Zoho. For another provider, they use Replace values on the Accounts screen, or replace the values over MCP or HTTPS as above. Revoking it and installing it again also works. A Google OAuth app left in Testing status expires its refresh tokens after 7 days, so publish it to Production. An unauthorized_client answer means the provider refused the OAuth client itself; fix the client with the provider, then replace the values the same way. Either way only calls through that account fail; the others keep working.
Sign in with Zoho
Press Sign in with Zoho. You approve Zoho Expense, Books, Inventory or Desk in the browser, and Mainmind stores the login. You do not create a client, and you do not handle a code.
The page is https://mainmind.app/oauth/zoho/connect. Add alias for an account that is already waiting, or provider=zoho-desk or provider=zoho-inventory for those built-in accounts. When you belong to more than one space, add workspace with the space’s name. A placement link for a Zoho account opens the same button.
Zoho Expense asks for full access to Expense. Books, Inventory and Desk each ask for that product only. India uses accounts.zoho.in. Other Zoho data centres use their own accounts host, and Mainmind picks the host from the account.
An agent can still pass credential {authorization_code} on replace_provider_connection_credential, with expected_credential_generation from list_provider_connections, when a code is already in hand. Built-in accounts — Zoho Desk, Zoho Inventory, Shopify and Amazon — use provider and that generation. The generation is a 64-character id, and the decimal generation still works. Mainmind checks that Zoho accepts the login, and only then replaces what is saved. A refusal keeps the previous login. The slot keeps one login, so there is no older one to restore.
Shopify and Amazon do not take a Zoho code. A replacement that Zoho, Shopify or Amazon refuses is not saved, and the previous login stays.
Access tokens from a saved login are kept until shortly before they expire, and every caller of that login shares them. A burst of calls does not mint a new token each time. When Zoho limits new tokens, Mainmind waits and says when to retry. The message includes Zoho’s own error, so a limit is distinct from a code Zoho rejected.
Adding, listing, API-key replacement and call_provider are open to the acting roles, teammate, co-owner and Owner; replacing oauth2-refresh/v1 values is for an Owner or co-owner. Any of them may add an account, passing its values when they hold them; when they do not, an Owner or co-owner places them (When a human holds the secret). A co-owner is an operational peer (Roles), so they hold the Owner's tool lease: the built-in provider adapters (Zoho, Amazon, Shopify), read and write, and every account, on the local permit and the server-side one alike. A teammate's local permit stays Git-only until each tool declares the capabilities it needs; their server-side permit carries the active accounts. Business actions still follow the space's Process and Authority.
Save Zoho Expense
Save a Zoho Expense login for India. An Owner or co-owner sends the account alias, the account type zoho-expense, the region in, and the three login fields client_id, client_secret and refresh_token.
Mainmind asks accounts.zoho.in, then reads Expense at www.zohoapis.in. The login is stored only when Zoho accepts it. The answer lists each field's length in bytes and its SHA-256, and it does not include the value. To change that login later, send the generation number from that answer with the new fields.
If Zoho does not accept the login, nothing new is stored and the previous save stays. Correct the three fields and send them again.
An agent holding the owner's connection calls issue_gateway_seal with alias, preset set to zoho-expense, and region set to in. Send no login fields. Mainmind answers with an address ending in /api/gateway-secrets/seal and a key. A shell posts one JSON object of the three fields to that address. The key goes in the header X-Mainmind-Seal-Key, not in the address and not in the body. Replace the capitals with the login Zoho already gave you. The answer is the same lengths and SHA-256. If Zoho does not accept the login, that link can be used again.
{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"refresh_token": "YOUR_REFRESH_TOKEN"
}Shopify, Amazon, Zoho Inventory and Zoho Desk still use Accounts. This does not move them.
An Owner or co-owner, already signed in, can send the same login without that link. The request is JSON to POST https://mainmind.app/api/gateway-secrets. Replace the capitals with the login Zoho already gave you.
{
"alias": "zoho-expense",
"preset": "zoho-expense",
"region": "in",
"fields": {
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"refresh_token": "YOUR_REFRESH_TOKEN"
}
}Move an account off secret-slots
An account added with secret-slots/v1 gave its values to a tool on your computer. No permit carries them now, so issue_tool_permit names that account in connections_omitted. To use it again, move it to a driver Mainmind calls for the tool:
- Before anything changes, note two things: the slot names
list_provider_connectionsshows for the old account, and the variable names the tool reads. The old slot names are gone once it is revoked. - Ask an Owner or co-owner to change the password or secret at the provider. Earlier permits copied the old values onto computers, and those copies keep working until they are changed. Remove the old values from the tool's settings.
- An Owner or co-owner revokes the old account with
revoke_provider_connection. Its alias is free again once it is revoked. - Install it again with no
credential, in one of the shapes below. The receipt carries aplacement_url. Send it to an Owner or co-owner, say which values go in each box (the shapes below say), and they paste the new values once. - Call
issue_tool_permit. Itscommands.environmentsets thelease_envnames.base_urlis this account's address on Mainmind, standing forapi_originplusapi_root: the tool adds its usual paths to it.ticketis the permit: the tool sends it as a Bearer token in itsAuthorizationheader, where it used to send its own token. Each sentinel is a name the tool insists is set, such as a password variable it checks before it starts, and gets the textmainmind. Use the names the tool reads. A permit lasts at most an hour; callissue_tool_permitagain for a new one. - The tool skips its own login: Mainmind logs in and adds the real token to each call. A call through Mainmind to the account's login address is refused before it leaves, so the tool's placeholder text never reaches the login. A tool that cannot take its API address from a variable needs that change first.
- Make one read call through the tool to check it.
An account that hands out a token after a login, with an email and password or a client ID and secret, is sign-in/v1. Mainmind sends those values only to the login address, which must be on the same origin as api_origin, and never puts them in an API call.
Shiprocket signs in with an API user, not the login you use for the Shiprocket panel. An Owner or co-owner makes one under Settings, then API, then Configure, with its own email and password, and pastes those:
{
"alias": "shiprocket",
"profile": {
"schema": "mainmind.connection-profile/v1",
"id": "shiprocket",
"driver": "sign-in/v1",
"slots": ["SHIPROCKET_EMAIL", "SHIPROCKET_PASSWORD"],
"sign_in": {
"url": "https://apiv2.shiprocket.in/v1/external/auth/login",
"format": "json",
"fields": { "email": "SHIPROCKET_EMAIL", "password": "SHIPROCKET_PASSWORD" },
"token_field": "token"
},
"api_origin": "https://apiv2.shiprocket.in",
"api_root": "/v1/external",
"request_headers": ["content-type"],
"response_headers": ["content-type"],
"lease_env": {
"base_url": "SHIPROCKET_BASE_URL",
"ticket": "SHIPROCKET_TOKEN",
"sentinels": ["SHIPROCKET_PASSWORD"]
}
}
}FedEx signs in with the API Key and Secret Key of a project in the FedEx Developer Portal: paste the API Key as the client ID and the Secret Key as the client secret. Its account number goes in request bodies, not in the login, so it is not one of the pasted values: the tool keeps it in its own settings. An account number is not a key.
{
"alias": "fedex",
"profile": {
"schema": "mainmind.connection-profile/v1",
"id": "fedex",
"driver": "sign-in/v1",
"slots": ["FEDEX_CLIENT_ID", "FEDEX_CLIENT_SECRET"],
"sign_in": {
"url": "https://apis.fedex.com/oauth/token",
"format": "form",
"fields": { "client_id": "FEDEX_CLIENT_ID", "client_secret": "FEDEX_CLIENT_SECRET" },
"constants": { "grant_type": "client_credentials" },
"token_field": "access_token",
"expires_in_field": "expires_in"
},
"api_origin": "https://apis.fedex.com",
"api_root": "/",
"request_headers": ["content-type"],
"response_headers": ["content-type"],
"lease_env": { "base_url": "FEDEX_BASE_URL", "ticket": "FEDEX_ACCESS_TOKEN" }
}
}Mainmind keeps that login's token and uses it on later calls until the login says it expires. When the login does not say, the token is kept for ten days. Shiprocket's login does not say, and its token lasts ten days. One login runs at a time, so two calls together do not sign in twice.
A call the account answers with 401 signs in again. A login the account answers with 429, or with 500 through 599, or that times out or cannot be reached, is tried once more and does not stop the account. The password is sent exactly as it was saved. A login that answers 401 or 403 stops the account, unless that answer says the problem is the token, such as invalid token. Accounts then shows that status, a short message with any password or token removed, and when a login last succeeded. Details lists recent logins and calls, with the time and the outcome, and never a password or token. An Owner or co-owner presses Try the saved login to use the stored password once, or Replace values to enter a new one.
An owner or co-owner's assistant can try that saved login without anyone typing the password into the chat. It calls retry_provider_sign_in with the account's alias. When the saved login works, the next call goes through. When the account refuses it again, the assistant does not type the password into the chat and does not ask for another try. It asks for a reseal link and posts the current password to that link from the environment.
To enter a new password or API key, the assistant calls replace_provider_connection_credential with request_reseal_link set and no values. Mainmind answers with one link that lasts ten minutes. The assistant posts one JSON object to that link from the environment. The keys are the names the account already uses, such as SHIPROCKET_PASSWORD or api_key, and the values never go through the chat. The link works once.
Add any other header the tool must send to request_headers.
An account that signs in with OAuth and a refresh token, such as Zoho or Google Ads, is oauth2-refresh/v1: the same steps apply, with header_slots for any value the provider wants in a header and lease_env for the tool, as in the Google Ads shape under Optional tasks and automatic provider receipts. If it needs a new refresh token, follow Get a new refresh token.
Get a new refresh token
When the Accounts screen says a login stopped, the provider no longer accepts the sign-in behind that connection. For Zoho, press Sign in with Zoho on that card. For another provider, sign in again in the same app that connected it the first time, with the same client. An owner or co-owner then uses Replace values on the card. If you did not set it up, send this section to whoever did, or ask your AI assistant to walk you through it.
A Google app left in Testing status expires its refresh tokens after 7 days. Publish it to Production in the Google Cloud console so the next one lasts.
If the card says the provider stopped accepting the connection's client, the client ID or secret no longer works. Fix or create the client with the provider, sign in again with that client, then enter the new values in Replace values.
Sign in with Amazon
The built-in Amazon Selling Partner account does not need a refresh token typed in. An Owner or co-owner signs in with Amazon, and Mainmind gets the refresh token itself.
- Open Accounts. Choose Add an account, then Amazon Selling Partner, or Sign in again on the Amazon card when it is already connected.
- In your app in Seller Central, add the two addresses the dialog shows:
https://mainmind.app/oauth/amazon/loginas the OAuth Login URI andhttps://mainmind.app/oauth/amazon/callbackas the OAuth Redirect URI. - Choose the Amazon store you sell on, then paste your app's ID (
amzn1.sp.solution.…), client ID and client secret. They are under Seller Central, Apps and Services, Develop Apps, your app, View (LWA credentials). When Amazon is already connected, leave the client ID and secret blank to keep the ones saved. - Press Sign in with Amazon, approve the app in Seller Central, and you come back to a page that says Amazon is connected. If Amazon sends the sign-in somewhere else, that page names the HTTP status and api.amazon.com, nothing is stored, and you start again from Accounts.
The store sets the region: India and the European stores use Europe, the Americas use North America, and Japan, Australia and Singapore use the Far East. Signing in again must use a store in the same region as the account already connected. A sign-in works once, for fifteen minutes, and only in the browser that started it. Your agent can use Amazon from its next call. Over HTTP the start is POST /api/tool-credentials/amazon/sign-in; see the HTTP API.