REFRACT Dispatcher API

REFRACT Dispatcher API

Overview of the customer-hosted REFRACT Dispatcher's HTTP API, its Microsoft Entra ID App Registration, and the two authentication models available to callers.

Every REFRACT Platform deployment exposes its rendering, e-signature, workflow, and delivery capabilities through a single customer-hosted Azure Function App: the Dispatcher. All integrations, whether a Power Automate flow, a Postman test, an ERP connector, or a script, ultimately call this same API.

Single-tenant, self-hosted

There is no shared Refract Logic service in the request path. The Dispatcher runs inside your own Azure subscription, and every access token is issued by your own Microsoft Entra ID tenant.

No function key

Dispatcher HTTP routes are registered anonymous at the Azure Functions level - there is no ?code= function key. Every request is authenticated by validating the Microsoft Entra ID JWT inside the route handler instead. See Authentication.


The "REFRACT Dispatcher API" App Registration

Every Dispatcher deployment provisions one Microsoft Entra ID App Registration, named "REFRACT Dispatcher API" by default (infra/deploy.ps1 creates and configures it automatically as part of the Onboarding session phase of the customer deployment guide). This app registration is a resource, not a client: it defines the API surface that other applications request tokens for. It is configured with:

ConfigurationPurpose
Application ID URI (api://<appId>)Lets Microsoft Entra ID resolve api://<appId> as a token audience
Render.Access app roleApplication permission every client-credentials (app-only) caller must hold. The Dispatcher refuses an app-only token without it on every authenticated route with 403, code ROLE_MISSING, and "role": "Render.Access". Grant it under the calling connector app's API permissions as an application permission, then grant admin consent. infra/deploy.ps1 creates it but grants it to no app. See Who may call.
Lifecycle.Reader app roleAssignable to users and groups, and grantable to other apps as an application permission. The Dispatcher requires it, in the token's roles claim, for reads of document records: audit events, signing and delivery status, workflow data and exports, opening a workflow for review, stored files opened through /api/doc and /api/gateway/v1/documents/open, evidence exports and archives, Section 508 outputs, the integrity verdict, verification and reproduction, explain traces and PDFs, and batch records; for a signed-in user re-rendering a stored payload or sending a stored document (an app token sends with Render.Access alone); for an app reading a workflow's sealed PDF or attachments; and, together with the document-access administrator role, for a signed-in person who is not one of the workflow's participants reading them. Without it those routes return 403 with code ROLE_MISSING. See Access: the Lifecycle.Reader Role, and Give people access to document records for assigning it to people.
user_impersonation delegated scopeDelegated permission for callers that sign in as a person: REFRACT Lifecycle (once the SPFx API access request is approved) and Postman testing. A signed-in user's token must carry it on every authenticated route, or the Dispatcher returns 403 with code SCOPE_MISSING.
Its own user_impersonation permission, admin-consentedinfra/deploy.ps1 adds it. Nothing in REFRACT signs in as this app, and callers should not: each one uses its own app registration.

infra/deploy.ps1 creates no client secret for this app, and it needs none: it is only ever the resource. Every caller is a separate app registration with its own credentials, such as a connector app for each source system. See Connect a source system.


Two Ways to Call the Dispatcher

Pick the model that matches your caller

Which authentication model you use depends on whether a human is present when the call happens.

Delegated (Authorization Code + PKCE)Client Credentials
Who signs inA human, interactivelyNobody - service-to-service
Client appREFRACT Lifecycle: SharePoint's own client for SPFx solutions. Postman: a testing app you register.A connector app you register for each source system
Client secret neededNot for REFRACT LifecycleYes, the connector app's own
Typical useSPFx dashboard, Postman / manual API testingPower Platform connector, ERP integrations (Business Central, Dynamics), scripts, scheduled jobs
Where it's documentedAuthentication, Testing with PostmanConnect a source system, Power Platform Connector Setup

Business Central, Dynamics 365 Sales, Dynamics GP, and the Power Platform connector

Each source system authenticates as its own connector app and requests a token for api://<dispatcher-app-client-id>/.default, where <dispatcher-app-client-id> is the client ID of "REFRACT Dispatcher API". The client ID and secret are the connector's; the scope is the same for every caller. Each connector needs the Render.Access application permission, and Lifecycle.Reader for the records it reads. See Connect a source system for the model, the portal steps, and each system's settings.


Endpoints

EndpointAuth modelRate limitNotes
POST /api/renderEitherNoneThe core render endpoint. Returns the rendered PDF as raw bytes. A signed-in user re-rendering a payload that has been rendered before needs the Lifecycle.Reader role; see Re-rendering a stored payload. Also used for REFRACT Dispatch batch submission, when the payload is an array and templateSettings.distribution is set.
GET /api/render?ping=trueEitherNoneHealth check. Returns a status PDF; 200 when healthy, 503 when not. See Status Route below.
POST /api/gateway/v1/documents/generateEither100 requests / 60sGateway route used by the Power Platform connector and suitable for any pure-OAuth integration. Same rendering logic as /api/render.
GET /api/gateway/v1/documents/openEither100 requests / 60sStreams the document bytes directly - the response body is the file, not a URL. Requires the Lifecycle.Reader role.
GET /api/gateway/v1/workflows/{workflowId}Either100 requests / 60sPoll workflow/signature status. Requires the Lifecycle.Reader role.
GET /api/gateway/v1/workflows/{workflowId}/pdfEither100 requests / 60sStreams the sealed PDF once complete. Open to the workflow's signed-in participants, and to an app token holding Lifecycle.Reader as well as Render.Access. A signed-in person who is not a participant needs Lifecycle.Reader and the document-access administrator role named by the Dispatcher's DOCUMENT_ACCESS_ADMIN_APP_ROLE setting, which standard deployments do not configure. The document's restriction policy, when it has one, still applies.
GET/POST /api/dispatch/batch/*EitherNoneREFRACT Dispatch batch lifecycle management - see REFRACT Dispatch API Reference.

The 100 requests / 60 second rate limit applies only to the four /api/gateway/v1/* routes, keyed per tenant and calling application. /api/render and the batch-dispatch routes are not rate limited.

Every request, regardless of endpoint or auth model, is validated against the same JWT claims:

ClaimValidates
audToken was issued for this Dispatcher's App Registration (accepted as either the bare client ID or the api://<client-id> form)
issToken was issued by this tenant's own Microsoft Entra ID
tidToken belongs to the authorised tenant
expToken has not expired

Status Route

GET /api/render?ping=true (or ?ping=status) returns a status-report PDF describing the health of the Dispatcher. It returns HTTP 200 when the system is healthy and 503 when it is not. A plain GET /api/render with no ping parameter also renders a status PDF, but always returns 200 regardless of health - use the ping parameter for monitoring probes that need the real status code.


Response Headers

Render responses carry X-Refract-CorrelationId. Which other headers are present depends on the request:

HeaderPresent whenValue
X-Refract-CorrelationIdRender responses, refusals includedThe correlation ID for this render. Empty when the request failed before one was assigned.
X-Refract-UploadStatusThe document was stored, or storing it was skippedok when stored, skipped when not
X-Refract-UploadErrorStoring was skipped because no storage is configured or generatedDocuments is not enabledThe reason
X-Refract-WebUrl / X-Refract-DriveItemIdThe document was stored in SharePointSharePoint item URL and DriveItem ID
X-Refract-BlobPathThe document was storedWhere the stored document is: its blob path, or its SharePoint URL. On a render that starts a workflow or is queued in relay mode, the intake receipt's location. On an error response, see Error Headers
X-Refract-AuditStatusThe audit write was skipped or failedskipped:<reason> or failed:<reason>
X-Refract-SkippedFeaturesA requested feature was skipped for the current licenseComma-separated feature list
X-Refract-WorkflowId / X-Refract-WorkflowPortalUrlThe render started a workflowThe workflow ID, and the participant's portal URL (/w/<linkReference>)
X-Refract-IntakeReceiptBlobPathA render that started a workflow or was queued in relay mode, when its intake receipt was storedThe intake receipt's location
X-Refract-Status / X-Refract-Reason / X-Refract-TimestampA render in relay modeStatus is queued or refused, with the reason and the time
X-Refract-Queue / X-Refract-PriorityA relay-mode render that was queuedThe queue and priority it was routed to
X-Refract-BatchId / X-Refract-DispatchReadiness / X-Refract-DecideUrlBatch-dispatch intake responseSee REFRACT Dispatch
X-Refract-TraceStageAn /api/accessibility answer served from a stored Section 508 tracesealed or original: which render the trace describes

The headers a failed render carries are listed under Error Headers.

Earlier Header Names

Before the platform was renamed, the headers above had another prefix with the same <Name>. The Dispatcher no longer sends the earlier names, and Access-Control-Expose-Headers no longer lists them. A reader built on an earlier name (an add-on, a flow built on the earlier Power Platform connector definition, or an older SPFx package) finds nothing under it: read the X-Refract- name.

Request Headers

The signing portal's signed calls to your Dispatcher carry X-Refract-ESign-* request headers. The Dispatcher reads them on POST /api/esign/callback, /api/esign/committed, /api/esign/consent/serve, /api/esign/consent/acknowledge, /api/esign/consent/slot-preview, /api/esign/email, and /api/esign/signer-event. You do not send them yourself, but a gateway or firewall in front of the Dispatcher must pass them through: a call without them is refused with 401.

HeaderValue
X-Refract-ESign-Signaturesha256=<hex>: an HMAC-SHA256 over the method, the path, the timestamp, the nonce, and the body
X-Refract-ESign-TimestampWhen the call was signed, an ISO 8601 time. Accepted within 5 minutes of the Dispatcher's clock.
X-Refract-ESign-NonceA value used once, 8 to 128 characters
X-Refract-ESign-TenantThe tenant ID, checked against the stored signing request
X-Refract-ESign-GroupThe signing group ID, on /api/esign/callback
X-Refract-ESign-SessionThe signing session ID, on /api/esign/callback

These headers were named X-VaultESign-<Name>, with the same <Name>. The signing portal sends each under both names, and the Dispatcher reads the X-Refract-ESign- name first, then the X-VaultESign- name.


Errors

Every error code the Dispatcher returns, the retry rule, and the error headers are on Error responses and codes. This section describes how a failed render answers.

A failed POST /api/render answers in one of two ways, and POST /api/gateway/v1/documents/generate answers the same way because it runs the same rendering logic:

  • A request problem answers 4xx with a JSON refusal body. The request named or carried something the Dispatcher cannot use, such as a template that is not there or a payload that is not JSON. Nothing is rendered or stored: no error report, no failed-render record, and no error audit event. Sending the same request again gets the same answer.
  • A fault of the service answers 500 with an error-report PDF. The Dispatcher could not produce the document for a reason of its own.

Request Problems

A request problem answers with the status below and this body:

{
  "error": "PayloadNotFound",
  "code": "PAYLOAD_NOT_FOUND",
  "message": "The payload file was not found in the payloads library.",
  "correlationId": "3f2c9a1e-7b4d-4c1a-9e2f-5d6b8a0c1e47"
}

code is the stable value to branch on. message is fixed wording for each code: it never contains the path, the templateId, a payload value, or the text of the underlying error. The Dispatcher's log keeps those, under the same correlation ID.

CodeStatuserrorMeaning
TEMPLATE_NOT_FOUND404TemplateNotFoundThe template is in neither the templates library nor the system templates library.
TEMPLATE_UNREADABLE422TemplateUnreadableThe template file is not JSON, or is JSON that is not an object.
TEMPLATE_LAYOUT_SCHEMA_MISSING422TemplateLayoutSchemaMissingThe stored template has no layoutSchema. Export it again from the Designer and upload it.
PAYLOAD_NOT_FOUND404PayloadNotFoundThe payload file is not in the payloads library.
PAYLOAD_UNREADABLE422PayloadUnreadableThe payload file is not JSON, or is JSON that is not an object or an array.
TEMPLATE_ID_UNKNOWN404TemplateIdUnknownNo installed template has the templateId in the request.
RENDER_TEMPLATE_ID_REQUIRED400TemplateIdRequiredA render by templateId left out the templateId.
RENDER_JSON_DATA_REQUIRED400JsonDataRequiredA render by templateId left out jsonData.
DIRECT_JSON_DATA_REQUIRED400JsonDataRequiredA direct render left out jsonData (the full payload, or a data record with a template).
DIRECT_LAYOUT_SCHEMA_MISSING400LayoutSchemaMissingA direct render's payload has no layoutSchema. Provide a Designer-exported layoutSchema.
DIRECT_DATA_MISSING400RenderDataMissingA direct render's payload has none of dataSource, sampleData, or data.
DIRECT_TEMPLATE_UNRESOLVED400TemplateUnresolvedA direct render's payload does not resolve to a template.
DIRECT_TEMPLATE_SHAPE_INVALID400TemplateShapeInvalidA direct render's template is a raw layoutSchema tree, not a resolved document definition. Put the layoutSchema in jsonData instead.
REDACTION_POLICY_NOT_FOUND422RedactionPolicyNotFoundThe redaction policy named in the template settings is not there.
REDACTION_POLICY_INVALID422RedactionPolicyInvalidThe redaction policy named in the template settings does not parse as a valid policy.

"Not there" means storage answered that the file does not exist. Anything else that stops a file from being read is a fault of the service, below.

The Dispatcher's other refusals, such as ROLE_MISSING (403) or PAYLOAD_PATH_REQUIRED (400, a templatePath with no payloadPath), use the same body.

Faults

A fault of the service still answers 500, with an error-report PDF as the body (Content-Type: application/pdf, Content-Disposition: attachment). Faults include storage that cannot be reached, a throttle that outlasts the Dispatcher's retries, an authentication failure between the Dispatcher and storage, a library with no drive or container configured, and any error the Dispatcher did not expect. The error report describes the failure; the response headers say why without opening the PDF.

Two request problems still answer this way: an image or structured-content asset the template names that cannot be fetched or decoded, and a redaction policy that parses but cannot be applied to the payload.

Two refusals after a successful render also answer with an error-report PDF. The document was rendered and stored, and only the stage after it was refused:

  • An eSign or Delivery session refused after the document rendered answers 500.
  • A workflow refused after the document rendered answers 422, and X-Refract-BlobPath names the stored document.

If the error report itself cannot be rendered, the 500 body is JSON instead, with "error": "RenderFailed" and "code": "RENDER_FAILED".

Error Headers

HeaderPresent onValue
X-Refract-CorrelationIdEvery response, including refusalsThe correlation ID for this request. A refusal's JSON body carries it too, as correlationId.
X-Refract-ErrorCodeEvery error-report responseRENDER_FAILED for an unexpected failure, or the code of the refusal the report names. A request problem has no error report; its code is in the JSON body.
X-Refract-ErrorMessageEvery error-report responseOne line of plain ASCII, at most 300 characters, in the Dispatcher's own wording. For RENDER_FAILED it is The document could not be generated. The error report has the details. It never carries the text of the underlying error, a path, or a payload value; the error report and the Dispatcher's log have those.
X-Refract-ErrorReportPathError-report responses whose report was storedWhere the stored error report is. Absent when the report was not stored; the PDF in the body is then the only copy.
X-Refract-LifecycleErrorCode / X-Refract-LifecycleErrorAn eSign, Delivery, or workflow refusal after the document renderedThe refusal's code and reason.
X-Refract-BlobPathThe workflow refusal 422; some error-report 500s for one release (below)On the 422, where the stored document is. On a 500, the error report's location, the same as X-Refract-ErrorReportPath.

X-Refract-BlobPath on error-report 500s

X-Refract-BlobPath means the document. Error-report 500 responses that set it before X-Refract-ErrorReportPath existed still set it, to the error report's location, so older add-ons keep working. It is removed from error-report 500 responses one release later. Read the error report's location from X-Refract-ErrorReportPath.

On every failed response (status 400 or more), Access-Control-Expose-Headers lists these headers and Content-Disposition, so browser code on another origin can read them.

Handling Errors

Branch on the content type to read the failure, and on the status to decide what to do next:

const res = await fetch(`${dispatcherUrl}/api/render`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify(request),
});

if (res.status === 200) return await res.arrayBuffer(); // the document

const correlationId = res.headers.get('X-Refract-CorrelationId');
const isJson = (res.headers.get('Content-Type') ?? '').includes('application/json');
const failure = isJson
  ? await res.json() // { error, code, message, correlationId }
  : {
      code: res.headers.get('X-Refract-ErrorCode'),
      message: res.headers.get('X-Refract-ErrorMessage'),
      reportPath: res.headers.get('X-Refract-ErrorReportPath'), // null when the report was not stored
      report: await res.arrayBuffer(), // the error-report PDF
    };

if (res.headers.get('X-Refract-LifecycleErrorCode')) {
  // The document was rendered and stored; only the eSign, Delivery, or workflow step after it was refused.
  // Fix what the code names. The document already exists, so a new render stores a second one.
} else if (res.status < 500) {
  // A request problem: resending the same request gets the same answer.
  // Fix the template, the payload, or the request body, using failure.code.
} else {
  // A fault of the service: keep correlationId and the error report for your administrator.
}

Log failure.code and the correlation ID in every case. Support finds the request in the Dispatcher's log by the correlation ID.


Next Steps

On this page