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"
}'
| Field | Required | Rules |
|---|---|---|
displayName | Yes | Not blank. Shown to people on the approval page. |
description | Yes | May be empty. Shown on the approval page. |
platform | Yes | web or native |
redirectUri | Yes | https:// for web; a custom scheme with a host for native |
clientType | No | confidential (default) or public |
chirpClientId | No | The 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"
}'
| Field | Rules |
|---|---|
clientId | Required |
authorizationCode | Required. The code from the redirect. |
redirectUri | Required. The same redirect URI as the approval. |
codeVerifier | Required if the approval carried a code_challenge |
clientSecret | Required 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:
| Status | error | Cause |
|---|---|---|
400 | Redirect URI does not match the registered app | |
400 | Authorization code has expired | Older than 5 minutes |
400 | Code verifier is required | The approval carried a challenge |
400 | Public clients must initiate authorization with a PKCE code_challenge | A public app’s approval carried no challenge |
403 | Code verifier is invalid | The verifier doesn’t match the challenge |
403 | Client secret is invalid | A confidential app sent no clientSecret or a wrong one |
403 | app_not_yet_active, app_disabled, app_suspended, app_terminated | The app is not active |
404 | App not found | Unknown clientId |
404 | Authorization code not found | Wrong code, or a code issued to another app or redirect URI |
409 | Authorization 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}'