REFRACT Workflow

REFRACT Workflow API Reference

HTTP endpoint reference for creating workflow sessions, submitting steps, managing approvals, and querying workflow status.

All REFRACT Workflow endpoints are hosted on the Dispatcher Azure Function. Initiator and approver routes require a valid Entra ID JWT: a signed-in user's token with the user_impersonation scope, or an app-only token with the Render.Access permission (see Who may call). Approve, reject, request changes, escalate, and the Entra path of a governance decision refuse an app-only token with 403 and code APP_CANNOT_DECIDE: decisions are made by signed-in people. Participant portal routes use a portal access token, sent as Authorization: Bearer <pt>. A token in the URL (?pt=) is refused with 401 LINK_TOKEN_IN_URL. The participant's link carries a link reference (/w/<linkReference>), not the token: the portal resolves the reference and sends the token to the Dispatcher on the participant's behalf.

Base URL

All paths are relative to your Dispatcher Function base URL, e.g. https://<your-dispatcher>.azurewebsites.net.


Create Workflow Session

POST /api/workflow

Creates a new workflow instance and returns a portal URL for the participant.

Auth: Entra ID JWT (initiator)

Request body:

{
  "templatePath": "templates/vendor-onboarding.rpkg",
  "correlationId": "vc-2026-0601-acme",
  "templateSettings": {
    "workflow": {
      "schemaPath": "Workflows/vendor-onboarding.yaml",
      "recipients": [
        { "email": "vendor@contoso.com", "displayName": "Vendor Contact" }
      ],
      "requireOtp": false,
      "organizationName": "Contoso Ltd",
      "message": "Please complete the vendor onboarding form."
    }
  },
  "options": {
    "documentId": "VENDOR-2026-001",
    "docType": "vendor-contract"
  }
}
FieldTypeRequiredDescription
templatePathstringYes*Path to the .rpkg template. Used for the full render engine at seal time. Also accepted as templateId for legacy callers.
correlationIdstringNoCaller-supplied identifier. Auto-generated UUID if absent, and honoured if supplied - unlike a plain render request, workflow creation keeps the caller's own value.
templateSettings.workflow.schemaPathstringNoExplicit path to the workflow schema YAML (e.g. Workflows/vendor-onboarding.yaml). When absent, schema is discovered from structured_content nodes inside the .rpkg template.
templateSettings.workflow.recipientsarrayNoOne or more recipients who receive the portal invitation email.
templateSettings.workflow.requireOtpbooleanNoWhen true, participant must verify an OTP before accessing workflow steps. Default false.
templateSettings.workflow.organizationNamestringNoOrganization name shown in notification emails.
templateSettings.workflow.messagestringNoCustom body text included in the invitation email.
templateSettings.workflow.remindersobjectNoAutomatic reminder settings: enabled, firstAfterDays, repeatEveryDays, maxReminders. On by default; set only the fields you want to change. Out-of-range or non-integer values are refused with HTTP 400 and code WORKFLOW_REMINDERS_INVALID. See Reminders.
templateSettings.esignobjectNoREFRACT eSign settings to trigger automatically after the final PDF is sealed. Set enabled: true and provide signers, signingMode. See REFRACT eSign.
options.documentIdstringNoDocument identifier for audit and display (e.g. contract number, invoice ID).
options.docTypestringNoDocument type label (e.g. vendor-contract, expense-claim).
initialDataobjectNoPre-populated field data for governance workflows with autoValidateOnCreate: true. Keys must match field key values in the schema. When provided, validation runs immediately and portal interaction is skipped.

templateSettings.delivery does not currently send an invitation

Setting templateSettings.delivery on a workflow session is accepted, but the mode is not carried through to the post-seal delivery message, so no recipient email is sent. To deliver a sealed workflow PDF, call POST /api/delivery directly once the workflow reaches complete.

Schema path resolution

