API REFERENCE
Beta
Read a Workflow
Returns the current state of one workflow. This is how you follow a workflow to completion, and it is the only poll handle on this API.
It is genuinely read-only: reading a workflow never advances it, never re-runs anything, and is safe to call as often as the rate throttle allows.
Get the URL to submit against from your Pitchpoint Account Representative.
Sample
url="https://api.pointservices.com/riskinsight-services-ws/resources/v1/workflows/wfpop_0000000000002080001"
curl -X GET "${url}" \
-H "Authorization: Bearer your_access_token_here" \
-H "Accept: application/json"
Header Properties
| Property | Value | Required? |
|---|---|---|
| Authorization | Bearer your token | true |
| Content-Type | application/json | false, but recommended |
| Accept | application/json | false |
| X-PPS-UserAlias | your value | false |
| X-PPS-UserAgent | your value | false |
| X-PPS-CorrelationID | your value | false |
This call sends no body, so Content-Type is not required. Sending it anyway costs nothing and is the safer habit: on the POST calls edge protection in deployed environments refuses a request that omits it, with a 403 carrying an HTML body rather than any response documented here.
Accept is not enforced — a request without it is answered normally with JSON — but send it anyway, so that a future change to content negotiation cannot alter what you receive.
X-PPS-CorrelationID is a support-tracing header and is not the same thing as the correlationId field in a request body. Only the body field is recorded against an order and searched by Find Workflows; setting the header globally in your HTTP client does not make your orders findable.
See the Common Headers guide for details on the optional X-PPS-* headers above.
Path Parameters
| Property | Description | Type |
|---|---|---|
| workflowId | (mandatory) The wfpop_-prefixed handle returned when the workflow was started. Accepted with or without the prefix. | string |
A runId addresses nothing. No endpoint on this API accepts one. If you paste a wfrunpop_ value here you are using the wrong identifier — the two are disjoint by prefix precisely so the mistake is catchable, and on a first run they share the same digits, so the prefix is the only reliable way to tell them apart. A runId exists solely to match a response to a webhook event’s data.runId.
This call reports the newest run in the workflow. After a reconfirm, the runId, timestamps, status and terms all describe the reconfirm rather than the original run.
Responses
Not every response carries the same body, so branch on the status code first, and then on what the body actually contains. Some responses carry the messages envelope described on this page; others carry a JSON problem document with an entirely different set of fields; others carry no JSON at all. A client that calls its JSON parser on every non-2xx body, or that reads messages[0].code without first checking that messages is present, will fail on the responses it is most likely to meet.
200
| Property | Description | Type |
|---|---|---|
| workflow | (present) The workflow’s current state. See section below. | object |
| terms | (optional) Terms describing the loan this run was started against. Never present on a 202. Also carried by each entry of a Find Workflows result. Do not assume it echoes everything you staged — see below. | array |
| messages | (present) Always an array, never null. Empty while processing. At a terminal status it carries the workflow’s recorded messages, one entry each — SUCCESS for complete, FAILED for failed. See below: absence of a code is not a status. | array |
| links | (present) self and reconfirm. | object |
workflow
| Property | Description | Type |
|---|---|---|
| workflowId | (present) Stable for the life of the workflow, including across reconfirms. | string |
| runId | (present) The newest run. Changes on every reconfirm. Addresses nothing. | string |
| status | (present) Lowercase and closed: processing, complete, failed, cancelled. See below. | string |
| sessionId | (optional) Present when the workflow was started against a session. Your way back to the data. | string |
| correlationId | (optional) The value supplied on the run that is current. Not inherited across a reconfirm. | string |
| product | (present) The resolved product selector for the current run. | object |
| productType | (present) The product family, matching product.service. | string |
| productName | (present) The model, matching product.model. After a reconfirm this reflects the follow-up product, not the one you originally started. | string |
| reconfirmAvailable | (optional) The gate to check before reconfirming. Only workflowId, runId and status are guaranteed on every workflow block, so compare this against true rather than assuming it is there. See Reconfirm a Workflow. | boolean |
| serviceable | (optional) Whether the workflow is billable on balance: its billing entries are netted, charges against reversals, and this is true only while a charge remains. It is therefore false on a workflow that has not been charged yet, and false again once charges are fully reversed — so false is not evidence that a workflow was free. Absent from both 202 acknowledgments. | boolean |
| referenceNumber | (optional) A reference number already carried by the loan this workflow was started against, echoed back. It arrives with the loan from whatever created it — a loan import, or a workflow started through an older non-public path — never from anything you send here. Absent when the loan carries none, which is the normal case for a loan staged through this API, and absent from both 202 acknowledgments. Do not treat it as a round-trip of your correlationId. | string |
| ordered | (optional) When the workflow was started. | string |
| fulfilled | (optional) When results became available. | string |
| completed | (optional) When the workflow reached a terminal state. | string |
Every optional field above is omitted rather than null when the stage that would set it has not happened, so a field’s absence is meaningful: a workflow with no completed has not finished.
terms carries the parties, not the whole loan. In practice this echo returns the person, property and participant terms and omits the loan-level ones — stage loanTerms and loanToValue successfully and neither appears here. Individual fields may be absent from the terms it does return. Use it to see which parties a run was started against, never as a record of what you sent. Keep your own copy of every snapshot you stage; that is the only dependable account of what you sent.
ordered, fulfilled and completed are variable-width timestamps. All are RFC 3339 UTC with a trailing Z, but fractional seconds are emitted only when they are not zero — so 2026-08-07T15:01:42Z and 2026-08-07T15:01:42.412Z both occur on the same field. Parse them with a date library; a fixed-offset read or a slice will work until the first timestamp that lands on a whole second.
status
| Value | Meaning |
|---|---|
processing | The only non-terminal value. Covers every state before the workflow is done. |
complete | Terminal. Never reported early — it means the result is final. Projects a SUCCESS message when one was recorded. |
failed | Terminal. The run did not produce a result. Projects a FAILED message when one was recorded; read its text for the reason. |
cancelled | Terminal. Reserved for a future cancellation source; no path emits it today. Carries no message code — neither SUCCESS nor FAILED. |
Values are lowercase. Compare them as written — Complete is not complete.
Handle failed explicitly. A workflow that reaches failed is finished and will not progress on its own; polling it further tells you nothing new. Where a reason was recorded it is the text of the FAILED message — surface it rather than discarding it, because it is the only account of what went wrong. A failed run may still be reconfirmable: check reconfirmAvailable rather than assuming it is not.
Branch on status, never on the presence or absence of a message code. The SUCCESS and FAILED codes are projections of what the workflow actually recorded, so a terminal workflow that recorded nothing carries an empty messages array — complete, but with no SUCCESS in sight. cancelled carries neither code by design. A client that treats “no FAILED” as success, or waits for SUCCESS before acting, will misread both cases. status is the answer; messages is the explanation.
The text on either code is whatever was recorded, not a fixed string. Do not compare it, and do not rely on a particular wording.
Following a workflow to completion
Poll this endpoint until status is one of the three terminal values. A schedule that stays clear of the rate throttle is two seconds for the first two checks, then five, then ten seconds thereafter, with an overall deadline after which you stop and investigate rather than polling forever.
A terminal state is the latest outcome, not necessarily a final one: a reconfirm moves the same workflowId back to processing under a new runId.
Polling is not the only option. These workflows publish workflow.updated and workflow.failed; registering an endpoint for both avoids polling and the rate throttle entirely — see the Webhooks guide. A delivered event reports that a run changed rather than what it now holds, so read the workflow afterwards. The runId here exists precisely so you can match a delivery back to the run it describes.
Example, a completed workflow:
{
"workflow": {
"workflowId": "wfpop_0000000000002080001",
"runId": "wfrunpop_0000000000002080001",
"sessionId": "0000000000013500001",
"correlationId": "alpha_numeric_string_to_help_identify_this_workflow",
"product": {
"service": "ADV",
"model": "FirstOrder"
},
"productType": "ADV",
"productName": "FirstOrder",
"status": "complete",
"reconfirmAvailable": true,
"serviceable": true,
"ordered": "2026-08-07T15:01:42Z",
"fulfilled": "2026-08-07T15:03:11Z",
"completed": "2026-08-07T15:03:11Z"
},
"terms": [
{
"person": {
"firstName": "Jane",
"lastName": "Sample"
}
}
],
"messages": [
{
"code": "SUCCESS",
"text": "Success",
"timestamp": "2026-08-07T15:03:11Z"
}
],
"links": {
"self": {
"href": "https://api.pointservices.com/riskinsight-services-ws/resources/v1/workflows/wfpop_0000000000002080001"
},
"reconfirm": {
"href": "https://api.pointservices.com/riskinsight-services-ws/resources/v1/workflows/wfpop_0000000000002080001",
"method": "POST"
}
}
}
401
No credential the platform could accept reached the service.
The body is a JSON problem document, not the messages envelope, and the response carries a WWW-Authenticate: Bearer realm="Helix" challenge. Read the machine-readable part from type; title and detail are human copy and are not contractual.
| Property | Description | Type |
|---|---|---|
| type | (present) An absolute URI identifying the problem. about:blank when no credential was sent at all. | string |
| title | (present) Short human-readable summary. | string |
| status | (present) Repeats the HTTP status code. | number |
| detail | (present) Human-readable explanation. Never machine-parsed. | string |
| instance | (present) An identifier for this one request. Quote it when contacting support. | string |
Examples:
{
"type": "about:blank",
"title": "Unauthorized",
"status": 401,
"detail": "Authentication is required. Present a bearer access token in the Authorization header.",
"instance": "urn:pps:request:0000000000000000000000000000000000000000000000000000"
}
{
"type": "https://pointservices.com/problems/invalid-token",
"title": "The access token was not accepted",
"status": 401,
"detail": "The access token was rejected.",
"instance": "urn:pps:request:0000000000000000000000000000000000000000000000000000"
}
The second form carries WWW-Authenticate: Bearer realm="Helix", error="invalid_token", and is what a token the service will not accept returns.
Treat a 401 and a 403 alike: both mean your credential did not work, and neither distinguishes reliably enough to build separate recovery paths on. Obtain a fresh access token and retry the request once. If it fails again, stop and surface the error rather than looping — repeated attempts with a credential the service has already refused will meet the rate throttle described below.
403
Your credential was understood but you are not permitted to read this workflow.
The body does not carry the messages envelope, so there is no code to branch on. Branch on the status alone, and treat the body as opaque: do not assume it is empty, and do not assume it is JSON.
A 403 is not always a permissions problem — try a fresh token first. A credential the service will not honour can be refused with either status, so a sudden wall of 403s on calls that worked a moment ago is more often an expired token than a change to your entitlements. Obtain a new access token and retry once. If it fails again, then treat it as entitlement and talk to your Account Representative.
A 403 is also produced at the edge, before this service is reached, when a request omits Content-Type or carries a value resembling SQL injection or cross-site scripting. Those refusals carry an HTML body and this service never sees them. A value that legitimately contains such text should be encoded — Base64, for instance — rather than sent raw.
404
No workflow exists with this identifier, or the path you called is not served. If you are polling a handle a 202 just gave you, check that you are using the wfpop_ workflowId and not the wfrunpop_ runId.
A 404 arrives in two different body shapes, decided by whether the path is served at all.
A path this service serves, naming something that does not exist carries a JSON problem document with Content-Type: application/problem+json. Read the machine-readable part from type.
| Property | Description | Type |
|---|---|---|
| type | (present) An absolute URI identifying the problem, ending /problems/not-found here. | string |
| title | (present) Short human-readable summary. | string |
| status | (present) Repeats the HTTP status code. | number |
| detail | (present) Human-readable explanation naming what was not found. Never machine-parsed. | string |
| instance | (present) An identifier for this one request. Quote it when contacting support. | string |
Example:
{
"type": "https://pointservices.com/problems/not-found",
"title": "No such resource",
"status": 404,
"detail": "Workflow[wfpop_0000000000002080001] not found",
"instance": "urn:pps:request:0000000000000000000000000000000000000000000000000000"
}
An identifier this API has already issued to you is readable the moment you receive it, so a 404 on one is final rather than a race. Retrying will not make it appear.
A path that is not served at all — most often a URL missing the riskinsight-services-ws context root — is refused by infrastructure in front of the application, which never sees the request. That response carries no body this API defines, so branch on the status alone and do not attempt to parse what comes back. Its exact shape depends on the environment you are calling.
If a 404 has no body, check the context root before anything else.
429
Refused by a rate throttle at the edge, before this service is reached. The throttle counts requests per source address across every endpoint on both the session and workflow surfaces.
The refusal carries no body, no messages array and no Retry-After header, and this service never sees it, so it will not appear in any support trace.
Back off and resume. When following a workflow to completion, an interval that stays clear of the throttle is two seconds for the first two checks, then five, then ten seconds thereafter, with an overall deadline after which you stop and investigate.
500
An unhandled failure inside the service. The body does not carry the messages envelope, so branch on the status alone.
A 500 means the outcome is unknown, not that nothing happened. A failure raised after a write has already committed leaves that write in place. This call changes nothing, so it is always safe to read again. Retrying blind is how one order becomes two, and nothing on this surface removes a duplicate for you.