# Rename or update a connection

**PUT** `/api/connections/{id}`

Renames a connection, or changes its details.

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

## Parameters

### Path

- `id` · Required · UUID: The connection’s ID.

### Body

- `name` · Optional · string: Up to 255 characters, trimmed, not blank. Needed when there is no `config`.
- `config` · Optional · object: Left out: only a rename, and the details stay. Sent: merged over the saved ones. A field left out or blank keeps its value; `null` removes it.
- `test` · Optional · boolean: `true` tests the merged details first and saves only if the service accepts them.

**What are the rarely used parameters?**

- `authType` · Optional · string: Up to 50 characters. Empty or left out keeps it.
- `oauthApp` · Optional · string: Only with sign-in tokens in `config` that came from the installation’s app: `installation`.

## Secrets follow their address

A blank secret keeps its saved value only while the connection still points at the same place. Change a host, port, database, address or connection string, and send every saved secret again; otherwise you get `422 SECRETS_REQUIRED`.

Any change to `config` clears the last test and error, and sets `status` to `active` (`pending` for a sign-in with no token). `pieceId` can’t change: make a new connection.

Sign-in tokens from elsewhere can be saved here as in [Create a connection](https://docs.gorunner.io/api/connections/create-connection).

## Responses

- 200 OK

  Updated. The answer holds no secret.

- 400 `VALIDATION_ERROR`

  `id` isn’t a UUID, the body isn’t JSON, the name is missing or blank, `authType` is over 50 characters, or the change leaves a required field empty (`details.fields`).

- 403 `FORBIDDEN`

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

- 403 `MANAGED_CONNECTION`

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

- 404 `NOT_FOUND`

  No connection with this ID in the token’s workspace.

- 422 `SECRETS_REQUIRED`

  The change moves where the connection points (a host, server, port, database, address or connection string) and left a saved secret blank. `details.fields` names the secrets to send again. Nothing changed.

- 422 `CONNECTION_TEST_FAILED`

  With `test: true`: the service refused the details (`details.cause`). Nothing was saved.

- 500 `INTERNAL_ERROR`

  Nothing changed. Try again.

- 503 `NO_VERDICT`

  With `test: true`: no answer came back (`details.reason`: `no_worker`, `timeout` or `not_served`). Nothing was saved.

- 503 `UNAVAILABLE`

  The installation’s OAuth app for the service (`oauthApp: "installation"`) couldn’t be read. Nothing was saved. 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) · [`403 ACCOUNT_SUSPENDED`](https://docs.gorunner.io/errors#ACCOUNT_SUSPENDED) · [`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
CONNECTION_ID="your-connection-id"

curl -X PUT "$GORUNNER/api/connections/$CONNECTION_ID" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "config": {
      "host": "db2.bakery.example",
      "password": "…"
    },
    "test": true
  }'
```

**JavaScript**

```js
const GORUNNER = "https://api.gorunner.io";
const TOKEN = process.env.GORUNNER_TOKEN;
const CONNECTION_ID = process.env.CONNECTION_ID;

const res = await fetch(
  `${GORUNNER}/api/connections/${CONNECTION_ID}`,
  {
    method: "PUT",
    headers: {
        Authorization: `Bearer ${TOKEN}`,
        "Content-Type": "application/json",
      },
    body: JSON.stringify({
        config: {
          host: "db2.bakery.example",
          password: "…",
        },
        test: true,
      }),
  },
);
const { data } = await res.json();
```

**Python**

```python
import os, requests

GORUNNER = "https://api.gorunner.io"
TOKEN = os.environ["GORUNNER_TOKEN"]
CONNECTION_ID = os.environ["CONNECTION_ID"]

