sondahub / Identity API
Identity: a fake user directory API
A company directory: people, groups and memberships — behind SCIM 2.0, SAML and OpenID Connect.
Three hundred people at a made-up company, Orbit Labs, in forty groups — departments, teams, roles, offices — with managers, titles and employee numbers. The same directory answers SCIM 2.0 at /scim/v2, signs people in over SAML at /saml/sso and fills OpenID Connect userinfo, so the person SCIM lists is the person who logs in. 1,797 records in all, free over REST, GraphQL, gRPC-Web, WebSocket and SSE, and through SCIM 2.0, SAML 2.0 and OpenID Connect — no key, no signup.
Connect
- Base URL
- https://api.sondahub.com/v1/identity
- OpenAPI 3
- https://api.sondahub.com/v1/identity/openapi.json
- GraphQL
- https://api.sondahub.com/v1/identity/graphql
- gRPC-Web
- https://api.sondahub.com/grpc/sondahub.identity.v1.IdentityService
- .proto
- https://api.sondahub.com/v1/identity/identity.proto
- WebSocket
- wss://api.sondahub.com/v1/identity/ws
- SSE
- https://api.sondahub.com/v1/identity/events
- The data
- users.json, groups.json, memberships.json
In Sonda: Import → From a URL with the OpenAPI address and the whole API lands as a project, one request per operation with example bodies. No keys, no headers to add. More on each protocol.
X-Sondahub-Session: send that token back and the next request sees your write. The server keeps nothing — without the token the world is the seed again. The examples on this page share one session until you reload. More on sessions.curl https://api.sondahub.com/v1/identity
Lists and filters
Every list answers { "data": [...], "meta": { "page", "limit", "total", "pages" } } with X-Total-Count and Link headers (next, prev, first, last). These options work on every collection and every nested route:
| Option | Meaning | Example |
|---|---|---|
page, limit | Paging, 1-based; limit 1–200, default 20. offset works too. | ?page=3&limit=50 |
sort | Comma list of fields, - for descending. Default id here. | ?sort=-id,id |
field=value | Equals. Booleans as true/false, null for missing. | ?id=1 |
_ne _gt _gte _lt _lte | Not equal and comparisons, on numbers, dates and strings. | ?id_gt=10 |
_like | Contains, case-insensitive. | ?user_name_like=an |
_in | Any of a comma list. | ?id_in=1,2,3 |
_null | true: missing; false: present. | ?display_name_null=true |
a.b=value | Inside a JSON field, dotted. | — |
q | Search across the text fields. | ?q=alpine |
fields | Only these fields back. | ?fields=id,user_name |
expand | Embed related records. | ?expand=manager,reports |
paging=cursor, cursor | Cursor paging: meta gains has_more, next_cursor and prev_cursor. starting_after / ending_before take an id, Stripe-style. | ?paging=cursor&limit=50 |
format | The same answer as CSV, XML, YAML, NDJSON or MessagePack — or ask with Accept. | ?format=csv |
A name that is not a field answers 400 and lists the fields. Writes answer 422 with one line per problem, 404 for a missing id, 405 with an Allow header for a verb a route does not take. Any request also takes chaos, a rate limit, an Idempotency-Key and problem+json errors, and any write a signed webhook.
users
People. user_name is unique and is the login everywhere (SAML, OIDC password grant: password "probe"); email is [email protected]. 300 records — the file, or the users API page with sample records.
user_name and email unique (409 already_exists, naming the user who has it); display_name defaults to the two names and active to true. The same people sign in through SAML and OIDC and are provisioned over SCIM.| Field | Type | Notes |
|---|---|---|
idread-only | int | Assigned by the server. Seed records keep their ids across restarts; records you create continue after the seed. |
created_atread-only | datetime | When the record was created (ISO 8601, UTC). |
updated_atread-only | datetime | When the record last changed. |
user_namerequired | string | Unique login. |
given_namerequired | string | |
family_namerequired | string | |
display_name | string | Defaults to given and family name. |
emailrequired | string | |
title | string | |
department | string | |
employee_number | string | |
manager_id | int → users | Who this person reports to; null for the CEO. |
phone | string | |
locale | string | |
timezone | string | |
office | string | |
active | bool | false: deprovisioned — cannot sign in. |
external_id | string | The id an upstream system (an HR tool, an IdP) knows this person by. |
last_login_at | datetime |
Relations: manager → one user through manager_id; reports → the users whose manager_id is this user; memberships → the memberships whose user_id is this user. Use ?expand=manager,reports,memberships to embed them, or the nested routes.
Endpoints
Try it
curl "https://api.sondahub.com/v1/identity/users?expand=manager&limit=3"
curl https://api.sondahub.com/v1/identity/users/1?expand=manager
curl "https://api.sondahub.com/v1/identity/users/1/reports?limit=5"
curl -i -X POST https://api.sondahub.com/v1/identity/users \
-H "Content-Type: application/json" \
-d '{"user_name":"ada.lovelace","given_name":"Ada","family_name":"Lovelace","email":"[email protected]","title":"Staff Engineer","department":"Engineering","employee_number":"E1042","locale":"en-US","timezone":"America/New_York","office":"New York"}'
curl "https://api.sondahub.com/v1/identity/users?sort=-id&limit=3" \ -H "X-Sondahub-Session: THE_TOKEN_FROM_THE_CREATE"
curl -X PATCH https://api.sondahub.com/v1/identity/users/1 \
-H "Content-Type: application/json" \
-d '{"user_name":"Changed user name"}'
curl -X DELETE https://api.sondahub.com/v1/identity/users/301 \ -H "X-Sondahub-Session: THE_TOKEN_FROM_THE_CREATE"
groups
Departments, teams, roles and offices. member_count follows the memberships. 40 records — the file, or the groups API page with sample records.
| Field | Type | Notes |
|---|---|---|
idread-only | int | Assigned by the server. Seed records keep their ids across restarts; records you create continue after the seed. |
created_atread-only | datetime | When the record was created (ISO 8601, UTC). |
updated_atread-only | datetime | When the record last changed. |
display_namerequired | string | |
slug | string | |
kind | enum | department team role office list |
description | text | |
external_id | string | |
member_countread-only | int |
Relations: memberships → the memberships whose group_id is this group. Use ?expand=memberships to embed them, or the nested routes.
Endpoints
Try it
curl "https://api.sondahub.com/v1/identity/groups?kind=team&limit=3"
curl https://api.sondahub.com/v1/identity/groups/1
curl "https://api.sondahub.com/v1/identity/groups/1/memberships?limit=5"
curl -i -X POST https://api.sondahub.com/v1/identity/groups \
-H "Content-Type: application/json" \
-d '{"display_name":"Platform Team","slug":"platform-team","kind":"team"}'
curl "https://api.sondahub.com/v1/identity/groups?sort=-id&limit=3" \ -H "X-Sondahub-Session: THE_TOKEN_FROM_THE_CREATE"
curl -X PATCH https://api.sondahub.com/v1/identity/groups/1 \
-H "Content-Type: application/json" \
-d '{"kind":"team"}'
curl -X DELETE https://api.sondahub.com/v1/identity/groups/41 \ -H "X-Sondahub-Session: THE_TOKEN_FROM_THE_CREATE"
memberships
A person in a group. POST one to add someone (409 if they are in it already); DELETE it to take them out. 1,457 records — the file, or the memberships API page with sample records.
already_member), defaults role to member and bumps the group’s member_count; DELETE lowers it.| Field | Type | Notes |
|---|---|---|
idread-only | int | Assigned by the server. Seed records keep their ids across restarts; records you create continue after the seed. |
created_atread-only | datetime | When the record was created (ISO 8601, UTC). |
updated_atread-only | datetime | When the record last changed. |
user_idrequired | int → users | |
group_idrequired | int → groups | |
role | enum | member owner |
Relations: user → one user through user_id; group → one group through group_id. Use ?expand=user,group to embed them, or the nested routes.
Endpoints
Try it
curl "https://api.sondahub.com/v1/identity/memberships?role=owner&expand=user&limit=3"
curl https://api.sondahub.com/v1/identity/memberships/1?expand=user
curl -i -X POST https://api.sondahub.com/v1/identity/memberships \
-H "Content-Type: application/json" \
-d '{"user_id":1,"group_id":1,"role":"member"}'
curl "https://api.sondahub.com/v1/identity/memberships?sort=-id&limit=3" \ -H "X-Sondahub-Session: THE_TOKEN_FROM_THE_CREATE"
curl -X PATCH https://api.sondahub.com/v1/identity/memberships/1 \
-H "Content-Type: application/json" \
-d '{"role":"owner"}'
curl -X DELETE https://api.sondahub.com/v1/identity/memberships/1458 \ -H "X-Sondahub-Session: THE_TOKEN_FROM_THE_CREATE"
WebSocket and SSE
The same stream two ways: the world's own activity, one tick a second, generated for your connection alone. Both push JSON text messages; SSE names each one with event: and numbers it with id:. ?topics=a,b narrows either.
| Topic | What arrives | How often |
|---|---|---|
directory | Someone joining, leaving, changing team, or being deactivated or reactivated. | ~6 s |
logins | A sign-in: who, how (SAML, OIDC, password), from where, and whether it worked. | 2 s |
wss://api.sondahub.com/v1/identity/ws?topics=directory
> {"type":"hello","api":"identity","topics":[…],"subscribed":[…]}
> {"type":"event","topic":"directory","api":"identity","ts":"…","data":{…}}
< {"type":"subscribe","topics":["directory"]} # narrow to some topics
< {"type":"ping"} # → {"type":"pong"}
< anything else # → echoed back as {"type":"echo"}
curl -N "https://api.sondahub.com/v1/identity/events?topics=directory"
retry: 3000
id: 1
event: directory
data: {"type":"event","topic":"directory",…}
GraphQL
One endpoint, https://api.sondahub.com/v1/identity/graphql: POST {"query", "variables"} or GET ?query=. Introspection is on, so Sonda's GraphQL mode loads the schema; the SDL is a click away. Every collection is a paged query with the same filter, sort and q options as REST (operators as suffixes: price_lt), a by-id query, relation fields both ways, and create, update, replace and delete mutations — simulated like every write, with the note in extensions.
curl https://api.sondahub.com/v1/identity/graphql -H "Content-Type: application/json" -d '{"query": "{ users(limit: 3, sort: \"-id\") { total data { id user_name given_name family_name manager { user_name } reports(limit: 2) { id } } } }"}'
{
users(limit: 3, sort: "-id") {
total
data {
id user_name given_name family_name
manager { user_name }
reports(limit: 2) { id }
}
}
}
gRPC-Web and Connect
The API is also the protobuf service sondahub.identity.v1.IdentityService, generated from the same registry as REST, so the wire and the docs cannot disagree. The .proto file is one click away; feed it to protoc, buf or your codegen. Calls go to https://api.sondahub.com/grpc/sondahub.identity.v1.IdentityService/{Method} over gRPC-Web (binary or text) or Connect (JSON or binary; GET for reads). Writes keep to the session like every other write. More on gRPC-Web.
| Method | What |
|---|---|
ListUsers | A page of users: page, limit, sort, q and filter (field → value, with the REST operator suffixes: price_lt → 20). |
GetUser | One user by id. |
CreateUser | Create a user (simulated, kept in your session token). id and the timestamps are set by the server. |
UpdateUser | Change the fields set in user (simulated). |
DeleteUser | Delete one (simulated); answers what was removed. |
ListGroups | A page of groups: page, limit, sort, q and filter (field → value, with the REST operator suffixes: price_lt → 20). |
ListMemberships | A page of memberships: page, limit, sort, q and filter (field → value, with the REST operator suffixes: price_lt → 20). |
Watchserver stream | The API's live activity as a server stream, each topic at its own pace: directory (every ~6 s), logins (every 2 s). max_events ends the stream (default 10, at most 120). |
Every collection has List, Get, Create, Update and Delete; the table shows them for users and the lists for the rest.
curl "https://api.sondahub.com/grpc/sondahub.identity.v1.IdentityService/GetUser?encoding=json&message=%7B%22id%22%3A1%7D"
curl -X POST https://api.sondahub.com/grpc/sondahub.identity.v1.IdentityService/ListUsers \
-H "Content-Type: application/json" \
-d '{"limit": 3, "sort": "-id"}'
SCIM, SAML and OpenID Connect
These people are not only rows. The same directory stands behind four doors, so a provisioning or sign-in integration can be tested end to end against one company:
| SCIM 2.0 | https://api.sondahub.com/scim/v2 | Users and Groups with filters, PATCH, Bulk and discovery; Bearer sonda-scim-token. |
| SAML IdP | https://api.sondahub.com/saml/metadata | Signs any directory user into your service provider; password probe. |
| SAML test SP | https://api.sondahub.com/saml/sp | Checks what your IdP sends, signature by signature. |
| OpenID Connect | https://api.sondahub.com/.well-known/openid-configuration | Sign in as any user (user_name / probe); id tokens carry their groups. |
curl -H "Authorization: Bearer sonda-scim-token" \ "https://api.sondahub.com/scim/v2/Users?filter=userName%20sw%20%22clara%22"