verification
use it
# add to stunt.yaml
services:
linkedin:
adapter: embedded:linkedin-style
# then
$ stunt upThe embedded: source extracts this adapter from the binary — no git clone, fully offline.
how to mock the LinkedIn API locally
Install stunt, paste the service block above into stunt.yaml, run stunt up — then point your client at localhost instead of LinkedIn API. It works offline, keeps state across requests and restarts, and every response is synthetic: the route shapes, status codes, pagination, and webhook delivery follow the real LinkedIn API v2 surface — the data is fake, the behavior is real.
backing stores
tokensrefresh_tokenscodespostscomments
api surface (8)
generated from the adapter manifest — exactly what boots, nothing more
8 routes
GET/oauth/v2/authorization
POST/oauth/v2/accessToken
GET/v2/userinfo
POST/v2/ugcPosts
GET/rest/comments
POST/rest/comments
GET/rest/posts/{urn}
GET/rest/memberCreatorPostAnalytics
VM-verified behaviors (43)
- ✓authorize without redirect_uri, state or client_id is invalid_request
- ✓authorize redirects back with a fresh code and the state echoed
- ✓a redirect_uri that already carries a query is joined with &
- ✓the exchange demands grant_type=authorization_code
- ✓an unknown code is 400 invalid_grant
- ✓a good exchange mints a 60-day token pair for a fresh member
- ✓the code is single-use: a replay is invalid_grant
- ✓client mismatches are 400 invalid_client
- ✓a mismatched attempt must not burn the code
- ✓a second flow mints a distinct member
- ✓the refresh grant demands client creds
- ✓an unknown refresh token is invalid_grant
- ✓refresh rotates the pair and keeps the member
- ✓the presented refresh token is single-use
- ✓rotation chains: the new refresh token refreshes again
- ✓a missing bearer is 401 in the service error envelope
- ✓wrong schemes and unknown bearers answer the same 401
- ✓every API route enforces the same gate
- ✓a bearer dies at its clock-derived 60-day expiry
- ✓userinfo returns the OAuth member profile
- ✓publishing as anyone but the token's member is a 403
- ✓a good publish mints a ugcPost urn echoed in x-linkedin-id
- ✓the post resolves to a share urn carrying its own author
- ✓resolving an unknown urn is a 404
- ✓unconfigured, publishing is unthrottled
- ✓arming fail_after injects 429 REQUEST_LIMIT_EXCEEDED
- ✓the limit is per member
- ✓a throttled attempt creates no post
- ✓q must be author
- ✓reply resolves urn:li:person:me to the authenticated member
- ✓commenting as anyone but the caller is a 403
- ✓replying to an unknown object is a 404
- ✓ingest lists only the token member's comments
- ✓member B's comment resolved me and lists only under B
- ✓createdOn is clock-stamped and monotonic
- ✓count pages with a next link that round-trips the query
- ✓without count the whole list returns unpaged
- ✓a malformed start cursor is a 400
- ✓an unknown entity is a 404
- ✓each queryType totals base+3/5/7/11 split across two daily buckets
- ✓entity accepts both the parenthesized and bare urn forms
- ✓start past the data returns an empty page
- ✓an unknown queryType falls back to the base total (deviation, as-is)
Not implemented (4)
- No ugcPosts delete or edit (DELETE /v2/ugcPosts/{id})
- No organizations or company page API (GET /v2/organizations)
- No reactions or likes endpoints (socialActions likes)
- No video upload flow or multi-image carousel posts
Differs from the real API (3)
- Rate-limit injection on POST /v2/ugcPosts publish is a simulator test hook
- An unknown analytics queryType silently falls back to the base total; real LinkedIn 400s
- Refresh tokens never expire; only access tokens carry the 60-day expiry
want more of the LinkedIn API surface? adapters are YAML + Starlark — the authoring guide covers adding routes, fixtures, and state machines. Open an issue for anything big first, or just file a PR — merged routes ship in the next release.