# Read a Workflow

Source: https://docs.pitchpointsolutions.com/api/workflow/get_workflow/

**Beta**

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.

> **Before Starting:** Get the URL to submit against from your Pitchpoint Account Representative.

### Sample

```bash
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 &mdash; a request without it is answered normally with JSON &mdash; 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](https://docs.pitchpointsolutions.com/api/workflow/list_workflows/); setting the header globally in your HTTP client does not make your orders findable.

See the [Common Headers guide](https://docs.pitchpointsolutions.com/developer_guides/headers/index/) 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 |

> **Warning:** **A `runId` addresses nothing.** No endpoint on this API accepts one. If you paste a `wfrunpop_` value here you are using the wrong identifier &mdash; the two are disjoint by prefix precisely so the mistake is catchable, and on a first run they share the same suffix, 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

> **Warning:** **Not every response carries the same body. Branch on the `Content-Type` first, and then on what the body contains.** `application/json` is the `messages` envelope described on this page: read `messages[].code`. `application/problem+json` is a problem document, with `type`, `title`, `status`, `detail` and `instance` and no `messages` key: read `type`, and compare it as the whole URI (for example `https://pointservices.com/problems/concurrent-modification`). Some rejections 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. See [Error handling](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/error_handling/) for the whole rule.

**200**

