Start a sign-in for a new connection
POST/
Starts a sign-in to an app’s service. The connection is made only once the sign-in works.
Needs an API token with the Write permission for connections (connection:write).
Parameters
Body
From an API token: pieceId (or connectionId) and returnUrl.
pieceIdOptional · stringAn app that signs in, such as
gmail. A trigger app is made under its action app.nameOptional · stringUp to 255 characters. Left blank: “Service · account” once the account is known. Not with
connectionId.connectionIdOptional · UUIDSign a saved connection in again instead. It keeps its name and app. Not one the partner manages.
returnUrlOptional · URLWhere the browser comes back to: https (http only for localhost), on the installation’s own dashboard or one of its allowed websites. Up to 2,048 characters.
What are the rarely used parameters?
configOptional · objectYour own app instead of the installation’s:
clientId,clientSecret,scope.modeOptional · stringLeave it out, or send
redirect. GoRunner’s own pages sendpopuportab, which a token can’t.returnToOptional · stringIgnored for a token. GoRunner’s own pages use it.
The sign-in
Send the person who should sign in to authorizeUrl: the service’s own consent page. The link works once, for 10 minutes. Give it to nobody else.
When the service sends the browser back, GoRunner makes the connection, then redirects to returnUrl with status=connected&connectionId=…&attemptId=…, or status=error&reason=…. No token is ever in that address, and your own query is kept.
A sign-in that is refused, fails or is abandoned makes nothing.
The return address
Its origin must be exactly the installation’s own dashboard, one of its allowed websites, or, for an embedded customer’s workspace, one of that account’s own. A wildcard entry allows none. On Cloud, the dashboard is the only choice. See Your customers’ connections.
It takes no Idempotency-Key: a retry starts a new sign-in, and an unused one runs out.
Responses
- 200OK
From a token:
attemptId,authorizeUrlandexpiresAt. The app, the name,
returnUrlorconnectionIdisn’t usable as written, or a name orconfigcame withconnectionId.details.fieldnames it.- 403
FORBIDDENThe token lacks the Write permission for connections.
An AI app signed in with OAuth. Signing in needs a person in a browser, or an API token.
The partner manages this connection through its own API, so it can’t be changed here.
This workspace may not use the app.
The workspace’s account is suspended. Nothing starts until it is resumed.
- 404
NOT_FOUNDconnectionIdisn’t in this workspace. The workspace’s account was deleted.
The app has nothing to sign in with, or its token address isn’t one this installation sends to.
returnUrlisn’t on an allowed website.details.originnames it.Something failed on GoRunner’s side. Try again.
- 503
UNAVAILABLESign-ins aren’t available on this server now, or the allowed websites couldn’t be read. Nothing started.
GoRunner couldn’t check which apps this workspace may use. Nothing changed. Try again.
The account’s state couldn’t be read. Nothing started. Try again.
Any call can also answer: 401 UNAUTHORIZED · 401 TOKEN_ENDED · 403 TOKEN_SURFACE_OFF · 403 FEATURE_OFF · 403 EMBED_NOT_ALLOWED · 403 EMBED_NOT_AVAILABLE · 413 PAYLOAD_TOO_LARGE · 415 INVALID_CONTENT_TYPE · 429 RATE_LIMITED · 503 BUSY · 503 PAUSED · 503 SERVICE_UNAVAILABLE. See Errors.
Request
GORUNNER="https://api.gorunner.io"
GORUNNER_TOKEN="grt_…" # needs Write for connections
curl -X POST "$GORUNNER/api/connections/oauth2/start" \
-H "Authorization: Bearer $GORUNNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pieceId": "gmail",
"name": "Gmail · Ada",
"returnUrl": "https://app.bakery.example/integrations/done?user=u_123"
}'const GORUNNER = "https://api.gorunner.io";
const TOKEN = process.env.GORUNNER_TOKEN;
const res = await fetch(
`${GORUNNER}/api/connections/oauth2/start`,
{
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
pieceId: "gmail",
name: "Gmail · Ada",
returnUrl: "https://app.bakery.example/integrations/done?user=u_123",
}),
},
);
const { data } = await res.json();import os, requests
GORUNNER = "https://api.gorunner.io"
TOKEN = os.environ["GORUNNER_TOKEN"]
res = requests.post(
f"{GORUNNER}/api/connections/oauth2/start",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"pieceId": "gmail",
"name": "Gmail · Ada",
"returnUrl": "https://app.bakery.example/integrations/done?user=u_123",
},
)
data = res.json()["data"]Response · 200 OK
{
"data": {
"attemptId": "Zq3XbN0iJm7dQe8TfR2wLg",
"authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&client_id=…&code_challenge=…&code_challenge_method=S256&prompt=consent&redirect_uri=…&response_type=code&scope=…&state=…",
"expiresAt": "2026-10-25T09:10:00Z"
}
}Every field
The body takes one of 2 forms.
Body, from GoRunner’s own pages
pieceIdRequired · stringAn app that signs in (declares OAuth2, or the generic
oauth2). A trigger’s app is made under its action app.nameOptional · stringOptional. Blank: "{Service} · {account}" once the account is known.
configOptional · objectOnly an own app’s
clientId,clientSecret,scope(and the generic app’sauthUrl,tokenUrl). Sealed into the attempt.modeOptional · stringOne of
popup,tab. Defaultpopup.returnToOptional · stringRequired with
mode: tab: the dashboard path to come back to (one leading/, no host).
Body, from an API token
pieceIdOptional · stringRequired for a new connection: an app that signs in (declares OAuth2, or the generic
oauth2). A trigger’s app is made under its action app (gmailtrigger→gmail). WithconnectionId, optional and must be that connection’s.nameOptional · stringA new connection’s name. Blank: "{Service} · {account}" once the account is known. Not with
connectionId.connectionIdOptional · UUIDSign this existing connection (in the caller’s workspace) in again; it keeps its name and its app. Not a partner-managed one.
returnUrlRequired · URLWhere the browser comes back to: an absolute https address (http only for localhost), no user info or
#fragment, whose origin is exactly an allowed website (or, with an API token onPOST /api/connections/oauth2/start, this installation’s own app address). Its own query is kept;status,connectionId/reasonandattemptIdare set.modeOptional · stringOne of
redirect. Defaultredirect.configOptional · objectYour own app instead of the installation’s:
clientId,clientSecret,scope(and the generic app’sauthUrl,tokenUrl), and an app’sbaseUrl. Sealed into the attempt. Not withconnectionId.
Response · 200 OK, to GoRunner’s own pages
dataobjectattemptIdstringNames this sign-in in the launch handshake and in the status route.
launchUrlURLPoint the popup opened on the click here. It is useless on its own: the page asks its opener for
bindSecretand goes nowhere without it.bindSecretstringSingle-use. Give it only to the popup, by postMessage to
apiOrigin, when it asks. Never put it in a URL or storage.apiOriginstringThe exact origin the launch and result pages are served on. Accept messages only from the popup AND this origin.
providerstringThe provider key of the app’s declared authorize URL (
google,github, …); empty for the genericoauth2app.redirectUriURLThe callback this server is redirected back to (
{PUBLIC_API_URL}/api/oauth2/callback). Register exactly this with the provider. It is a browser endpoint returning HTML; you never call it.expiresAttimeWhen this attempt stops being usable (15 minutes after the press).
modestringHow the window asked to sign in:
tabis opened withPOST /api/connections/oauth2/attempts/{attemptId}/go.One of
popup,tab.
Response · 200 OK, to an API token
dataobjectattemptIdstringNames this sign-in: read it with the same credential, and it comes back on
returnUrl.authorizeUrlURLThe provider’s consent page, complete: its
stateis single-use and bound to this attempt. Send the person who should sign in there, and nobody else.expiresAttimeWhen the link stops working (10 minutes after the start).