res = requests.put(
    f"{GORUNNER}/api/connections/{CONNECTION_ID}",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={
      "config": {
        "host": "db2.bakery.example",
        "password": "…",
      },
      "test": True,
    },
)
data = res.json()["data"]
```

Response · 200 OK, shortened

```json
{
  "data": {
    "id": "0f8b2c7e-4a51-4d3e-9b2a-6c1d2e3f4a5b",
    "pieceId": "postgres",
    "name": "Warehouse database",
    "authType": "custom",
    "status": "active",
    "lastTestedAt": "2026-10-25T09:00:00Z",
    "account": "warehouse@db2.bakery.example:5432/orders",
    "workspaceId": "c41a7e02-9b3d-4f6a-8e15-7d2c0b9a4f68",
    "orgId": "e9d2b6f1-3a7c-4d05-b8e4-1c6f2a9d7b30",
    "createdBy": "5d8e1f3a-0b6c-4e2d-9f7a-1c3b5e8d2a40",
    "createdAt": "2026-10-20T08:00:00Z",
    "updatedAt": "2026-10-24T16:00:00Z"
  }
}
```

**Every field**

### Body

- `name` · Optional · string: New label; trimmed; blank is a 400; at most 255 characters. Optional when `config` is sent (absent keeps the stored name); required otherwise.
- `authType` · Optional · string: Optional, at most 50 characters. An empty string or omission leaves the stored value unchanged.
- `config` · Optional · object: OPTIONAL, and its ABSENCE (`null` reads as absent) is the signal for a rename-only update (the verdict is kept). Sent, it is MERGED over the stored config: a key left out, or a string left blank, keeps the saved value; `null` removes a key; anything else replaces it. A sign-in’s tokens are kept unless named. Any config change resets the verdict: `status` `active` (or `pending` for a sign-in with no token), `lastTestedAt` and `lastError` cleared, `account` recomputed. A change that moves a where field (host, server, port, database, a URL, a connection string) must send every stored secret again, else 422 `SECRETS_REQUIRED` (`details.fields` names their keys, `details.changed` the where keys).
- `test` · Optional · boolean: Test the merged config first, as on create: refused → 422 `CONNECTION_TEST_FAILED`, no verdict → 503 `NO_VERDICT`, nothing changed.
- `oauthApp` · Optional · string: Only with OAuth2 tokens in `config` (an `accessToken`): they came from the installation’s app for this service, which refreshes them. Refused without tokens, with `clientId` in `config`, or when the installation has no app for the service. One of `installation`.

### Response · 200 OK

- `data` · object: A stored credential set for one app. The encrypted credential itself is never present in this object — the model tags it `json:"-"` — so there is no field here, and no endpoint anywhere, that returns a secret you wrote.
  - `id` · UUID: The connection’s id. This is the value a workflow step or trigger carries as `connectionId` to use these credentials.
  - `pieceId` · string: The app these credentials belong to (e.g. `seatable`, `postgres`, `googlesheets`, or the generic `oauth2`). Fixed at creation and not changeable.
  - `name` · string: Human label. Not unique.
  - `authType` · string: Free-text label recording the auth shape. Defaults to `api_key` when not supplied at creation. Conventionally one of the app’s `auth.type` values, but NOT validated against the app — treat it as advisory.
  - `status` · string: The last known verdict on these credentials. `active` = created normally, or the last test passed. `pending` = an OAuth2 connection that has not completed the authorization flow (no access token yet). `unverified` = the last test proved nothing because the app has no connection test; credentials are checked on first real use. `error` = the last test was refused by the provider. `expired` = a refresh was refused for good — the provider rejected the stored grant or the client (revoked, aged out), or the OAuth app that issued the tokens was replaced or removed; signing in again (Re-authorize) mends it. The test endpoint (and `test: true` on create or update), the OAuth2 sign-in and a refused refresh set it; an update that sends `config` resets it to `active` (`pending` for a sign-in with no token); a rename leaves it. One of `active`, `pending`, `unverified`, `error`, `expired`.
  - `lastTestedAt` · time: When the connection was last tested. Absent if never tested.
  - `tokenExpiresAt` · time: OAuth2 access-token expiry, when known. Refresh happens automatically at run time; there is no manual refresh call.
  - `refreshable` · boolean: The stored config carries a refresh token, so `tokenExpiresAt` is when the access token is next RENEWED (on use), not when the connection ends. False for pasted access tokens and API keys. A connection whose refresh the provider refuses for good is marked `status: expired` on the next refresh attempt and needs signing in again.
  - `managedBy` · string: Present only on a connection the installation’s partner manages through the embed API. It is read-only here: update, delete and the OAuth2 sign-in answer 403 `MANAGED_CONNECTION`; test and replace stay open. One of `partner`.
  - `workspaceId` · UUID: Owning workspace. Populated on the create’s answer; the all-zero UUID on the list, the single read and an update’s answer, which read the row the way the list does and do not select it.
  - `orgId` · UUID: Owning organization. Same caveat as `workspaceId`: all-zero except in the create’s answer.
  - `createdBy` · UUID: The user who created it. Absent when no person did (the partner’s, a machine’s, rows from before creators were recorded).
  - `createdAt` · time
  - `updatedAt` · time
  - `account` · string: Who or what the credential is, never a secret: the Google address a test names, `user@host` (with port and database) from a typed credential’s non-secret fields, a SeaTable base’s name and server. Shown to every reader. Absent until known.
  - `createdByName` · string: The creator’s name (else their address), only while they are a member of the organization.
  - `createdByMember` · boolean: False once the creator is no longer a member of the organization. Absent with `createdBy`.
  - `lastError` · object: The last failure that still stands. Written by a failed test, a refused refresh (`expired`), and a run’s or poll’s definite refusal; a run’s 403 is kept as news (`cause: forbidden`) without changing `status`. Cleared by a working test, a sign-in, a config change, and the next run or poll that uses the connection and works.
    - `at` · time
    - `source` · string: One of `test`, `run`, `trigger`, `refresh`, `signin`.
    - `cause` · string: `refused` — the service refused the credential (401, SMTP 535, Postgres 28P01, MySQL 1045, …); `expired` — a refresh was refused for good; `server` — the server could not be reached; `forbidden` — a 403, news only; `other`. One of `refused`, `expired`, `server`, `forbidden`, `other`.
    - `message` · string: The service’s own words, masked of the step’s secrets, at most 1,000 characters.
    - `runId` · UUID
    - `workflowId` · UUID
    - `workflowName` · string: While the workflow exists.
  - `lastUse` · object: The newest use the audit trail records (its `connection.decrypt` rows). Absent when the trail has none.
    - `at` · time
    - `kind` · string: One of `run`, `trigger`, `test`, `editor`, `signin`.
    - `runId` · UUID
    - `workflowId` · UUID
    - `workflowName` · string
    - `byName` · string: The person, for a test, the builder or a sign-in, while a member of the organization.
  - `oauthClient` · string: For a connection that signs in: `managed` — the installation’s app; `byo` — its own client id; `pasted` — an access token with no refresh token, which cannot renew itself. One of `managed`, `byo`, `pasted`.
  - `lastSignInAt` · time: When a sign-in last stored tokens.
  - `expiresAt` · time: When the connection stops working with no way to renew: `tokenExpiresAt` of a connection that is not `refreshable`. Absent for a refreshable one and when unknown.
  - `test` · object: Only on the answer of a create or a change sent with `test: true`: the test that let it be saved.
    - `success` · boolean: Always `true` here: a refused test saves nothing and is a 422.
    - `message` · string: The app’s own words.
    - `unverified` · boolean: Present and true when the app cannot be tested without running a real step: saved, and nothing was proven.

Related: [Get a connection’s settings](https://docs.gorunner.io/api/connections/get-connection-settings) · [Test sign-in details before saving](https://docs.gorunner.io/api/connections/test-connection-draft)

Source: https://docs.gorunner.io/api/connections/update-connection
