Gonnector Open Platform · App Integration Guide

The open platform lets you (a third-party app developer) offer SaaS connectivity to your end users: OAuth authorization, managed credentials with auto-refresh, and proxied upstream API calls.

Architecture at a glance

Your end users ──browser──▶ your backend ──AppKey──▶ open platform (platform) │ master token + tenant mapping ▼ gonnector kernel (one isolated tenant per user)
  • Your backend calls the platform API with an App Key; end users never talk to the platform directly.
  • Identity hierarchy: developer account → app (app_id) → end user (open_id).
  • Each end user maps to a physically isolated tenant in the gonnector kernel — logically tenant = appid:openid (the kernel forbids : in tenant IDs, so it is stored physically as appid-openid). Data is isolated at the row level and encrypted with AES-256-GCM bound to the tenant: invisible and undecryptable across tenants.

1. Quick start

  1. Sign in in the Dashboard with Google or GitHub (the account is created automatically on first sign-in), create an app, and save the App Key (sk_ prefix, shown only once).
  2. Add an integration on the app detail page: pick a provider (e.g. github) and fill in the OAuth Client ID / Secret you registered upstream.
  3. Follow the API below to create connections for your users and complete OAuth.
  4. Call upstream APIs through the proxy endpoint — credentials are injected and refreshed automatically.

Every API request (except Dashboard login) authenticates with the App Key:

Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Calls on an existing connection (status, authorize / complete, refresh, delete, proxy, native endpoints, records) additionally need the connection credentials described in 4.1a, so the App Key alone cannot use any connection.

The base URL in examples below is https://platform.example.com.


2. Core concepts

ConceptDescription
app_idApp identifier, e.g. app_1a2b3c4d5e6f7788, generated when the app is created
app_keyApp secret, sk_ prefix, shown only once on creation; multiple keys supported, with expiry and revocation
open_idEnd-user identifier, unique within an app, u_ prefix, generated by the platform
integrationA provider credential set configured on the app (custom name, e.g. github-main), referenced when creating connections
connection_idConnection identifier; you may supply your own (e.g. reuse your user ID), otherwise one is generated
connection_tokenPer-connection secret, ct_ prefix, returned once when the connection is created; required (with the App Key) on every call on that connection

Idempotency: POST /api/v1/users with an external_id is idempotent — if the user already exists it is returned as-is. You can use your own user primary key as external_id and keep no mapping table of your own.


3. End-user API

3.1 Create an end user

POST /api/v1/users
{
"external_id": "user-42",
"display_name": "Alice",
"metadata": { "plan": "pro" }
}
FieldTypeRequiredDescription
external_idstring ≤191noYour own user identifier; repeated calls are idempotent
display_namestring ≤191noDisplay name
metadataobjectnoArbitrary JSON, stored and echoed back

Response 201

{
"user": {
"open_id": "u_9f8e7d6c5b4a39281706",
"tenant": "app_1a2b3c4d5e6f7788-u_9f8e7d6c5b4a39281706",
"external_id": "user-42",
"display_name": "Alice",
"metadata": { "plan": "pro" },
"created_at": "2026-10-29T08:00:00.000Z"
}
}

3.2 List end users

GET /api/v1/users?limit=50&after={cursor} # pagination
GET /api/v1/users?external_id=user-42 # exact lookup by external ID

Response 200

{
"users": [ { "open_id": "u_...", "external_id": "user-42", "...": "..." } ],
"next_cursor": "clx..." // null when there are no more pages
}

3.3 Update / delete an end user

PATCH /api/v1/users/{open_id} # body: { "display_name": "...", "metadata": {...} }
DELETE /api/v1/users/{open_id}

Deleting an end user cascades platform-side records and ownership entries, but does not delete the tenant's data inside the gonnector kernel. For a full cleanup, delete the connections first (see 4.4).


4. Connection API

4.1 Create a connection

