Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API examples

The endpoints a graph app uses, with real requests and responses. The full endpoint list is in the generated API reference.

Base URL: https://socialgraph.stackwell.tech. Bodies are JSON (Content-Type: application/json); times are UTC, written YYYY-MM-DD HH:MM:SS.

Register an app

POST /api/graph/apps (developer token; see Authentication)

curl https://socialgraph.stackwell.tech/api/graph/apps \
  -H "Authorization: Bearer $DEVELOPER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Acme Planner",
    "description": "Plans events with your friends.",
    "platform": "web",
    "redirectUri": "https://acme.example/social-graph/callback"
  }'
FieldRequiredRules
displayNameYesNot blank. Shown to people on the approval page.
descriptionYesMay be empty. Shown on the approval page.
platformYesweb or native
redirectUriYeshttps:// for web; a custom scheme with a host for native
clientTypeNoconfidential (default) or public
chirpClientIdNoThe ChirpAuth client your app’s people sign in with (cs_…). Name it if your app reads people’s signing keys: GET /api/graph-access/keys/{id} returns the key a person registered from this client, and answers 409 for an app without one.

Any other field is refused with 400.

Response, 200 OK:

{
  "id": "b96e0063-f7a0-42d1-b1bd-dd7f1308b09e",
  "clientId": "graphapp-ab6bd084821c471eaab9ef7f83b09bf9",
  "clientSecret": "graphsecret-31gbf7gqBhl25kvWApma3Rq61uj_0rjdX3Gu7bhk3GE",
  "displayName": "Acme Planner",
  "description": "Plans events with your friends.",
  "status": "draft",
  "platform": "web",
  "redirectUri": "https://acme.example/social-graph/callback",
  "owner": {
    "id": "ae0e7aa0-5622-4cbf-a180-6fbebbd150fa",
    "displayName": "user-ae0e7aa0",
    "avatarUrl": null
  },
  "isOwner": true,
  "myGrantId": "4bb50311-94ca-45d0-834f-1667e639b74d",
  "myAccessToken": "graph-access-LyWr1Y-LZaVBxKe31xE5NmNiCjDYfsizbbiMucm7cyA",
  "myGrantedAt": 1793911264,
  "verified": false,
  "clientType": "confidential"
}

clientSecret appears only here and when you rotate it; save it. A public app gets "clientSecret": null. myAccessToken is an access token for your own Social Graph account on this app, also shown only once. id is the app id the status and rotation calls take.

Activate or disable it

POST /api/graph/apps/{id}/status (developer token), body {"status": "active"}. The status is draft, active or disabled; see Graph apps. Returns the app in the shape above, with clientSecret and myAccessToken set to null.

Rotate the client secret

POST /api/graph/apps/{id}/rotate-secret (developer token, no body). Returns the app with the new clientSecret. The old secret stops working at once.

Both calls return 404 Graph app not found for an id that isn’t one of your apps, and change nothing.

Send the person to approve

Link the person to:

https://socialgraph.stackwell.tech/graph/authorize?client_id={clientId}&redirect_uri={redirectUri}&state={state}&code_challenge={challenge}

URL-encode each value. After they decide, their browser is sent to:

https://acme.example/social-graph/callback?code=graphcode-nl7_h3Yz90TH_jGjWAnhTz5DFuvra8oIjPG-GCrcjyE&state=xyz
https://acme.example/social-graph/callback?error=access_denied&state=xyz

See Approval for the rules.

Exchange the code

POST /api/graph/token (no Authorization header)

curl https://socialgraph.stackwell.tech/api/graph/token \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "graphapp-ab6bd084821c471eaab9ef7f83b09bf9",
    "authorizationCode": "graphcode-nl7_h3Yz90TH_jGjWAnhTz5DFuvra8oIjPG-GCrcjyE",
    "redirectUri": "https://acme.example/social-graph/callback",
    "clientSecret": "graphsecret-31gbf7gqBhl25kvWApma3Rq61uj_0rjdX3Gu7bhk3GE",
    "codeVerifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
  }'
