POST /api/v1/sync
Drive a Shrubbery state transition from an external system. The lookup is keyed by (externalSystem, externalId, assigner) where assigner = token owner; the matching row is updated to the requested status. completed_at and activated_at are populated server-side when relevant.
Quick start
curl -X POST https://shrubbery.eu/api/v1/sync \
-H "Authorization: Bearer $SHRUBBERY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"externalSystem": "jira",
"externalId": "PROJ-123",
"newStatus": "Completed"
}'A successful response is 200 OK with the Shrubbery's id and the applied newStatus.
Reference
- Try it interactively:
/api-reference#tag/webhooks/post-api-v1-sync— Scalar's sample switcher (curl / TypeScript / Python / Ruby / Go) + Test Request client with Bearer-token paste. - OpenAPI JSON:
content/docs/_generated/openapi.json— auto-emitted from Zod, no manual drift.
Notes
- Scope is implicit. The lookup is silently filtered to rows where the assigner equals the token owner. There is no way for a token to mutate another user's rows; a mismatched external ID returns
404 not_foundrather than leaking existence. - Allowed transitions. Sync accepts
Active,Completed, andRefused. It does not acceptPending_Handshake(no path to retract consent via external systems),Draft(drafts have no external mapping), orRenegotiated(which is an event type, not a status — see the Renegotiation Flow). - Audit attribution. The resulting
shrubbery_eventsrow carriesactor_kind = api_tokenwith the calling token's ID. CFS arithmetic treats the transition identically to a user-driven one; the Dossier surfaces the actor distinction. - Idempotency. Replaying the same transition is safe: the second call updates the row to the already-current status and returns
200.completed_at/activated_atare only set on the first transition into the relevant state, not refreshed.
Last updated: 17 May 2026