POST /api/v1/connections
{
"open_id": "u_9f8e7d6c5b4a39281706",
"integration": "github-main",
"connection_id": "conn-user-42-github",
"metadata": { "purpose": "import-repos" }
}
FieldTypeRequiredDescription
open_idstringyesEnd-user open_id
integrationstringyesIntegration name or integration ID
connection_idstringnoCustom connection ID ([A-Za-z0-9_-], ≤96); generated when omitted
config / metadataobjectnoPassed through to the kernel connection object
credentialsobjectnoCredential fields for API Key / Basic style providers (keys from the provider's credentials definition, string values). Passed to the kernel and verified there; the platform never stores them

The platform automatically pushes the integration credentials into the user's isolated tenant (idempotent), then creates the connection.

Response 201

{
"connection": {
"id": "conn-user-42-github",
"open_id": "u_9f8e7d6c5b4a39281706",
"provider": "github",
"status": "pending"
},
"connection_token": "ct_6f1d…"
}

connection_token is shown only in this response (the platform stores only its SHA-256). Save it with your record of the connection right away — without it the connection can only be deleted and recreated.

4.1a Connection credentials 🔒

Every call on an existing connection must carry, next to the App Key:

X-Open-Id: u_9f8e7d6c5b4a39281706
X-Connection-Token: ct_6f1d…
  • X-Open-Id is the end user on whose behalf you are calling. Fill it from the identity of the user making the request in your app, not from your stored connection record: if your code ever pairs one user's request with another user's connection, the platform rejects it (403 open_id_mismatch) instead of serving the other user's account.
  • X-Connection-Token is the token from 4.1. App Key and connection token are both required, so leaking only one of them (e.g. only your environment variables or only your database) does not expose any connection.
  • Connections created before connection tokens existed are rejected with 403 legacy_connection; they can still be deleted (4.4) and must be recreated.

4.2 Start OAuth authorization ⭐

POST /api/v1/connections/{connection_id}/authorize

Response 200

{ "authorization_url": "https://github.com/login/oauth/authorize?...", "open_id": "u_..." }

What you need to do: redirect the end user's browser to authorization_url. The OAuth callback is handled by the gonnector kernel's public URL (sessions are valid for 10 minutes); once the flow completes the connection becomes usable. Poll GET /api/v1/connections/{id} to detect completion (status becomes an available state).

Full sequence:

End-user browser your backend platform / kernel │ 1. "Connect GitHub" click │ │ │──────────────────────▶│ │ │ │ 2. POST /users │ (idempotent; skipped if present) │ │ 3. POST /connections │ │ 4. POST /authorize │ │ │◀── authorization_url │ 5. 302 to authorization_url │ │◀──────────────────────│ │ │ 6. user signs in on GitHub and consents │ │───────────────────────────────────────────▶│ │ 7. GitHub calls back the kernel public URL │ │◀───────────────────────────────────────────│ │ 8. your frontend polls connection status → available

4.2a QR-code / device-style authorization ⭐

Providers with a native adapter (definition.native_adapter in GET /api/v1/providers/{id}, e.g. quark-drive, baidu-netdisk) do not use a browser redirect back to the kernel. authorize still starts the flow, then the app finishes it with complete:

POST /api/v1/connections/{connection_id}/authorize
→ { "state": "<64 chars>", "authorization_url": "...", "expires_at": "...", "page_code": "..." }
POST /api/v1/connections/{connection_id}/complete
{ "state": "<from authorize>", "code": "optional", "wait_seconds": 120 }
→ { "connection": { "id": "...", "status": "active", ... } }
  • baidu-netdisk: authorization_url is a QR-code image. Show it to the end user, then call complete with state and wait_seconds — the kernel polls the scan result for up to that many seconds (max 300).
  • quark-drive: authorization_url is the authorization page. Either submit the code the user copies from the redirect URL, or omit code and let the kernel poll page_code for up to wait_seconds.
  • One-phase import (adapters that accept pasted login credentials, e.g. Baidu bduss / stoken): omit state and send { "credentials": { ... } }.
  • While the user has not scanned / authorized yet, complete returns 202 { "status": "pending" } and the state stays valid — call complete again with the same state. A Baidu poll round can take ~30 s even with a small wait_seconds, so use generous client timeouts.
  • state is valid for 15 minutes and is consumed on success or on any other failure (4xx); call authorize again for a fresh state. Codes are never replayed.

4.3 Get connection status

GET /api/v1/connections/{connection_id}

Returns live status from the kernel (credentials always redacted): status, expires_at, multi-credential slot states, verification, and so on. Common status values: pending / authorization_required (awaiting authorization), available states, deleting, etc.

GET /api/v1/connections?open_id={open_id} # list all connections of a user

4.4 Delete a connection

DELETE /api/v1/connections/{connection_id}

Runs the kernel deletion lifecycle (including upstream cleanup hooks) and removes the platform ownership record on success. Needs the connection credentials (4.1a); legacy connections without a token can be deleted with the App Key and X-Open-Id alone so their stored credentials can be cleaned up.

4.5 Refresh credentials manually

POST /api/v1/connections/{connection_id}/refresh

Credentials refresh automatically on a schedule; this endpoint forces an immediate refresh and returns an async job_id for polling.


5. Upstream API proxy ⭐

Call upstream SaaS APIs as the end user — credential injection, refresh on expiry and domain-boundary protections are all handled by the kernel. You only supply the path:

{METHOD} /api/v1/proxy/{connection_id}[/{endpoint...}]?{upstream-query}

Example: fetch the current user with a GitHub connection

curl -H "Authorization: Bearer sk_..." \
-H "X-Open-Id: u_..." -H "X-Connection-Token: ct_..." \
"https://platform.example.com/api/v1/proxy/conn-user-42-github/user"

Example: POST request upstream

curl -X POST -H "Authorization: Bearer sk_..." \
-H "X-Open-Id: u_..." -H "X-Connection-Token: ct_..." \
-H "Content-Type: application/json" \
-d '{"query":"weather city=beijing"}' \
"https://platform.example.com/api/v1/proxy/{connection_id}/v3/weather"
  • GET/POST/PUT/PATCH/DELETE supported; request and response bodies are streamed through (including large payloads).
  • endpoint is the upstream path (the part after the host; omit it for the upstream root); query strings are forwarded verbatim.
  • Only connections owned by the current app and passing the connection credentials check (4.1a) are accessible.
  • Upstream 4xx/5xx responses are returned as-is. Rejections by the platform itself carry an X-Platform-Error: <code> response header (see 8), so you can tell them apart from upstream errors.

Request headers are forwarded like the kernel proxy does (e.g. Accept, X-GitHub-Api-Version, X-Native-Session to reuse a native session created below). Authorization, Cookie, X-Tenant-ID, X-Open-Id, X-Connection-Token and hop-by-hop headers are dropped — credentials are always injected by the kernel, and connection credentials never leave the platform.

Native endpoints

MethodPathDescription
POST/api/v1/connections/{id}/queryNative query {action, params} (e.g. moji-weather)
POST/api/v1/connections/{id}/sessionsNative per-target session {kind, params} → {session_id} (e.g. Baidu share share_verify)
POST/api/v1/connections/{id}/uploadNative upload: multipart (file, dir_path, parent_fid) or raw body + X-File-Name / X-Dir-Path / X-Parent-Fid; streamed to the kernel

Reading synced records

For connections with syncs configured, read records directly (paginated, with incremental cursors):

GET /api/v1/records/{connection_id}/{model}?cursor=&limit=&updated_after=&include_deleted=

6. Provider catalog

GET /api/v1/providers?q=github&limit=50
GET /api/v1/providers/{provider_id}

Returns the list of available providers (id, name, auth mode, capabilities and blockers) — useful for rendering a "connectable services" catalog in the Dashboard or your own frontend. The single-provider response includes the raw definition: credentials / connection_config field descriptions for forms, and native_adapter for providers that use the QR-code / device flow (4.2a).

GET /api/v1/integrations

Lists the integrations configured on your app (id, name, provider, scopes) — the account types your end users can connect. Secrets are never returned.


7. Dashboard management API (cookie session)

These endpoints power the Dashboard and can also be scripted. Sign-in uses OAuth (Google / GitHub): open GET /api/auth/oauth/{google|github} in a browser, complete the provider consent screen, and you are redirected back with the gcp_session cookie set (JWT, valid for 7 days). Accounts are created automatically on first sign-in; an existing account with the same verified email is linked.

OAuth client credentials are configured via the GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET environment variables; register the callback URLs <platform-url>/api/auth/oauth/google/callback and <platform-url>/api/auth/oauth/github/callback in the respective developer consoles.

MethodPathDescription
GET/api/auth/oauth/{provider}Start OAuth sign-in (redirects to the provider)
GET/api/auth/oauth/{provider}/callbackOAuth callback (provider redirects here)
POST/api/auth/logoutSign out
GET/api/auth/meCurrent account
GET / POST/api/appsList apps / create an app (returns one-time app_key)
GET / PATCH / DELETE/api/apps/{app_id}App detail (keys, integrations, counts) / rename / delete
POST/api/apps/{app_id}/keysCreate a key {name, expires_in_days?} (returns one-time app_key)
DELETE/api/apps/{app_id}/keys/{key_id}Revoke a key
GET / POST/api/apps/{app_id}/integrationsList / create {name, provider, client_id, client_secret, scopes, config}
PATCH / DELETE/api/apps/{app_id}/integrations/{id}Update credentials / delete

client_secret is stored AES-256-GCM encrypted and never returned by any endpoint.


8. Error format

All errors use one shape:

{ "error": { "code": "not_found", "message": "connection not found" } }

Errors produced by the platform itself also carry the code in an X-Platform-Error response header (kernel and upstream errors do not).

HTTPcodeWhen
400invalid_requestvalidation failed (message includes field details)
401unauthorizedApp Key missing / invalid / expired / revoked
403forbiddenaccessing another party's resource
400invalid_requestX-Open-Id missing on a connection call
401connection_token_requiredX-Connection-Token missing on a connection call
403open_id_mismatchthe connection does not belong to X-Open-Id (your app paired a user with someone else's connection)
403invalid_connection_tokenX-Connection-Token does not match the connection
403legacy_connectionthe connection predates connection tokens; delete and recreate it
404not_foundresource does not exist or does not belong to the app (indistinguishable, prevents probing)
409already_revokedconflicts
4xx/5xxupstream_errorgonnector kernel error passed through with its status code (e.g. 503 throttling, 409 tenant conflict)

9. Security best practices

  1. Keep the App Key server-side only. Never ship it to browsers, mobile apps or mini-programs.
  2. One key per environment: separate keys for production / staging, set expires_in_days for periodic rotation, and revoke immediately in the Dashboard on leakage.
  3. open_id is the tenant boundary. Never map multiple real users to the same open_id, or their connections and data will share one tenant.
  4. Store connection tokens apart from the App Key (e.g. tokens in your database, the App Key in your secret store / environment) so one leak is not enough. Never send either to clients. Fill X-Open-Id from the requesting user's identity so the platform can catch ownership mix-ups in your code.
  5. Do not forward user-supplied full URLs to the proxy endpoint — pass only the path; domain boundaries (SSRF protection) are enforced by the kernel.
  6. App Keys are hash-stored with instant revocation; adding per-key rate limiting at your gateway is recommended (the platform does not yet ship per-key quotas).

10. End-to-end example (curl)

# 0) Create an app in the Dashboard and grab the app_key
KEY="sk_xxxxxxxxxxxxxxxxxxxxxxxx"
BASE="https://platform.example.com"
# 1) Ensure the end user exists (idempotent)
OPEN_ID=$(curl -s -X POST "$BASE/api/v1/users" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"external_id":"user-42"}' | jq -r .user.open_id)
# 2) Create a connection (reuse your own business key as the connection ID);
# save connection_token now — it is returned only once
TOKEN=$(curl -s -X POST "$BASE/api/v1/connections" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d "{\"open_id\":\"$OPEN_ID\",\"integration\":\"github-main\",\"connection_id\":\"user-42-gh\"}" \
| jq -r .connection_token)
CONN=(-H "X-Open-Id: $OPEN_ID" -H "X-Connection-Token: $TOKEN")
# 3) Start authorization; return authorization_url to the frontend for redirect
curl -s -X POST "$BASE/api/v1/connections/user-42-gh/authorize" \
-H "Authorization: Bearer $KEY" "${CONN[@]}"
# 4) After the user completes OAuth, poll the status
curl -s "$BASE/api/v1/connections/user-42-gh" \
-H "Authorization: Bearer $KEY" "${CONN[@]}"
# 5) Call the upstream through the proxy
curl -s "$BASE/api/v1/proxy/user-42-gh/user/repos?per_page=5" \
-H "Authorization: Bearer $KEY" "${CONN[@]}"