FieldRules
clientIdRequired
authorizationCodeRequired. The code from the redirect.
redirectUriRequired. The same redirect URI as the approval.
codeVerifierRequired if the approval carried a code_challenge
clientSecretRequired for a confidential app, with or without a code_challenge

Response, 200 OK:

{
  "accessToken": "graph-access-LX5hpmIpEI2UphioXi7Luz1n8Y1nBCt5yj5uSJ5bPF0",
  "tokenType": "Bearer",
  "me": {
    "id": "psd_308eeb407af0419bbf26c3db19ef5e9c",
    "displayName": "user-23369e30",
    "avatarUrl": null
  }
}

me is the person as GET /api/graph-access/me returns them: me.id is your app’s psd_ id for them.

Errors you may see:

StatuserrorCause
400Redirect URI does not match the registered app
400Authorization code has expiredOlder than 5 minutes
400Code verifier is requiredThe approval carried a challenge
400Public clients must initiate authorization with a PKCE code_challengeA public app’s approval carried no challenge
403Code verifier is invalidThe verifier doesn’t match the challenge
403Client secret is invalidA confidential app sent no clientSecret or a wrong one
403app_not_yet_active, app_disabled, app_suspended, app_terminatedThe app is not active
404App not foundUnknown clientId
404Authorization code not foundWrong code, or a code issued to another app or redirect URI
409Authorization code has already been used

The person

GET /api/graph-access/me

curl https://socialgraph.stackwell.tech/api/graph-access/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "id": "psd_308eeb407af0419bbf26c3db19ef5e9c",
  "displayName": "user-23369e30",
  "avatarUrl": null
}

Their connections

GET /api/graph-access/connections

[
  {
    "userId": "psd_4adcf14e44b5495b9e6b7e971d1e3885",
    "displayName": "user-8572fe12",
    "avatarUrl": null
  },
  {
    "userId": "psd_389ff13095374ece90ce63cbd7f7e10e",
    "displayName": "user-9bbce742",
    "avatarUrl": null
  }
]

At most 100, sorted by display name.

Are they connected?

GET /api/graph-access/connections/{id}/status, where {id} is a psd_ id your app has been shown:

{"connected": true}

An id your app has not been shown returns {"connected": false}.

Mutual connections

GET /api/graph-access/connections/{id}/mutuals lists the people connected to both the token’s person and {id}, in the connection list shape:

[
  {
    "userId": "psd_389ff13095374ece90ce63cbd7f7e10e",
    "displayName": "user-9bbce742",
    "avatarUrl": null
  }
]

An id your app has not been shown returns [].

Memberships

Record your own id for the token’s person, POST /api/graph-access/membership:

curl https://socialgraph.stackwell.tech/api/graph-access/membership \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"appUserId": "acme-user-1842", "appDisplayName": "Alice P."}'
{
  "appUserId": "acme-user-1842",
  "appDisplayName": "Alice P.",
  "createdAt": 1793911264,
  "updatedAt": 1793911264
}

Times in responses are whole seconds since the Unix epoch (UTC), as JSON integers.

appUserId is required and must not already belong to another person in your app (409 That app user id is already claimed for this app). appDisplayName is optional.

GET /api/graph-access/membership returns {"membership": {...}} in that shape, or {"membership": null}. DELETE /api/graph-access/membership removes it and returns {"ok": true}.

Which of the person’s connections use your app, GET /api/graph-access/connections/members:

[
  {
    "userId": "psd_389ff13095374ece90ce63cbd7f7e10e",
    "displayName": "user-9bbce742",
    "avatarUrl": null,
    "appUserId": "acme-user-2210",
    "appDisplayName": "Bob",
    "registeredAt": 1793911264
  }
]

Public endpoints

curl -fsS https://socialgraph.stackwell.tech/health | grep -Fx '{"ok":true}'