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:
| Configuration | Purpose |
|---|---|
Application ID URI (api://<appId>) | Lets Microsoft Entra ID resolve api://<appId> as a token audience |
Render.Access app role | Application 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 role | Assignable 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 scope | Delegated 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-consented | infra/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 in | A human, interactively | Nobody - service-to-service |
| Client app | REFRACT 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 needed | Not for REFRACT Lifecycle | Yes, the connector app's own |
| Typical use | SPFx dashboard, Postman / manual API testing | Power Platform connector, ERP integrations (Business Central, Dynamics), scripts, scheduled jobs |
| Where it's documented | Authentication, Testing with Postman | Connect 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
| Endpoint | Auth model | Rate limit | Notes |
|---|---|---|---|
POST /api/render | Either | None | The 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=true | Either | None | Health check. Returns a status PDF; 200 when healthy, 503 when not. See Status Route below. |
POST /api/gateway/v1/documents/generate | Either | 100 requests / 60s | Gateway 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/open | Either | 100 requests / 60s | Streams the document bytes directly - the response body is the file, not a URL. Requires the Lifecycle.Reader role. |
GET /api/gateway/v1/workflows/{workflowId} | Either | 100 requests / 60s | Poll workflow/signature status. Requires the Lifecycle.Reader role. |
GET /api/gateway/v1/workflows/{workflowId}/pdf | Either | 100 requests / 60s | Streams 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/* | Either | None | REFRACT 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:
| Claim | Validates |
|---|---|
aud | Token was issued for this Dispatcher's App Registration (accepted as either the bare client ID or the api://<client-id> form) |
iss | Token was issued by this tenant's own Microsoft Entra ID |
tid | Token belongs to the authorised tenant |
exp | Token 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:
| Header | Present when | Value |
|---|---|---|
X-Refract-CorrelationId | Render responses, refusals included | The correlation ID for this render. Empty when the request failed before one was assigned. |
X-Refract-UploadStatus | The document was stored, or storing it was skipped | ok when stored, skipped when not |
X-Refract-UploadError | Storing was skipped because no storage is configured or generatedDocuments is not enabled | The reason |
X-Refract-WebUrl / X-Refract-DriveItemId | The document was stored in SharePoint | SharePoint item URL and DriveItem ID |
X-Refract-BlobPath | The document was stored | Where 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-AuditStatus | The audit write was skipped or failed | skipped:<reason> or failed:<reason> |
X-Refract-SkippedFeatures | A requested feature was skipped for the current license | Comma-separated feature list |
X-Refract-WorkflowId / X-Refract-WorkflowPortalUrl | The render started a workflow | The workflow ID, and the participant's portal URL (/w/<linkReference>) |
X-Refract-IntakeReceiptBlobPath | A render that started a workflow or was queued in relay mode, when its intake receipt was stored | The intake receipt's location |
X-Refract-Status / X-Refract-Reason / X-Refract-Timestamp | A render in relay mode | Status is queued or refused, with the reason and the time |
X-Refract-Queue / X-Refract-Priority | A relay-mode render that was queued | The queue and priority it was routed to |
X-Refract-BatchId / X-Refract-DispatchReadiness / X-Refract-DecideUrl | Batch-dispatch intake response | See REFRACT Dispatch |
X-Refract-TraceStage | An /api/accessibility answer served from a stored Section 508 trace | sealed 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.
| Header | Value |
|---|---|
X-Refract-ESign-Signature | sha256=<hex>: an HMAC-SHA256 over the method, the path, the timestamp, the nonce, and the body |
X-Refract-ESign-Timestamp | When the call was signed, an ISO 8601 time. Accepted within 5 minutes of the Dispatcher's clock. |
X-Refract-ESign-Nonce | A value used once, 8 to 128 characters |
X-Refract-ESign-Tenant | The tenant ID, checked against the stored signing request |
X-Refract-ESign-Group | The signing group ID, on /api/esign/callback |
X-Refract-ESign-Session | The 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
4xxwith 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
500with 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.
| Code | Status | error | Meaning |
|---|---|---|---|
TEMPLATE_NOT_FOUND | 404 | TemplateNotFound | The template is in neither the templates library nor the system templates library. |
TEMPLATE_UNREADABLE | 422 | TemplateUnreadable | The template file is not JSON, or is JSON that is not an object. |
TEMPLATE_LAYOUT_SCHEMA_MISSING | 422 | TemplateLayoutSchemaMissing | The stored template has no layoutSchema. Export it again from the Designer and upload it. |
PAYLOAD_NOT_FOUND | 404 | PayloadNotFound | The payload file is not in the payloads library. |
PAYLOAD_UNREADABLE | 422 | PayloadUnreadable | The payload file is not JSON, or is JSON that is not an object or an array. |
TEMPLATE_ID_UNKNOWN | 404 | TemplateIdUnknown | No installed template has the templateId in the request. |
RENDER_TEMPLATE_ID_REQUIRED | 400 | TemplateIdRequired | A render by templateId left out the templateId. |
RENDER_JSON_DATA_REQUIRED | 400 | JsonDataRequired | A render by templateId left out jsonData. |
DIRECT_JSON_DATA_REQUIRED | 400 | JsonDataRequired | A direct render left out jsonData (the full payload, or a data record with a template). |
DIRECT_LAYOUT_SCHEMA_MISSING | 400 | LayoutSchemaMissing | A direct render's payload has no layoutSchema. Provide a Designer-exported layoutSchema. |
DIRECT_DATA_MISSING | 400 | RenderDataMissing | A direct render's payload has none of dataSource, sampleData, or data. |
DIRECT_TEMPLATE_UNRESOLVED | 400 | TemplateUnresolved | A direct render's payload does not resolve to a template. |
DIRECT_TEMPLATE_SHAPE_INVALID | 400 | TemplateShapeInvalid | A direct render's template is a raw layoutSchema tree, not a resolved document definition. Put the layoutSchema in jsonData instead. |
REDACTION_POLICY_NOT_FOUND | 422 | RedactionPolicyNotFound | The redaction policy named in the template settings is not there. |
REDACTION_POLICY_INVALID | 422 | RedactionPolicyInvalid | The 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, andX-Refract-BlobPathnames 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
| Header | Present on | Value |
|---|---|---|
X-Refract-CorrelationId | Every response, including refusals | The correlation ID for this request. A refusal's JSON body carries it too, as correlationId. |
X-Refract-ErrorCode | Every error-report response | RENDER_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-ErrorMessage | Every error-report response | One 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-ErrorReportPath | Error-report responses whose report was stored | Where 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-LifecycleError | An eSign, Delivery, or workflow refusal after the document rendered | The refusal's code and reason. |
X-Refract-BlobPath | The 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
- Authentication - The two auth models in detail, and who may call
- Connect a source system - Connector apps for Business Central, Dynamics 365 Sales, Dynamics GP, Power Platform, and scripts
- Error responses and codes - Every error code, the error headers, and when to retry
- Testing with Postman - Step-by-step OAuth 2.0 configuration for manual API testing
- Power Platform Connector Setup - Client-credentials setup for Power Automate and Power Apps
- REFRACT Dispatch API Reference - Batch dispatch lifecycle endpoints
REFRACT Export
RefractExport extends the REFRACT PLATFORM document-processing model to governed enterprise data exports - scheduled, asynchronous CSV packages with the same audit, storage, and lifecycle model used across REFRACT.
Authentication
How callers get tokens for the Dispatcher - connector apps with client credentials, and people through REFRACT Lifecycle or a testing app - and which callers the Dispatcher accepts.