# Create a connection from code

Find the fields an app needs, then create the connection, or send a person to sign in.

## You need

- **A token that may add connections**: **Custom**, with `connection:read` and `connection:write`. The three ready-made choices have `connection:read` but not `connection:write`. [Permissions](https://docs.gorunner.io/api/authentication#scopes)
- **The app’s ID**: Such as `seatable` or `gmail`, from [List apps](https://docs.gorunner.io/api/apps/list-apps) or the app’s page.

## 1. Read the app’s fields

[Get an app](https://docs.gorunner.io/api/apps/get-app) says what a connection holds, in `auth`:

- `required`: Whether the app uses a connection at all.
- `type`: `none`, `api_key`, `bearer_token`, `basic_auth`, `oauth2` or `custom`.
- `fields[]`: Each one’s `name`, `title`, `type` and `required`. A `password` field is a secret.
- `oauth2`: There only when the app signs in with a browser: its `authorizationUrl`, `tokenUrl` and `scopes`.

An app with `auth.oauth2` signs in, whatever its `type` (Gmail’s is `custom`): go to [step 5](https://docs.gorunner.io/api/create-a-connection#sign-in).

```bash
curl -s "$GORUNNER/api/pieces/seatable" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" | jq .data.auth
```

```text
{
  "type": "custom",
  "required": true,
  "description": "Connect one SeaTable base. …",
  "fields": [
    { "name": "serverUrl", "title": "Server URL",
      "type": "string", "required": false, … },
    { "name": "apiToken", "title": "API Token",
      "type": "password", "required": true, … }
  ]
}
```

## 2. Create it

Send the app’s ID, a name, and a `config` whose keys are the field names.

A field the app marks `required` must have a value, or the answer is `400` naming it. The rest are stored as sent: a misspelled key of an optional field shows only when you test or use the connection, so test it next. To check the details first without saving anything, send the same body to [Test sign-in details before saving](https://docs.gorunner.io/api/connections/test-connection-draft).

You should see `201` and the connection, without its secrets.

```bash
curl -s -X POST "$GORUNNER/api/connections" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pieceId": "seatable",
       "name": "SeaTable · Orders",
       "config": {"apiToken": "'"$SEATABLE_TOKEN"'"}}'
```

```text
{
  "data": {
    "id": "0054f06e-…",
    "pieceId": "seatable",
    "name": "SeaTable · Orders",
    "authType": "api_key",
    "status": "active",
    "account": "cloud.seatable.io",
    …
  }
}
```

## 3. Test it

[Test a connection](https://docs.gorunner.io/api/connections/test-connection) asks the service. The answer is `200` either way: read `success`, and `error` when it is false.

You should see `"success": true`. Here it is false, because the SeaTable token was made up.

```bash
curl -s -X POST "$GORUNNER/api/connections/$CONNECTION_ID/test" \
  -H "Authorization: Bearer $GORUNNER_TOKEN"
```

```text
{
  "data": {
    "success": false,
    "status": "error",
    "cause": "refused",
    "error": "seatable: cloud.seatable.io rejected the API token (403) — …"
  }
}
```

## 4. Use it in a workflow

Put its ID in a step’s `connectionId`, beside `pieceId`. Never put secrets in `config`. The connection must be in the workflow’s workspace.

[Workflow definition](https://docs.gorunner.io/api/workflow-definition)

```json
{
  "name": "add_order",
  "type": "PIECE",
  "pieceId": "seatable",
  "actionId": "seatable",
  "connectionId": "0054f06e-…",
  "config": { … }
}
```

## 5. Start a sign-in

For an app that signs in, such as Gmail, start a sign-in. The connection is made only when the person finishes it.

*Self-hosted and Embed only:*

`returnUrl` is where the browser comes back. It must be https, or http on localhost. Its origin must be the installation’s own dashboard, one of its allowed websites, added in **Platform › Partner keys**, or one of the embed account’s own.

*Cloud only:*

`returnUrl` is where the browser comes back. On Cloud it must be on GoRunner’s own dashboard, `https://app.gorunner.io`. Cloud keeps no allowed websites you can add.

The answer has `authorizeUrl`: the service’s own sign-in page. It works once, for 10 minutes.

```bash
curl -s -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.acme.example/done"}'
```

```text
{
  "data": {
    "attemptId": "lWbsqG2aUqJ8vtM5oPw09w",
    "authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth?…",
    "expiresAt": "2026-10-25T18:33:22Z"
  }
}
```

## 6. Send the person

Open `authorizeUrl` in the browser of the person who should sign in. Give it to no one else.

When they finish, the browser comes back to `returnUrl` with:

- **It worked**: `status=connected`, `connectionId` and `attemptId`.
- **It didn’t**: `status=error`, `attemptId` and a `reason`: `denied`, `service_error`, `failed`, `cancelled` or `expired`.

No token or secret is ever in that address. Your own query parameters are kept.

## 7. Read the outcome

Your server can also ask: `pending`, `connected` with `connectionId`, or `failed` with `reason`. Only the token that started it can read it, for an hour after the link runs out.

To stop it, call [Stop a sign-in](https://docs.gorunner.io/api/connections/cancel-connection-oauth2-attempt).

```bash
curl -s "$GORUNNER/api/connections/oauth2/attempts/$ATTEMPT_ID" \
  -H "Authorization: Bearer $GORUNNER_TOKEN"
```

```text
{
  "data": {
    "attemptId": "lWbsqG2aUqJ8vtM5oPw09w",
    "status": "pending",
    "expiresAt": "2026-10-25T18:33:22Z"
  }
}
```

**Can I bring tokens my own sign-in got?**

Yes. In `config`, send `accessToken`, `refreshToken`, `expiresAt` (RFC 3339) and `tokenType`, with the app that issued them: your `clientId` and `clientSecret`, or `"oauthApp": "installation"`. GoRunner refreshes them like a sign-in’s. A refresh token with no app to refresh it is refused.

**What if a connection stops working?**

Change its secrets with [Rename or update a connection](https://docs.gorunner.io/api/connections/update-connection). Sign an app that signs in again with [Start a sign-in for a connection](https://docs.gorunner.io/api/connections/start-connection-oauth2), which needs the person in a browser.

**Can an AI app create a connection?**

No. A person makes it, in GoRunner or by this API. An app can build a workflow that needs one: it saves as a draft, and publishing names the step that needs a connection.

## Related

- [Create a connection](https://docs.gorunner.io/api/connections/create-connection): Every field and response
- [Connections](https://docs.gorunner.io/build/connections): What a connection is, and its states · 4 min
- [Your customers’ connections](https://docs.gorunner.io/embed/customer-connections): The same sign-in from the Partner API

Source: https://docs.gorunner.io/api/create-a-connection
