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 asappid-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
- 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). - 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. - Follow the API below to create connections for your users and complete OAuth.
- 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_xxxxxxxxxxxxxxxxxxxxxxxxContent-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
| Concept | Description |
|---|---|
app_id | App identifier, e.g. app_1a2b3c4d5e6f7788, generated when the app is created |
app_key | App secret, sk_ prefix, shown only once on creation; multiple keys supported, with expiry and revocation |
open_id | End-user identifier, unique within an app, u_ prefix, generated by the platform |
integration | A provider credential set configured on the app (custom name, e.g. github-main), referenced when creating connections |
connection_id | Connection identifier; you may supply your own (e.g. reuse your user ID), otherwise one is generated |
connection_token | Per-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/userswith anexternal_idis idempotent — if the user already exists it is returned as-is. You can use your own user primary key asexternal_idand 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" }}
| Field | Type | Required | Description |
|---|---|---|---|
external_id | string ≤191 | no | Your own user identifier; repeated calls are idempotent |
display_name | string ≤191 | no | Display name |
metadata | object | no | Arbitrary 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} # paginationGET /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" }}
| Field | Type | Required | Description |
|---|---|---|---|
open_id | string | yes | End-user open_id |
integration | string | yes | Integration name or integration ID |
connection_id | string | no | Custom connection ID ([A-Za-z0-9_-], ≤96); generated when omitted |
config / metadata | object | no | Passed through to the kernel connection object |
credentials | object | no | Credential 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_9f8e7d6c5b4a39281706X-Connection-Token: ct_6f1d…
X-Open-Idis 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-Tokenis 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_urlis a QR-code image. Show it to the end user, then callcompletewithstateandwait_seconds— the kernel polls the scan result for up to that many seconds (max 300).quark-drive:authorization_urlis the authorization page. Either submit thecodethe user copies from the redirect URL, or omitcodeand let the kernel pollpage_codefor up towait_seconds.- One-phase import (adapters that accept pasted login credentials, e.g. Baidu
bduss/stoken): omitstateand send{ "credentials": { ... } }. - While the user has not scanned / authorized yet,
completereturns202 { "status": "pending" }and thestatestays valid — callcompleteagain with the samestate. A Baidu poll round can take ~30 s even with a smallwait_seconds, so use generous client timeouts. stateis valid for 15 minutes and is consumed on success or on any other failure (4xx); callauthorizeagain for a freshstate. 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/DELETEsupported; request and response bodies are streamed through (including large payloads).endpointis 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/5xxresponses are returned as-is. Rejections by the platform itself carry anX-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
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/connections/{id}/query | Native query {action, params} (e.g. moji-weather) |
| POST | /api/v1/connections/{id}/sessions | Native per-target session {kind, params} → {session_id} (e.g. Baidu share share_verify) |
| POST | /api/v1/connections/{id}/upload | Native 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=50GET /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.
| Method | Path | Description |
|---|---|---|
| GET | /api/auth/oauth/{provider} | Start OAuth sign-in (redirects to the provider) |
| GET | /api/auth/oauth/{provider}/callback | OAuth callback (provider redirects here) |
| POST | /api/auth/logout | Sign out |
| GET | /api/auth/me | Current account |
| GET / POST | /api/apps | List 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}/keys | Create 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}/integrations | List / create {name, provider, client_id, client_secret, scopes, config} |
| PATCH / DELETE | /api/apps/{app_id}/integrations/{id} | Update credentials / delete |
client_secretis 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).
| HTTP | code | When |
|---|---|---|
| 400 | invalid_request | validation failed (message includes field details) |
| 401 | unauthorized | App Key missing / invalid / expired / revoked |
| 403 | forbidden | accessing another party's resource |
| 400 | invalid_request | X-Open-Id missing on a connection call |
| 401 | connection_token_required | X-Connection-Token missing on a connection call |
| 403 | open_id_mismatch | the connection does not belong to X-Open-Id (your app paired a user with someone else's connection) |
| 403 | invalid_connection_token | X-Connection-Token does not match the connection |
| 403 | legacy_connection | the connection predates connection tokens; delete and recreate it |
| 404 | not_found | resource does not exist or does not belong to the app (indistinguishable, prevents probing) |
| 409 | already_revoked | conflicts |
| 4xx/5xx | upstream_error | gonnector kernel error passed through with its status code (e.g. 503 throttling, 409 tenant conflict) |
9. Security best practices
- Keep the App Key server-side only. Never ship it to browsers, mobile apps or mini-programs.
- One key per environment: separate keys for production / staging, set
expires_in_daysfor periodic rotation, and revoke immediately in the Dashboard on leakage. - 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.
- 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-Idfrom the requesting user's identity so the platform can catch ownership mix-ups in your code. - Do not forward user-supplied full URLs to the proxy endpoint — pass only the path; domain boundaries (SSRF protection) are enforced by the kernel.
- 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_keyKEY="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 onceTOKEN=$(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 redirectcurl -s -X POST "$BASE/api/v1/connections/user-42-gh/authorize" \-H "Authorization: Bearer $KEY" "${CONN[@]}"# 4) After the user completes OAuth, poll the statuscurl -s "$BASE/api/v1/connections/user-42-gh" \-H "Authorization: Bearer $KEY" "${CONN[@]}"# 5) Call the upstream through the proxycurl -s "$BASE/api/v1/proxy/user-42-gh/user/repos?per_page=5" \-H "Authorization: Bearer $KEY" "${CONN[@]}"