# Start a sign-in for a new connection

**POST** `/api/connections/oauth2/start`

Starts a sign-in to an app’s service. The connection is made only once the sign-in works.

Needs [an API token](https://docs.gorunner.io/api/authentication#scopes) with the **Write** permission for connections (`connection:write`).

## Parameters

### Body

From an API token: `pieceId` (or `connectionId`) and `returnUrl`.

- `pieceId` · Optional · string: An app that signs in, such as `gmail`. A trigger app is made under its action app.
- `name` · Optional · string: Up to 255 characters. Left blank: “Service · account” once the account is known. Not with `connectionId`.
- `connectionId` · Optional · UUID: Sign a saved connection in again instead. It keeps its name and app. Not one the partner manages.
- `returnUrl` · Optional · URL: Where 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?**

- `config` · Optional · object: Your own app instead of the installation’s: `clientId`, `clientSecret`, `scope`.
- `mode` · Optional · string: Leave it out, or send `redirect`. GoRunner’s own pages send `popup` or `tab`, which a token can’t.
- `returnTo` · Optional · string: Ignored 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](https://docs.gorunner.io/embed/customer-connections).

It takes no `Idempotency-Key`: a retry starts a new sign-in, and an unused one runs out.

## Responses

- 200 OK

  From a token: `attemptId`, `authorizeUrl` and `expiresAt`.

- 400 `VALIDATION_ERROR`

  The app, the name, `returnUrl` or `connectionId` isn’t usable as written, or a name or `config` came with `connectionId`. `details.field` names it.

- 403 `FORBIDDEN`

  The token lacks the **Write** permission for connections.

- 403 `BROWSER_REQUIRED`

  An AI app signed in with OAuth. Signing in needs a person in a browser, or an API token.

- 403 `MANAGED_CONNECTION`

  The partner manages this connection through its own API, so it can’t be changed here.

- 403 `PIECE_NOT_ALLOWED`

  This workspace may not use the app.

- 403 `ACCOUNT_SUSPENDED`

  The workspace’s account is suspended. Nothing starts until it is resumed.

- 404 `NOT_FOUND`

  `connectionId` isn’t in this workspace.

- 409 `ACCOUNT_DELETED`

  The workspace’s account was deleted.

- 422 `VALIDATION_ERROR`

  The app has nothing to sign in with, or its token address isn’t one this installation sends to.

- 422 `RETURN_URL_NOT_ALLOWED`

  `returnUrl` isn’t on an allowed website. `details.origin` names it.

- 500 `INTERNAL_ERROR`

  Something failed on GoRunner’s side. Try again.

- 503 `UNAVAILABLE`

  Sign-ins aren’t available on this server now, or the allowed websites couldn’t be read. Nothing started.

- 503 `PIECES_UNAVAILABLE`

  GoRunner couldn’t check which apps this workspace may use. Nothing changed. Try again.

- 503 `STATE_UNAVAILABLE`

  The account’s state couldn’t be read. Nothing started. Try again.

Any call can also answer: [`401 UNAUTHORIZED`](https://docs.gorunner.io/errors#UNAUTHORIZED) · [`401 TOKEN_ENDED`](https://docs.gorunner.io/errors#TOKEN_ENDED) · [`403 TOKEN_SURFACE_OFF`](https://docs.gorunner.io/errors#TOKEN_SURFACE_OFF) · [`403 FEATURE_OFF`](https://docs.gorunner.io/errors#FEATURE_OFF) · [`403 EMBED_NOT_ALLOWED`](https://docs.gorunner.io/errors#EMBED_NOT_ALLOWED) · [`403 EMBED_NOT_AVAILABLE`](https://docs.gorunner.io/errors#EMBED_NOT_AVAILABLE) · [`413 PAYLOAD_TOO_LARGE`](https://docs.gorunner.io/errors#PAYLOAD_TOO_LARGE) · [`415 INVALID_CONTENT_TYPE`](https://docs.gorunner.io/errors#INVALID_CONTENT_TYPE) · [`429 RATE_LIMITED`](https://docs.gorunner.io/errors#RATE_LIMITED) · [`503 BUSY`](https://docs.gorunner.io/errors#BUSY) · [`503 PAUSED`](https://docs.gorunner.io/errors#PAUSED) · [`503 SERVICE_UNAVAILABLE`](https://docs.gorunner.io/errors#SERVICE_UNAVAILABLE). See [Errors](https://docs.gorunner.io/api/errors#by-status).

Request

**curl**

```bash
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"
  }'
```

**JavaScript**

```js
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();
```

**Python**

```python
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

```json
{
  "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

- `pieceId` · Required · string: An app that signs in (declares OAuth2, or the generic `oauth2`). A trigger’s app is made under its action app.
- `name` · Optional · string: Optional. Blank: "{Service} · {account}" once the account is known.
- `config` · Optional · object: Only an own app’s `clientId`, `clientSecret`, `scope` (and the generic app’s `authUrl`, `tokenUrl`). Sealed into the attempt.
- `mode` · Optional · string: One of `popup`, `tab`. Default `popup`.
- `returnTo` · Optional · string: Required with `mode: tab`: the dashboard path to come back to (one leading `/`, no host).

### Body, from an API token

- `pieceId` · Optional · string: Required 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`). With `connectionId`, optional and must be that connection’s.
- `name` · Optional · string: A new connection’s name. Blank: "{Service} · {account}" once the account is known. Not with `connectionId`.
- `connectionId` · Optional · UUID: Sign this existing connection (in the caller’s workspace) in again; it keeps its name and its app. Not a partner-managed one.
- `returnUrl` · Required · URL: Where 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 on `POST /api/connections/oauth2/start`, this installation’s own app address). Its own query is kept; `status`, `connectionId`/`reason` and `attemptId` are set.
- `mode` · Optional · string: One of `redirect`. Default `redirect`.
- `config` · Optional · object: Your own app instead of the installation’s: `clientId`, `clientSecret`, `scope` (and the generic app’s `authUrl`, `tokenUrl`), and an app’s `baseUrl`. Sealed into the attempt. Not with `connectionId`.

### Response · 200 OK, to GoRunner’s own pages

- `data` · object
  - `attemptId` · string: Names this sign-in in the launch handshake and in the status route.
  - `launchUrl` · URL: Point the popup opened on the click here. It is useless on its own: the page asks its opener for `bindSecret` and goes nowhere without it.
  - `bindSecret` · string: Single-use. Give it only to the popup, by postMessage to `apiOrigin`, when it asks. Never put it in a URL or storage.
  - `apiOrigin` · string: The exact origin the launch and result pages are served on. Accept messages only from the popup AND this origin.
  - `provider` · string: The provider key of the app’s declared authorize URL (`google`, `github`, …); empty for the generic `oauth2` app.
  - `redirectUri` · URL: The 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.
  - `expiresAt` · time: When this attempt stops being usable (15 minutes after the press).
  - `mode` · string: How the window asked to sign in: `tab` is opened with `POST /api/connections/oauth2/attempts/{attemptId}/go`. One of `popup`, `tab`.

### Response · 200 OK, to an API token

- `data` · object
  - `attemptId` · string: Names this sign-in: read it with the same credential, and it comes back on `returnUrl`.
  - `authorizeUrl` · URL: The provider’s consent page, complete: its `state` is single-use and bound to this attempt. Send the person who should sign in there, and nobody else.
  - `expiresAt` · time: When the link stops working (10 minutes after the start).

Related: [Check a sign-in](https://docs.gorunner.io/api/connections/get-connection-oauth2-attempt-any) · [Create a connection from code](https://docs.gorunner.io/api/create-a-connection) · [Start a connection sign-in from your own button](https://docs.gorunner.io/embed/api/connections/start-connection-sign-in)

Source: https://docs.gorunner.io/api/connections/start-connection-oauth2-draft
