Connect the organisation for server-side work
const url = 'https://api.sloose.com/auth/orgs/org_9f3c/connect';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"return":"example","environment":"production","mode":"popup","crmOrg":"4400000001","connectorId":"cnn_9f2a"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.sloose.com/auth/orgs/org_9f3c/connect \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "return": "example", "environment": "production", "mode": "popup", "crmOrg": "4400000001", "connectorId": "cnn_9f2a" }'Returns a URL that starts Zoho’s offline-scope grant — this is how an organisation connects a CRM. The callback reads which CRM authorised, makes a connection for it — another organisation holding the same CRM is no reason to refuse; each organisation connects with its own grant — stores the refresh token on it so the server can read the CRM unattended, and reads the CRM’s modules and fields.
A sandbox or a developer account is connected the same way: pick it on Zoho’s screen and it becomes a connection like any other, counted under the plan’s connections_max. The callback asks each of Zoho’s hosts — the standard one, a sandbox’s and a developer account’s — which organisation the grant is for; when two answer for different organisations, or for one that does not say what it is, the page the grant is running in asks the person which one they chose (POST /auth/zoho/connect/choose); a grant whose answer says type: sandbox is a sandbox and asks nothing.
An organisation may hold several connections — a client’s CRM each, say — as many as the plan’s connections_max. Adding an organisation the organisation already holds as a live connection is refused (409 in the window, or ?connect_error=), naming that connection, whose Reconnect is how its grant is renewed. A sandbox is a separate connection with an organisation id of its own, named as a sandbox, and kept on Zoho’s sandbox host when that host answers for it. Adding one the organisation holds only as a disconnected connection brings that connection back, keeping its id. Any other is a new one. A disconnected connection holds no slot. To renew one connection in particular — its grant expired, or was revoked at Zoho — send its connectorId: the grant lands on that row, keeping its id, or is refused when Zoho answers for a different organisation.
Only an administrator can connect. No data centre is chosen: the grant starts on the accounts host of the data centre the person last signed in to Zoho on — so Zoho does not ask them to sign in again — or on Zoho’s US host for a person who has never signed in with Zoho, which sends them to their own centre; and the callback is told which one issued the code. The refresh token is encrypted at rest and never leaves the server.
How the outcome comes back depends on mode. In a window of its own (popup, the default, which a CRM tab uses because it cannot navigate the CRM away) the grant ends on a page that posts { type: "sloose:connected", orgId, connectorId } or { type: "sloose:auth-error", message } to its opener. Outside a CRM tab (redirect) the page itself goes to Zoho, and the callback sends it back to return — the page’s full address — with ?connected=<connection id> or ?connect_error=<why> added; the app says an outcome only when the address also carries the nonce it left in its own tab, because the address is anybody’s to write.
return may be on the widget origin or the account origin, because the settings page that starts this renders in both — wider than a sign-in, which posts a session token back and is the widget’s alone.
Session only: connecting needs a person at Zoho’s consent screen, and managing connections is never an organisation’s API token’s to do (D5).
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”The organisation id the session was issued for. A token for one organisation can never read another.
Example
org_9f3cThe organisation id the session was issued for. A token for one organisation can never read another.
Query Parameters
Section titled “Query Parameters”Alternative to the body field.
Alternative to the body field.
Request Body
Section titled “Request Body”object
Where the outcome goes, on the widget origin or the account origin — settings render in both. With mode: popup, the origin the popup posts its outcome to. With mode: redirect, the FULL address of the page to come back to, whose origin is one of those two; its hash is dropped. Also accepted as a ?return= query parameter.
Ignored, and kept only so a caller that still sends it is not refused: which of Zoho’s hosts a grant answers on — a sandbox’s, a developer account’s, or the standard one — is found by the callback asking each, and a sandbox is found by the answer’s own type.
popup (the default): the grant runs in a window of its own and ends by posting to its opener — what a CRM tab does, since it cannot navigate the CRM away. redirect: the page itself goes to the returned URL and comes back to return, which is then the FULL address of that page, with ?connected=<connection id> or ?connect_error=<why> added — what the web app does, where a popup is only a smaller, blockable tab (design §13.3).
The Zoho CRM organisation (its id) the connect is for, when the caller knows it — a CRM tab knows the organisation it is framed in. The callback names it to Zoho (X-CRM-ORG), which a person in several organisations needs: a call naming none goes to their default organisation. Zoho’s own answer still decides which organisation the grant is for, and must be this one.
Example
4400000001Reconnect this connection — one of the organisation’s own, from GET /orgs/{org}/connectors — rather than add one. The grant renews that row and no other, so its id, which templates, runs and imports name, never changes; and when the organisation chosen on Zoho’s screen is not this connection’s, the outcome says so and nothing is written.
Example
cnn_9f2aResponses
Section titled “Responses”Where to send the administrator.
object
Open this in a window of its own (popup), or go to it (redirect).
Examplegenerated
{ "url": "example"}The request — its body or its query — did not match the schema. issues carries the Zod issue list.
The error envelope every non-2xx answer uses.
object
Human-readable explanation.
Machine-readable reason. Absent on a few legacy 400s.
Examplegenerated
{ "error": "example", "code": "example"}No bearer, or one that no longer resolves: a session that is unknown or expired (code: AUTH_REQUIRED), or an API token that was revoked or expired (TOKEN_REVOKED, TOKEN_EXPIRED), whose organisation is gone (ORG_NOT_FOUND) or whose member or user row is (MEMBERSHIP_GONE).
The error envelope every non-2xx answer uses.
object
Human-readable explanation.
Machine-readable reason. Absent on a few legacy 400s.
Examplegenerated
{ "error": "example", "code": "example"}The bearer’s role is too low, it was issued for a different organisation, or it is an API token (SESSION_REQUIRED). Like every route behind a session: a session signed in but in no organisation is refused code: NO_ORG, a banned person code: BANNED, and a closed organisation code: ORG_CLOSED.
The error envelope every non-2xx answer uses.
object
Human-readable explanation.
Machine-readable reason. Absent on a few legacy 400s.
Examplegenerated
{ "error": "example", "code": "example"}No such connection, or it is not visible to this session.
The error envelope every non-2xx answer uses.
object
Human-readable explanation.
Machine-readable reason. Absent on a few legacy 400s.
Examplegenerated
{ "error": "example", "code": "example"}ZOHO_CLIENT_ID is not configured.
The error envelope every non-2xx answer uses.
object
Human-readable explanation.
Machine-readable reason. Absent on a few legacy 400s.
Examplegenerated
{ "error": "example", "code": "example"}The restore switch is on (code: MAINTENANCE): every route but /health and /robots.txt is refused before anything is read, except the operator’s own POST /staff/people/{id}/delete with the ADMIN_TOKEN. Sloose is down for maintenance. Please try again in a few minutes.
object
Example
{ "code": "MAINTENANCE"}Headers
Section titled “Headers”Seconds until asking again is worth it (300 while the switch is on).