Skip to content

Transition the engagement state

Opportunities module

POST/engagements/{id}/transitionsengagements.transition

Moves the engagement state machine through its transition RPC, which writes its ledger. Active is refused until every blocking onboarding item passes. Complete narrows external access to money and documents. Requires `Idempotency-Key`; a replay returns the original response. A rule that refuses the request returns 422 with `refusal` and `reason`.

  • Requires the capability opportunities.engagement.write.
  • OAuth scopes: opportunities.
  • Send an Idempotency-Key header. A replay with the same key returns the original response.
  • Error statuses: 400, 401, 404, 409, 422, 429. Errors are RFC 9457 problem details.

Path and query parameters

engagements.transition: Path and query parameters
FieldTypeRequiredDescription
idstring (uuid)YesIdentifier of the engagement.

Request body: EngagementTransitionRequest

engagements.transition: EngagementTransitionRequest
FieldTypeRequiredDescription
expected_state"Onboarding" | "Blocked" | "Active" | "Closing" | "Complete" | "Canceled"YesThe state the caller believes the row is in (compare-and-set).
to_state"Onboarding" | "Blocked" | "Active" | "Closing" | "Complete" | "Canceled"YesTarget state.
reasonstringNoReason, required by some transitions.Constraints: length 1 to any
{
  "expected_state": "Onboarding",
  "to_state": "Active",
  "reason": "Client approved the revised scope."
}

Response 200: Engagement

engagements.transition: Engagement
FieldTypeRequiredDescription
idstring (uuid)YesIdentifier of the engagement.
org_idstring (uuid)YesIdentifier of the owning organization, derived from the parent row and never accepted from the caller.
created_atstring (date-time)YesWhen the row was created.
created_bystring (uuid) | nullYesIdentifier of the person who created the row; null when a seed, platform or system process created it.
updated_atstring (date-time)YesWhen the row was last updated.
updated_bystring (uuid) | nullYesIdentifier of the person who last updated the row; null when a seed, platform or system process updated it.
person_idstring (uuid) | nullYesIdentifier of the engaged person.
organization_idstring (uuid) | nullYesIdentifier of the engaged external account.
project_idstring (uuid) | nullYesIdentifier of the project.
offer_idstring (uuid) | nullYesIdentifier of the accepted offer.
role_typeExternalRoleTypeYesExternal role type (Section 4.4.2).Constraints: one of "Client", "Vendor", "Contractor", "Crew", "Staff", "Artist", "Artist Representative", "Sponsor"
engagement_stateEngagementStateYesEngagement lifecycle state. Active requires every blocking onboarding item to pass.Constraints: one of "Onboarding", "Blocked", "Active", "Closing", "Complete", "Canceled"
activated_atstring (date-time) | nullYesWhen it became Active.
completed_atstring (date-time) | nullYesWhen it became Complete.
application_idstring (uuid) | nullYesIdentifier of the accepted application the engagement came from.
worker_classificationstring | nullYesWorker classification, from xpms.worker_classification; null for Client and Sponsor engagements.Constraints: pattern ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$
pay_basisstring | nullYesPay basis; with the arrangement a permitted canon pair.Constraints: length 1 to 300
arrangementstring | nullYesArrangement; with the pay basis a permitted canon pair.Constraints: length 1 to 300
start_datestring (date)YesFirst day of the engagement; defaults to the offer's or the opportunity's.
end_datestring (date) | nullYesLast day of the engagement; defaults to the offer's or the opportunity's.
counterparty_idstring (uuid) | nullYesIdentifier of the counterparty (vendor) the holder is engaged through; null when direct.
{
  "id": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f",
  "org_id": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f",
  "created_at": "2026-10-08T14:30:00Z",
  "created_by": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f",
  "updated_at": "2026-10-08T14:30:00Z",
  "updated_by": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f",
  "person_id": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f",
  "organization_id": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f",
  "project_id": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f",
  "offer_id": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f",
  "role_type": "Crew",
  "engagement_state": "Onboarding",
  "activated_at": "2026-10-08T14:30:00Z",
  "completed_at": "2026-10-08T14:30:00Z",
  "application_id": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f",
  "worker_classification": "INDEPENDENT_CONTRACTOR",
  "pay_basis": "Day Rate",
  "arrangement": "Per Engagement",
  "start_date": "2026-10-08",
  "end_date": "2026-10-08",
  "counterparty_id": "0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f"
}

Code samples

curl -X POST "$XOS_BASE_URL/engagements/0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f/transitions" \
  -H "Authorization: Bearer $XOS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"expected_state":"Onboarding","to_state":"Active","reason":"Client approved the revised scope."}'
const { data } = await xos.engagements.transition("0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f", {
    "expected_state": "Onboarding",
    "to_state": "Active",
    "reason": "Client approved the revised scope."
  });
response = xos.engagements.transition("0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f", {"expected_state": "Onboarding", "to_state": "Active", "reason": "Client approved the revised scope."})
xos engagements transition 0192f1e2-7c3a-7b4d-9e8f-1a2b3c4d5e6f --data '{"expected_state":"Onboarding","to_state":"Active","reason":"Client approved the revised scope."}'