When templateSettings.workflow.schemaPath is set, the schema YAML is fetched directly from the SP assets library. When absent, REFRACT Workflow discovers the schema from structured_content nodes inside the .rpkg template file. Use schemaPath when your PDF layout template and workflow form schema are separate files.

Response 201:

{
  "workflowId": "a3f1c2d4-8e9b-4f0a-b2c3-1d4e5f6a7b8c",
  "correlationId": "vc-2026-0601-acme",
  "state": "in-progress",
  "portalUrl": "https://portal.refractplatform.com/w/<linkReference>"
}
FieldDescription
workflowIdUUID for this workflow session. Use this to poll status and perform approver actions.
correlationIdCorrelation ID (caller-supplied or auto-generated).
statein-progress (participant can begin entering data).
portalUrlParticipant portal URL, in the form /w/<linkReference>. The reference is registered with the portal, which resolves it; the URL carries no token. The link lives at most 1 day, and the portal reissues a shorter-lived, 4-hour portal token once the participant is inside (see When a Session Ends). When the portal does not answer the registration (unreachable, a timeout, or a 5xx), the workflow is not created and the answer is 503 WORKFLOW_LINK_UNAVAILABLE with Retry-After: 60: send the same request again after the wait. When the portal refuses it, the workflow is not created and the answer is 502 WORKFLOW_LINK_NOT_REGISTERED. See When a Link Cannot Be Registered Yet. Forward to the participant via email or your own notification.

Idempotency

Submitting the same correlationId twice returns the existing session (HTTP 200 with idempotent: true) rather than creating a duplicate.


Get Workflow (Portal Bootstrap)

GET /api/workflow/{workflowId}

Returns the workflow instance, schema, and current step state for the participant portal.

Auth: Portal access token (Authorization: Bearer <pt>). An Entra ID JWT is also accepted from a signed-in participant of the workflow or a caller with the Lifecycle.Reader role; anyone else gets 403 with code ROLE_MISSING. An app-only token is never a participant, so it needs the role.

Response 200: Full portal payload including schema steps, current data snapshots, and - only when the workflow is validation-failed - validationResults.


Submit Step

PATCH /api/workflow/{workflowId}/step/{stepIndex}

Saves the participant's field data for a single step. Can be called multiple times before the step is submitted.

Auth: Portal access token

Request body:

{
  "responses": {
    "vendor_name": "Contoso Supplies Ltd",
    "invoice_total": 47500,
    "payment_terms": "net30"
  }
}

The field is responses, an object keyed by field key.

Response 200: Updated step entity.


Advance Step

POST /api/workflow/{workflowId}/advance

Marks the current step as submitted and advances to the next step, or - on the final step - triggers schema validation. This route reads no request body; the current step's own entity must already be in submitted state (via the PATCH .../step/{stepIndex} call above) before advance will accept it.

Auth: Portal access token or Entra ID JWT

Preconditions (409 otherwise): the workflow must be in-progress or changes-requested, and the current step must already be submitted.

Response 200, mid-workflow:

{ "state": "in-progress", "currentStepIndex": 1, "portalToken": "..." }

Response 200, final step, validation passed:

{ "state": "pending-approval", "allStepsComplete": true, "approverStages": [ "..." ], "hasWarnings": false }

Response 200, final step, validation failed:

{
  "state": "validation-failed",
  "validationErrors": [
    {
      "ruleId": "po_amount_match",
      "severity": "error",
      "message": "Invoice total exceeds the approved PO amount.",
      "fieldKey": "invoice_total"
    }
  ],
  "validationWarnings": []
}

Approve Workflow

POST /api/workflow/{workflowId}/approve

Records the approver's decision and enqueues the seal message. The route does not read a request body - any comment sent is ignored.

Auth: Entra ID JWT of a signed-in approver. An app-only token gets 403 with code APP_CANNOT_DECIDE.

Response 200:

{ "workflowId": "a3f1c2d4-8e9b-4f0a-b2c3-1d4e5f6a7b8c", "state": "approved" }

