GoRunner docs
Search or askSearch⌘K

Create a connection

POST/api/connections

Saves the sign-in details for one app, so workflow steps can use them.

Needs an API token with the Write permission for connections (connection:write).

Parameters

Headers

Idempotency-KeyOptional · string

Up to 255 characters. The same request with the same key within 24 hours answers the first result again, instead of saving twice.

Body

pieceIdRequired · string

The app, such as postgres. It must be an app that uses a connection.

nameRequired · string

Up to 255 characters, trimmed. Not blank.

configOptional · object

The details, by the field names the app lists in its auth (Get an app). A field marked required must have a value. Anything else is stored as sent, so a mistake there shows when it is tested or used.

testOptional · boolean

true tests the details first and saves only if the service accepts them.

What are the rarely used parameters?
authTypeOptional · string

A label for the kind of sign-in, up to 50 characters. Left out: api_key. It changes nothing.

oauthAppOptional · string

Only with sign-in tokens in config that came from the installation’s app: installation.

Use it in a workflow

A step or trigger names it by ID: "connectionId": "…" next to its pieceId. Never put the secret in a step’s config. A connection serves one app only; a trigger app’s connection is made under its action app (gmailtrigger under gmail).

The details are encrypted when saved and can’t be read back. Keep them on your side if you need them again. See Create a connection from code.

Apps you sign in to

For an app that signs in, such as Gmail, Start a sign-in for a new connection instead: the connection is made only when the sign-in works.

Tokens you already got from your own sign-in can be saved here: accessToken, refreshToken, expiresAt (RFC 3339) and tokenType, with the app that issued them, your clientId and clientSecret or oauthApp: "installation". They renew like a sign-in’s. A refresh token with no app to renew it is refused.

Saved with only a client ID and secret, a sign-in connection is pending until someone signs in.

Responses

  • 201Created

    Saved. The answer holds no secret.

  • pieceId or name is missing, the app isn’t one this installation runs or doesn’t use a connection, config isn’t valid or lacks a required field (details.fields), authType is over 50 characters, or Idempotency-Key is over 255 characters.

  • The token lacks the Write permission for connections.

  • A request with this Idempotency-Key is still in progress, or its answer was too large to keep.

  • This Idempotency-Key was already used with a different request.

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

  • The connection wasn’t saved. Try again.

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

  • 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 · 401 TOKEN_ENDED · 403 TOKEN_SURFACE_OFF · 403 FEATURE_OFF · 403 EMBED_NOT_ALLOWED · 403 EMBED_NOT_AVAILABLE · 403 ACCOUNT_SUSPENDED · 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" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "pieceId": "postgres",
    "name": "Warehouse database",
    "config": {
      "host": "db.bakery.example",
      "port": "5432",
      "database": "orders",
      "user": "warehouse",
      "password": "…"
    },
    "test": true
  }'

Response · 201 Created, shortened

json
{
  "data": {
    "id": "0f8b2c7e-4a51-4d3e-9b2a-6c1d2e3f4a5b",
    "pieceId": "postgres",
    "name": "Warehouse database",
    "authType": "custom",
    "status": "active",
    "lastTestedAt": "2026-10-24T16:00:00Z",
    "account": "warehouse@db.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

pieceIdRequired · string

The app this credential is for. Must be an app the live worker fleet runs, and that app must declare auth.required: true — see GET /api/pieces/{id}.

nameRequired · string

Human label. Trimmed; required and must not be blank; at most 255 characters (counted in characters, not bytes).

authTypeOptional · string

Optional free-text label for the auth shape, at most 50 characters. Defaults to api_key. Not validated against the app and does not change how the credential is used — the app decides that.

configOptional · object

The credential itself. Keys are the names of the app’s auth.fields[] (from GET /api/pieces/{pieceId}); a field of type: "password" holds a secret. For an OAuth2 app this normally holds only clientId and clientSecret — the authorize/token URLs and scopes come from the app. For the generic oauth2 app you also supply authUrl, tokenUrl and scope yourself. Encrypted at rest and NEVER returned by any endpoint. Fields the app marks required must have a value (400 naming them). The rest are stored as sent, and keys the app does not declare are stored and ignored, so a misspelled optional key surfaces only at test or run time.

testOptional · boolean

Test the config first and save only if the service accepts it: refused → 422 CONNECTION_TEST_FAILED (details.cause), no verdict → 503 NO_VERDICT (details.reason), nothing saved in either case. Accepted → 201 with the row tested now and test.

oauthAppOptional · 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 · 201 Created

dataobject

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.

idUUID

The connection’s id. This is the value a workflow step or trigger carries as connectionId to use these credentials.

pieceIdstring

The app these credentials belong to (e.g. seatable, postgres, googlesheets, or the generic oauth2). Fixed at creation and not changeable.

namestring

Human label. Not unique.

authTypestring

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.

statusstring

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.

lastTestedAttime

When the connection was last tested. Absent if never tested.

tokenExpiresAttime

OAuth2 access-token expiry, when known. Refresh happens automatically at run time; there is no manual refresh call.

refreshableboolean

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.

managedBystring

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.

workspaceIdUUID

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.

orgIdUUID

Owning organization. Same caveat as workspaceId: all-zero except in the create’s answer.

createdByUUID

The user who created it. Absent when no person did (the partner’s, a machine’s, rows from before creators were recorded).

createdAttime
updatedAttime
accountstring

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.

createdByNamestring

The creator’s name (else their address), only while they are a member of the organization.

createdByMemberboolean

False once the creator is no longer a member of the organization. Absent with createdBy.

lastErrorobject

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.

attime
sourcestring

One of test, run, trigger, refresh, signin.

causestring

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.

messagestring

The service’s own words, masked of the step’s secrets, at most 1,000 characters.

runIdUUID
workflowIdUUID
workflowNamestring

While the workflow exists.

lastUseobject

The newest use the audit trail records (its connection.decrypt rows). Absent when the trail has none.

attime
kindstring

One of run, trigger, test, editor, signin.

runIdUUID
workflowIdUUID
workflowNamestring
byNamestring

The person, for a test, the builder or a sign-in, while a member of the organization.

oauthClientstring

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.

lastSignInAttime

When a sign-in last stored tokens.

expiresAttime

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.

testobject

Only on the answer of a create or a change sent with test: true: the test that let it be saved.

successboolean

Always true here: a refused test saves nothing and is a 422.

messagestring

The app’s own words.

unverifiedboolean

Present and true when the app cannot be tested without running a real step: saved, and nothing was proven.