From agentforge
Enforces the core-be public API contract conventions — snake_case route params, prefixed public ids, the uniform method→status policy, and the header naming matrix — across routes, validators, tests, OpenAPI/Postman docs, and the route-status gates. Use when adding or changing any route, param, header, public id, or response status.
How this skill is triggered — by the user, by Claude, or both
Slash command
/agentforge:api-contract-guardThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Every path param is snake_case and semantic: `{plan_id}`, `{subscription_id}`, `{session_id}`, `{upload_id}`, `{auth_method_id}`, … — never `{id}` or camelCase. The active organization is NOT a path param — it is carried by the signed `org` JWT claim; the active-org resource is the singular `/tenancy/organization` (sub-resources like settings/memberships/roles/api-keys nest under it).
{plan_id}, {subscription_id}, {session_id}, {upload_id}, {auth_method_id}, … — never {id} or camelCase. The active organization is NOT a path param — it is carried by the signed org JWT claim; the active-org resource is the singular /tenancy/organization (sub-resources like settings/memberships/roles/api-keys nest under it).PARAM_NAME_TO_ENTITY (src/shared/utils/identity/public-id.util.ts) so validators, test materializers, and OpenAPI docs derive the entity automatically.<prefix>_<21 chars of [a-z0-9]> (e.g. org_a1b2c3d4e5f6g7h8i9j0k); prefixes live in PUBLIC_ID_PREFIXES.generatePublicId(entity) requires the entity; never hand-roll ids. Externally the field is always id — the words "public id/publicId" never appear in documentation.public_id to varchar(28), add the prefix to the map, the migration backfill, and the OpenAPI parameter docs come free via the map.*.dto.ts z.object keys) and response body property (*.serializer.ts output keys) is snake_case: file_name, content_type, created_at, avatar_key — never fileName/createdAt. The one external identifier stays id.errors[].field (and field-keyed details) values name the body property, so they are snake_case too (e.g. field: 'content_type', not 'contentType').navigator.credentials JSON (rawId, clientDataJSON, …) — and JWT claims.src/tests/unit/api/snake-case-body-keys.policy.unit.test.ts (scans every *.dto.ts / *.serializer.ts; allowlist for the documented exceptions). Renaming a body/response key is an API change → run the breaking-change gate (see sync checklist).| GET | POST | PUT/PATCH | DELETE |
|---|---|---|---|
| 200 | 201 | 200 | 204 |
Exceptions (protocol-owned, stay 200): POST /billing/webhook, POST /api/v1/mcp.
The policy is enforced centrally in method-status-policy.middleware.ts; declared statuses live in tooling/openapi/route-catalog/route-success-statuses.json and are runtime-verified by pnpm validate:route-success-coverage (drift fails CI).
details; documented on every POST/PATCH/PUT; only a param-less, query-less GET/DELETE documents no 400 at all.Authorization: Bearer <ACCESS_TOKEN>).assertTeamOrganization(...) guard, not 409), or X-Idempotency-Key reused with a different payload.Retry-After + X-RateLimit-* headers.assertTeamOrganization(organization, capability) (src/domains/tenancy/sub-domains/organization/organization-capability.ts).capabilities object (can_invite_members, can_manage_members, can_manage_roles, can_transfer_ownership, can_delete) describing the org type's capability (not the caller's permission), so clients discover this without probing for a 422.O column (both | team), kept in sync with tooling/openapi/route-catalog/route-org-scope.json by pnpm validate:route-org-scope.Authorization: Bearer <ACCESS_TOKEN> — every authed route (OpenAPI security scheme; Postman collection-level bearer {{ACCESS_TOKEN}}).Content-Type: application/json — any body.X-Organization-Id — legacy header read directly by a few consumers (e.g. the upload domain); org-scoped routes resolve the active organization from the signed org JWT claim, NOT this header. Switch the active org via /auth/switch-to-personal / /auth/switch-to-organization (which re-mint the access token).X-Idempotency-Key — all mutating routes (optional, auto-generate in clients); REQUIRED on the 8 writes registered with config.idempotencyRequired: true (org create, memberships, transfer-ownership, invitations, subscription create/change-plan/cancel/resume).X-Captcha-Token — public auth forms only (login, magic-link send, password forgot/reset, email verify, webauthn authenticate options, oauth authorize).X-CSRF-Token — POST /auth/refresh only (double-submit of the csrf_token cookie). Keeps the X- form (frontend-framework default).Stripe-Signature — sent BY Stripe to the webhook routes; the app never sends it.X-Request-Id, X-Client-Request-Id, X-Api-Key, X-CSRF-Token, X-RateLimit-* (server-emitted with Retry-After on 429), Helmet's security headers, X-Forwarded-For. Custom headers use the X- form for visual consistency with the infrastructure headers: X-Organization-Id, X-Idempotency-Key, X-Idempotency-Replay (response marker), X-Captcha-Token. Standards keep their fixed names: Authorization, Stripe-Signature, Retry-After.
pnpm routes:catalog → registry key updates in route-success-statuses.json.METHOD /path keys.ROUTE_EXAMPLE_CAPTURE=1 pnpm test && pnpm routes:examples to refresh captured samples.pnpm docs:generate:multilang && pnpm docs:postman then pnpm docs:check.validate:route-success-statuses, validate:route-success-coverage, unit suites for the response map and examples fixture.pnpm docs:breaking (local mirror of the CI oasdiff gate); intentional breaks get narrow entries in .github/oasdiff/breaking-changes-ignore.txt.npx claudepluginhub nikunjmavani/core-beGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.