Reject Workflow

POST /api/workflow/{workflowId}/reject

Records a rejection decision. No PDF is produced.

Auth: Entra ID JWT of a signed-in approver or reviewer. An app-only token gets 403 with code APP_CANNOT_DECIDE.

Request body:

{
  "reason": "Vendor not on approved supplier list."
}

reason is the only field read, and is optional with no minimum length.

Response 200:

{ "workflowId": "a3f1c2d4-8e9b-4f0a-b2c3-1d4e5f6a7b8c", "state": "rejected" }

Open for Review

POST /api/workflow/{workflowId}/review-open

Moves a pending-approval workflow to under-review and returns the full submission (steps, submitted data, validation results, change-request history) for the reviewer. Must be called before request-changes below.

Auth: Entra ID JWT from someone who reviews the workflow (a participant, the workflow administrator, an approver any stage names, or a member of the current stage's group), or a caller with the Lifecycle.Reader role. Anyone else in the tenant gets 403 with code ROLE_MISSING.


Request Changes

POST /api/workflow/{workflowId}/request-changes

Returns the submission to the participant with structured change requests. Requires the workflow to already be under-review (call review-open first) - 409 otherwise. Workflow transitions to changes-requested.

Auth: Entra ID JWT of a signed-in person. An app-only token gets 403 with code APP_CANNOT_DECIDE.

Request body:

{
  "comments": "Invoice total does not match attached invoice PDF.",
  "changeRequests": [
    {
      "message": "Invoice total does not match attached invoice PDF.",
      "fieldKey": "invoice_total",
      "severity": "error"
    }
  ],
  "notifyParticipant": true
}
FieldTypeDescription
commentsstringOptional free-text note. If changeRequests is omitted, this becomes a single synthesized change request.
changeRequestsarrayOptional; the field is changeRequests, not changes. At least one of comments/changeRequests is required.
changeRequests[].messagestringHuman-readable correction instruction shown to participant.
changeRequests[].fieldKeystringOptional; links the change request to a specific field for portal highlighting.
changeRequests[].severityinfo | warning | errorUrgency indicator.
notifyParticipantbooleanOptional.

Get Workflow Status

GET /api/workflow/{workflowId}/status

Returns the current state and key milestones.

Auth: Portal access token, or an Entra ID JWT from a signed-in participant of the workflow (its initiator included) or a caller with the Lifecycle.Reader role. An app-only token is never a participant, even of a workflow it created, so it needs the role. Without either, 403 with code ROLE_MISSING.

Response 200:

{
  "workflowId": "a3f1c2d4-8e9b-4f0a-b2c3-1d4e5f6a7b8c",
  "correlationId": "vc-2026-0601-acme",
  "state": "pending-approval",
  "currentStepIndex": 2,
  "totalSteps": 2,
  "title": "Vendor Onboarding",
  "createdAt": "2026-06-09T10:00:00.000Z",
  "updatedAt": "2026-06-09T10:05:00.000Z",
  "completedAt": null,
  "sealedBlobPath": null,
  "approverHash": null,
  "decidedAt": null,
  "decisionAction": null,
  "decisionComment": null,
  "approverEmail": null,
  "recipientEmail": "vendor@contoso.com"
}

validationOutcome, validationResults, and currentApprovalStage are not part of this response. validationResults does appear, but only on the portal-bootstrap route (GET /api/workflow/{workflowId}, when the workflow is validation-failed) and on review-open.


Workflow States

draft | in-progress | validating | validation-failed | pending-approval |
under-review | changes-requested | escalated | approved | rejected |
sealing | complete | cancelled

These are the exact state strings used throughout the API. There is no separate isComplete/isRejected boolean pair on this API - that vocabulary belongs only to the Power Platform connector's own swagger layer.


Attachments

POST /api/workflow/{workflowId}/attachment?fieldKey=...&filename=...
GET  /api/workflow/{workflowId}/attachment?blobPath=...

Uploads and downloads an attachment field's file. The upload route reads the raw request body directly - there is no pre-signed SAS upload step. A 25 MB size cap and a content-type allowlist (PDF, PNG, JPEG, GIF, WebP, Word, Excel, plain text) are enforced on both the declared Content-Type and the file's own bytes; a mismatch between the two is rejected.

Auth: Upload: portal access token. Download (GET): Entra ID JWT only; a portal token is refused and uses POST /api/workflow/{workflowId}/attachment/download instead. The workflow's signed-in participants pass. An app-only token needs the Lifecycle.Reader role (otherwise 403 ROLE_MISSING). Any other signed-in person needs Lifecycle.Reader and also the document-access administrator role named by the Dispatcher's DOCUMENT_ACCESS_ADMIN_APP_ROLE setting, which standard deployments do not configure. The attachment's restriction policy, when it has one, still applies.


Governance Decision

POST /api/workflow/{workflowId}/governance-decision

Records an approver decision on a governance workflow. Requires the workflow to be validation-failed - 409 otherwise.

Auth: Portal access token (reviewer), or the Entra ID JWT of a signed-in person. An app-only token gets 403 with code APP_CANNOT_DECIDE.

Request body:

{
  "action": "approve-exception",
  "comment": "Invoice exceeds PO by 1.6% - within tolerance; approved."
}
action valueBehaviour
approve-exceptionOne or more error rules failed; approver overrides with a written justification. comment must be at least 10 characters. Justification sealed into PDF.
rejectSubmission rejected. No PDF produced. comment has no minimum length.
request-correctionReturns the workflow to validation-failed with a message for the participant. comment has no minimum length.

The fields are action and comment - not decision and justification.


Resend Invitation

POST /api/workflow/{workflowId}/resend-invite

Re-sends the workflow invitation email to one party: the participant or the approver. Subject to portal-side rate limiting.

Auth: Entra ID JWT: a signed-in user, or an app-only token with Render.Access.

Request body:

{ "party": "approver" }
FieldTypeRequiredDescription
partyparticipant | approverYesWho to re-send to. The request is refused with 400 if party is missing, and with 409 if it names the party the workflow is not waiting on.

Response: 200 { "ok": true, "sentToHash": "..." } when the invitation was queued. When the portal cannot register the link just now, the invitation is held and sent once the link registers, and the answer is 202 { "ok": true, "held": true, "sentToHash": "..." } (see When a Link Cannot Be Registered Yet).

In an escalated workflow, a Resend to the approver goes to the assigned approver, never to the escalation target. A Resend does not move the workflow's updatedAt, and the next automatic reminder waits while the person was emailed in the last 12 hours. See Reminders.


Escalate Workflow

POST /api/workflow/{workflowId}/escalate

Escalates the workflow to a different approver. Requires the workflow to be under-review or pending-approval.

Auth: Entra ID JWT of a signed-in initiator. An app-only token gets 403 with code APP_CANNOT_DECIDE.

Request body:

{ "escalateTo": "director@contoso.com", "comments": "No response after 5 days" }

escalateTo (an email address) is required; comments is optional. New state: escalated.

Once a stage is escalated, the assigned approver is no longer sent automatic reminders. See Reminders and escalation.


OTP - Send Code

POST /api/workflow/{workflowId}/otp/send

Sends a one-time passcode to the participant's email. Only valid when requireOtp: true.

Auth: Portal access token. Rate-limited to one send per 60 seconds.


OTP - Verify Code

POST /api/workflow/{workflowId}/otp/verify

Verifies the participant's OTP code.

Auth: Portal access token

Request body: { "otp": "483921" }

OTP constraints: 6 digits, 10-minute TTL, 5-attempt lockout.

Configure Your First Workflow

See the Workflow Schema reference for step definitions, validation rules, and approval routing.

On this page