| Property | Description                                                                                                       | Type   |
|----------|-----------------------------------------------------------------------------------------------------------------------|--------|
| workflow | (present) The workflow's current state. See section below.                                                         | object |
| terms    | (optional) The terms this run was started against. Never present on a 202. Also carried by each entry of a [Find Workflows](https://docs.pitchpointsolutions.com/api/workflow/list_workflows/) result. **Do not assume it echoes everything you staged** &mdash; see below. | array  |
| referenceNumber | (optional) The reference number recorded on the snapshot these `terms` came from; the same value as `workflow.referenceNumber`. Omitted when that snapshot carries none. | string |
| messages | (present) Always an array, never null. Empty while `processing`. At a terminal status it carries the messages the workflow **recorded** about its outcome, one entry each &mdash; `SUCCESS` for `complete`, `FAILED` for `failed`. See below: absence of a code is not a status. | array  |

##### 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          | (present) The session the workflow runs against. Every workflow has one: [Start a Workflow](https://docs.pitchpointsolutions.com/api/workflow/start_workflow/) creates one for you. Your way back to the data: [Read a Session](https://docs.pitchpointsolutions.com/api/session/get_session/) reports it, and [Read the Staged Data](https://docs.pitchpointsolutions.com/api/session/read_session_sami/) returns its terms. | string  |
| correlationId      | (optional) The value supplied on the run that is current. Not inherited across a reconfirm. Not the same as the `correlationId` on a term; see [Correlation ids on terms](https://docs.pitchpointsolutions.com/api/sami_terms/conventions/#correlation-ids-on-terms). | string  |
| product            | (optional) The resolved product selector for the current run.                                                                                                                | object  |
| productType        | (optional) The product family, matching `product.service`.                                                                                                                  | string  |
| productName        | (optional) 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](https://docs.pitchpointsolutions.com/api/workflow/reconfirm_workflow/). | boolean |
| serviceable        | (optional) Whether the workflow is serviceable, which is to say billable. **Once an outcome has been [reported](https://docs.pitchpointsolutions.com/api/workflow/edit_workflow/)**, this is `true` when any entry of `outcomes` is serviceable and `false` when none is, whatever order they were reported in. **Before the first report**, the workflow's 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 &mdash; so `false` is not evidence that a workflow was free. Absent from both 202 acknowledgments. | boolean |
| outcomes           | (optional) Every outcome reported about this workflow with [Edit a Workflow](https://docs.pitchpointsolutions.com/api/workflow/edit_workflow/), newest first. Absent until the first report, and from both 202 acknowledgments. Reports belong to the whole workflow, so a reconfirm carries the same list. See [outcomes](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/#outcomes) below. | array   |
| referenceNumber    | (optional) The reference number recorded on the snapshot this run was ordered against: the `referenceNumber` sent with the [stage](https://docs.pitchpointsolutions.com/api/session/stage_session_sami/), [start](https://docs.pitchpointsolutions.com/api/workflow/start_workflow/) or [reconfirm](https://docs.pitchpointsolutions.com/api/workflow/reconfirm_workflow/) that recorded those terms. It is **not your `correlationId`**. It belongs to this run, so a later stage never changes what an existing workflow reports. Absent when that snapshot carries none. | string  |
| ordered            | (optional) When the workflow was started.                                                                                                                                      | string  |
| fulfilled          | (optional) When results became available.                                                                                                                                    | string  |
| completed          | (optional) When PitchPoint recorded the workflow as completed on its side. This does not follow `status`: a `complete` workflow often has no `completed` yet, and `fulfilled` is when its results became available. Read `status` rather than this field. | string  |

Every optional field above is **omitted rather than null** when the stage that would set it has not happened or the value never resolved. Only `workflowId`, `runId` and `status` are guaranteed on every workflow block.

> **Warning:** **`terms` is not a faithful copy of what you staged.** The party terms (`person`, `property`, `participant` and `company`) come back as the product targeted them, which for most products is narrower than what the session holds. The loan-level terms (`loanTerms`, `loanToValue`, `transactionDetail`, `loanDetail`, `refinance`, `housingExpense` and `closing`) describe the loan rather than a party, so each comes back whenever the loan carries it. A term carrying no values is never echoed. Individual fields may be absent from the terms that do come back, and values may come back normalized rather than as you sent them. Use the echo 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.

Each party term carries a `correlationId`: yours when you sent one, otherwise one the service generated. Match an echoed party term to the one you sent on that value; see [Correlation ids on terms](https://docs.pitchpointsolutions.com/api/sami_terms/conventions/#correlation-ids-on-terms).

> **Warning:** **`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 &mdash; 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's results are ready.                  |
| `complete`   | Terminal. Your results are ready to fetch; see [Following a workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/#following-a-workflow). 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. PPS does not set this status; treat it as terminal if you see it. Carries **no** message code &mdash; neither `SUCCESS` nor `FAILED`. |

Values are lowercase. Compare them as written &mdash; `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 &mdash; 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.

> **Warning:** **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** &mdash; 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.

##### outcomes

Present once an approved integration system has reported what happened to the application behind the workflow; see [Edit a Workflow](https://docs.pitchpointsolutions.com/api/workflow/edit_workflow/). One entry per report:

| Property    | Description | Type    |
|-------------|-------------|---------|
| reason      | (present) The [outcome code](https://docs.pitchpointsolutions.com/api/workflow/edit_workflow/#outcome-codes) that was reported. | string  |
| serviceable | (present) Whether the product counted this outcome as serviceable when it was reported: `true` when the product's serviceable list named it, `false` when its unserviceable list did. Decided when the report was recorded, so a later change to the product's lists never changes it. | boolean |
| reportedAt  | (optional) When this outcome was recorded. It never changes. | string  |

Every report is kept, and no report replaces another. The list is newest first, but **the order is for display only**: outcomes can be reported in any order, so no entry is the workflow's current outcome. Read `workflow.serviceable` for the answer: it is `true` when any entry is serviceable, so one entry's `serviceable` can be `false` while the workflow's is `true`.

Outcomes never change what this API allows. In particular they don't affect `reconfirmAvailable`.

The list of outcome codes is closed but can grow. Treat a `reason` you don't recognize as an unrecognized outcome rather than failing to read the workflow.

##### Following a workflow

**`complete` means your results are ready.** A read that returns `complete` and a `workflow.updated` delivery tell you the same thing, so act on whichever reaches you first: read the findings.

If you poll this endpoint instead, stop at any terminal status. 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](https://docs.pitchpointsolutions.com/api/workflow/reconfirm_workflow/) moves the same `workflowId` back to `processing` under a new `runId`. Occasionally PitchPoint reopens a run for more work under the same `runId`; it reads `processing` again until it is ready, and you receive a fresh `workflow.updated` when it is.

These workflows publish `workflow.updated` and `workflow.failed`; registering an endpoint for both avoids polling and the rate throttle entirely &mdash; see the [Webhooks guide](https://docs.pitchpointsolutions.com/developer_guides/webhooks/index/). 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:
```json
{
  "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"
    }
  ]
}
```

**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:
```json
{
  "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.

The `invalid-token` type deliberately does not say why the token was refused: the platform cannot tell an expired token from a revoked, badly signed or malformed one. Treat every `invalid-token` the same way.

> **Warning:** **Refresh your token on a 401, and only on a 401.** An expiring token moves a call from 200 to 401, never to 403. Obtain a fresh access token and retry the request once. If it fails again, stop and surface the error rather than looping &mdash; 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.

A 403 is an **authorization decision**: your identity was established and the answer is still no. It is never a statement about your token, so refreshing the token will not turn it into a 200. A refused request leaves nothing behind, so there is nothing to clean up.

The body is a problem document (`application/problem+json`), not the `messages` envelope, so there is no `code` to branch on. Its `type` is one of two values:

| `type` | Meaning |
|--------|---------|
| `about:blank` | The request was denied before the operation itself ran. |
| `https://pointservices.com/problems/not-authorized` | A policy decision about the specific resource or product you named. |

Example:
```json
{
  "type": "https://pointservices.com/problems/not-authorized",
  "title": "Not authorized",
  "status": 403,
  "detail": "The authenticated identity is not permitted to perform this action.",
  "instance": "urn:pps:request:0000000000000000000000000000000000000000000000000000"
}
```

> **Warning:** **A 403 can also come from the edge, before this service is reached,** when a request omits `Content-Type` or carries a value resembling SQL injection or cross-site scripting. That refusal carries an HTML body, not a problem document, and this service never sees it. Check the `Content-Type` of the response before you parse it. A value that legitimately contains such text should be encoded &mdash; Base64, for instance &mdash; 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.

When this API answers, the body is a problem document (`application/problem+json`) in both cases, and its `type` tells them apart.

**`https://pointservices.com/problems/not-found`** means the identifier you sent did not resolve.

| Property | Description                                                                              | Type   |
|----------|--------------------------------------------------------------------------------------------|--------|
| type     | (present) `https://pointservices.com/problems/not-found` here. Compare it as the whole URI. | 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:
```json
{
  "type": "https://pointservices.com/problems/not-found",
  "title": "No such resource",
  "status": 404,
  "detail": "Workflow[wfpop_0000000000002080001] not found",
  "instance": "urn:pps:request:0000000000000000000000000000000000000000000000000000"
}
```

Treat this as a single outcome: nothing in the body narrows down why the identifier did not resolve. Check the identifier you sent, and the tenancy of the credential you sent it with. 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.

**`about:blank`** means the path itself is not served here, so the request never reached this operation: a wrong host, or an environment where this API is not deployed.

A URL missing the `/riskinsight-services-ws/resources/v1` path prefix does not reach this API at all. The 404 then comes from infrastructure in front of it, and carries no body this API defines, so don't try to parse it. If a 404 has no problem document, check the path prefix 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. Because the edge sends this response before the request reaches the service, it is not described in the OpenAPI specifications or the API Explorer.

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 is a problem document (`application/problem+json`) with `type` `about:blank`, not the `messages` envelope, so branch on the status alone. When you report the error to PPS, quote the document's `instance`: it carries the same id as the `x-pps-request-id` response header, and it is what lets PPS find this exact request.

**Retrying this call is safe.** This call changes nothing, so it is always safe to read again.
