# Start a connection sign-in from your own button

**POST** `/api/embed/v1/accounts/{accountId}/connections/oauth2/start`

Starts a customer’s sign-in to a service from your own Connect button. No GoRunner screen.

Needs a partner key: `Authorization: Bearer gre_…`, from [Platform › Partner keys](https://docs.gorunner.io/embed/platform-screens#partner-keys). Call it from your server only.

Embed · Self-hosted with an Embed license

## Parameters

### Path

- `accountId` · Required · string: Your ID for the customer’s account: 1 to 255 bytes of UTF-8, case-sensitive. URL-encode it: it may contain `/`.

### Body

- `pieceId` · Optional · string: Needed for a new connection: an app that signs in, such as `gmail`. A trigger app is made under its action app. Optional with `connectionId`.
- `returnUrl` · Required · URL: Where the browser comes back to: https (http only for localhost), on exactly one of the installation’s allowed websites or this account’s own; a wildcard entry allows none. No user name, password or `#fragment`. Up to 2,048 characters.
- `name` · Optional · string: Up to 255 characters. Left blank: “Service · account” once the account is known.
- `connectionId` · Optional · UUID: Sign a customer’s connection in again instead. Not one you manage: replace those with Create or replace a managed connection.
- `workspaceId` · Optional · string: The workspace, by your ID. Left out: the default workspace.

**What are the rarely used parameters?**

- `config` · Optional · object: Your own app instead of the installation’s: `clientId`, `clientSecret`, `scope`.
- `mode` · Optional · string: Always `redirect`.

## The sign-in

`authorizeUrl` is the service’s own consent page, with the installation’s app unless `config` names yours. It works once, for 10 minutes, and only for this key, account and app.

When the service sends the browser back, the connection is made as one made in the embed (`managedBy: customer`), audited as your key, and your page hears `connection.created`. Then the browser goes to `returnUrl` with `status=connected&connectionId=…&attemptId=…`, or `status=error&reason=…`. Never a token in that address.

Read the outcome with [Check how a connection sign-in ended](https://docs.gorunner.io/embed/api/connections/get-connection-sign-in). Use `connectionId` in a workflow step.

## Retries

It takes no `Idempotency-Key`: a stored answer would hand back a link already used. A retry starts a new sign-in; the unused one runs out.

## Responses

- 200 OK

  Started: send the customer’s browser to `authorizeUrl`.

- 400 `VALIDATION_ERROR`

  A field isn’t valid as written. `details.field` names it.

- 403 `PIECE_NOT_ALLOWED`

  This account may not use the app.

- 403 `MANAGED_CONNECTION`

  `connectionId` names a connection you manage.

- 404 `NOT_FOUND`

  No such account, workspace or connection.

- 409 `ACCOUNT_DELETED`

  The account is deleted. Nothing starts in it.

- 422 `RETURN_URL_NOT_ALLOWED`

  `returnUrl` isn’t on an allowed website (`details.origin`).

- 422 `VALIDATION_ERROR`

  There is no app to sign in with for this service.

- 500 `INTERNAL_ERROR`

  Something failed on the installation’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.

Any call can also answer: [`401 UNAUTHORIZED`](https://docs.gorunner.io/errors#UNAUTHORIZED) · [`403 EMBED_NOT_LICENSED`](https://docs.gorunner.io/errors#EMBED_NOT_LICENSED) · [`403 IP_NOT_ALLOWED`](https://docs.gorunner.io/errors#IP_NOT_ALLOWED) · [`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). See [Errors](https://docs.gorunner.io/embed/api#errors).

Request

**curl**

```bash
GORUNNER="https://automations.example.com"
GORUNNER_EMBED_KEY="YOUR_PARTNER_KEY"  # gre_…
ACCOUNT_ID="acct-bakery"

URL="$GORUNNER/api/embed/v1/accounts/$ACCOUNT_ID/"
URL="${URL}connections/oauth2/start"
curl -X POST "$URL" \
  -H "Authorization: Bearer $GORUNNER_EMBED_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "pieceId": "gmail",
    "returnUrl": "https://app.bakery.example/integrations/done?user=ada-123"
  }'
```

**JavaScript**

```js
const GORUNNER = "https://automations.example.com";
const KEY = process.env.GORUNNER_EMBED_KEY; // gre_…
const ACCOUNT_ID = encodeURIComponent("acct-bakery");

const res = await fetch(
  `${GORUNNER}/api/embed/v1/accounts/` +
    `${ACCOUNT_ID}/connections/oauth2/start`,
  {
    method: "POST",
    headers: {
        Authorization: `Bearer ${KEY}`,
        "Content-Type": "application/json",
      },
    body: JSON.stringify({
        pieceId: "gmail",
        returnUrl: "https://app.bakery.example/integrations/done?user=ada-123",
      }),
  },
);
const { data } = await res.json();
```

**Python**

```python
import os, requests
from urllib.parse import quote

GORUNNER = "https://automations.example.com"
KEY = os.environ["GORUNNER_EMBED_KEY"]  # gre_…
ACCOUNT_ID = quote("acct-bakery", safe="")

res = requests.post(
    f"{GORUNNER}/api/embed/v1/accounts/"
    f"{ACCOUNT_ID}/connections/oauth2/start",
    headers={"Authorization": f"Bearer {KEY}"},
    json={
      "pieceId": "gmail",
      "returnUrl": "https://app.bakery.example/integrations/done?user=ada-123",
    },
)
data = res.json()["data"]
```

Response · 200 OK

```json
{
  "data": {
    "attemptId": "Zq3XbN0iJm7dQe8TfR2wLg",
    "authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth?…&state=…",
    "expiresAt": "2026-10-25T09:10:00Z"
  }
}
```

**Every field**

### Body

- `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`.
- `workspaceId` · Optional · string: The workspace the connection goes in (the partner’s id; `default` names the default one). Omitted = the account’s default workspace; the empty value is refused.

### Response · 200 OK

- `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 how a connection sign-in ended](https://docs.gorunner.io/embed/api/connections/get-connection-sign-in) · [Your customers’ connections](https://docs.gorunner.io/embed/customer-connections) · [Allow a website for one account](https://docs.gorunner.io/embed/api/websites/add-account-website)

Source: https://docs.gorunner.io/embed/api/connections/start-connection-sign-in
