# Edit a Workflow

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

**Beta**

Report what happened to the application behind a workflow, as your system of record knows it: for example, that a loan funded or that an applicant withdrew. Send a `POST` to the workflow's `/edit` address. It is a partial update, and today its one field is `outcome`.

The outcomes you report with this call make **outcome-based billing** possible. A product set up for it bills a workflow according to the outcome of the application it supports, rather than for every workflow started. A mortgage lender, for example, can be billed only for loans that fund, and not for applications that are withdrawn or denied. You report only the outcome. Whether it counts as **serviceable**, which is to say billable, is decided by the product's configuration, never by the caller.

> **Before Starting:** Ask your Pitchpoint Account Representative to set up your product to take outcome reports, to tell you which outcomes it accepts and which of them count as serviceable, and to grant your integration's credential the permission to report.

### Sample

```bash
url="https://api.pointservices.com/riskinsight-services-ws/resources/v1/workflows/wfpop_0000000000002080001/edit"
curl -X POST "${url}" \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "outcome": "LOAN_FUNDED"
  }'
```

#### Header Properties

| Property                  | Value                  | Required? |
|---------------------------|------------------------|-----------|
| Authorization             | Bearer *your token*    | true      |
| Content-Type              | application/json       | true      |
| Accept                    | application/json       | false     |
| X-PPS-UserAlias           | *your value*           | false     |
| X-PPS-UserAgent           | *your value*           | false     |
| X-PPS-CorrelationID       | *your value*           | false     |

Send `Content-Type: application/json` on **every** POST, including one with no body at all. In deployed environments a request that arrives without it is refused by edge protection in front of the application, with a `403` carrying an HTML body rather than any response documented here &mdash; and because the service never sees the request, nothing can explain it to you. A local or unprotected deployment may accept the same request, so do not take a working call there as evidence the header is optional.

`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.

This call takes no `Idempotency-Key`, and ignores one if you send it. It needs none: sending the same outcome again only records it again, so a call that timed out can simply be sent again.

#### Path Parameters

| Property   | Description                                                                                              | Type   |
|------------|--------------------------------------------------------------------------------------------------------------|--------|
| workflowId | (mandatory) The `wfpop_` handle returned when the workflow was started. Accepted with or without the prefix. A `wfrunpop_` runId is refused with a 400: an outcome is about the whole workflow, never one run of it. | string |

#### Request Data Properties

Every field is optional, and a field you leave out is left unchanged. A JSON `null` is treated the same as leaving the field out. A body with none of these fields, including no body at all and `{}`, is a 400, because there would be nothing to edit. A field this API doesn't list is ignored rather than refused.

| Property | Description | Type | Required |
|----------|-------------|------|----------|
| outcome | What happened to the application, as one of the [outcome codes](https://docs.pitchpointsolutions.com/api/workflow/edit_workflow/#outcome-codes) below. Matched exactly, so `loan_funded` is not `LOAN_FUNDED`. It must be one your product accepts. | string | no |

#### Who may call it

Reporting an outcome needs its own permission, which PPS grants to a credential on request. It is separate from the permission to start or reconfirm workflows, so the systems that order workflows can't report outcomes unless they are granted it as well. The credential must also be allowed to read the workflow. If either is missing, the call is refused with a 403 and nothing is recorded.

#### What your product accepts

Your Pitchpoint Account Representative configures a product that takes outcome reports with two lists, to suit your business process: the outcomes that count as serviceable, and the outcomes that don't. The product accepts only an outcome on one of the two lists. Any other outcome is refused with a 400 whose text names the outcomes the product accepts, and nothing is recorded. A workflow whose product doesn't take outcome reports can't take one at all: the call is a 400.

Each report is judged against the product's lists **as they stand when you report it**, and that answer is kept with the report. A later change to the lists applies from the next report on, and never changes an outcome already reported.

#### How the reports add up

- **Every report is kept, and order doesn't matter.** No report replaces another, so none of them is the workflow's current outcome. Your system can send outcomes in any order.
- **The workflow is serviceable once any reported outcome is.** From the first report on, `workflow.serviceable` is `true` when any entry of `workflow.outcomes` is serviceable, and `false` when none is. A later unserviceable report doesn't undo an earlier serviceable one.
- **Report every workflow.** While your product takes outcome reports, PPS waits for the first report on each of its workflows before it settles that workflow's billable status. A workflow nobody reports on stays unsettled.
- **Report when the outcome changes, not on every sync.** Each call records a new report and a new entry in the workflow's history, so sending the same outcome on every synchronization only adds history.
- **Outcomes don't change what this API allows.** A reconfirm, and every other call, is accepted or refused exactly as it would be had no outcome been reported. `reconfirmAvailable` is still the only gate for a reconfirm.

#### Outcome codes

The codes are industry-agnostic: report the one that fits your application, from whichever group describes it. The list is closed but can grow. If you meet a code you don't recognize in `workflow.outcomes`, treat it as an unrecognized outcome rather than failing.

**Application outcomes**, for an application of any kind: a loan, a rental or a job. For a mortgage, the first four are also HMDA action-taken outcomes (Regulation C, Appendix A).

| Code | Meaning | HMDA code |
|------|---------|-----------|
| `APPLICATION_APPROVED_BUT_NOT_ACCEPTED` | Approved, but the applicant never accepted the approval within the specified time. A job offer the candidate declines is this one. | 2 |
| `APPLICATION_DENIED` | The application was not approved. A candidate who is turned down is this one. | 3 |
| `APPLICATION_WITHDRAWN` | The applicant expressly withdrew the application before it was denied or the file was closed for incompleteness. | 4 |
| `FILE_CLOSED_FOR_INCOMPLETENESS` | A written notice of incompleteness went unanswered within the period it specified, so the file was closed. | 5 |
| `APPLICATION_CANCELLED` | Cancelled by you or your system, without the applicant withdrawing. | |
| `DUPLICATE_APPLICATION` | Duplicates another application, which carries the outcome. | |
| `TEST_APPLICATION` | A test or training file, not a real application. | |

**Mortgage outcomes.** The names follow the MISMO 3.4 HMDA disposition vocabulary.

| Code | Meaning | HMDA code |
|------|---------|-----------|
| `LOAN_ORIGINATED` | A covered loan was originated, including one resulting from a request for preapproval. | 1 |
| `LOAN_PURCHASED_BY_YOUR_INSTITUTION` | Your institution purchased the loan. | 6 |
| `PREAPPROVAL_REQUEST_DENIED` | A preapproval request was not approved. | 7 |
| `PREAPPROVAL_REQUEST_APPROVED_BUT_NOT_ACCEPTED` | A preapproval was approved but never accepted within the specified time. | 8 |
| `LOAN_FUNDED` | The proceeds were disbursed. HMDA has no code for it: it counts an origination at closing, which can come before funding. | |
| `LOAN_RESCINDED` | The borrower rescinded the loan after closing. HMDA reports this as code 2 and has no code of its own for it. | |

**Tenant-screening outcomes**, for a rental application.

| Code | Meaning |
|------|---------|
| `LEASE_SIGNED` | The application became a signed lease. |
| `APPLICANT_MOVED_IN` | The tenant took occupancy, for a business that bills at move-in rather than at signing. |
| `UNIT_NO_LONGER_AVAILABLE` | The unit was leased to another applicant, or taken off the market, before this application concluded. |
| `APPROVED_WITH_CONDITIONS` | Approved subject to a condition such as an extra deposit, a co-signer or prepaid rent. An interim decision. |

**Employment-screening outcomes**, for a pre-employment screening. The application outcomes above apply too.

| Code | Meaning |
|------|---------|
| `CANDIDATE_HIRED` | The candidate was hired. |
| `CANDIDATE_STARTED` | The new hire started work, for a business that bills at the start date rather than at hire. |
| `OFFER_RESCINDED` | The employer withdrew a (conditional) offer, typically after the pre-adverse and adverse action process on the screening report. |
| `POSITION_NO_LONGER_AVAILABLE` | The position was filled by another candidate, or its requisition was closed or cancelled, before this candidate's process concluded. |

### 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**

The outcome was recorded. The body is the workflow's current state, exactly as [Read a Workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/) would return it, so `workflow.outcomes` already includes the report you just sent, beside every earlier one.

| Property | Description | Type |
|----------|-------------|------|
| workflow | (present) The workflow's current state. See [Read a Workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/#workflow) for every field. | object |
| terms | (optional) As on [Read a Workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/). | array |
| referenceNumber | (optional) As on [Read a Workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/). | string |
| messages | (present) As on [Read a Workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/). | array |

Each entry of `workflow.outcomes` carries the `reason` reported, whether the product counted it as `serviceable`, and `reportedAt`, when it was recorded. Entries are newest first, but the order is for display only: no entry is the workflow's current outcome. See [outcomes](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/#outcomes) for the full description.

Example, a loan reported as withdrawn and later as funded:
```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,
    "outcomes": [
      {
        "reason": "LOAN_FUNDED",
        "serviceable": true,
        "reportedAt": "2026-09-14T18:22:05Z"
      },
      {
        "reason": "APPLICATION_WITHDRAWN",
        "serviceable": false,
        "reportedAt": "2026-08-21T13:40:17Z"
      }
    ],
    "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"
    }
  ]
}
```

The withdrawal counted as unserviceable when it was reported. The funding counted as serviceable, so the workflow is serviceable, whatever order the two arrived in.

**400**

The edit was refused and **nothing was recorded**. Causes: a body with no field this API lists, including no body, `{}` and `{"outcome": null}`; an `outcome` that is not one of the [outcome codes](https://docs.pitchpointsolutions.com/api/workflow/edit_workflow/#outcome-codes); an `outcome` for a workflow whose product doesn't take outcome reports; an `outcome` the product doesn't accept, whose text names the outcomes it does; and a `wfrunpop_` runId, or any other identifier that names a run rather than the workflow. An `outcome` sent as an object or an array, a body that is malformed JSON or a literal `null`, and a `Content-Type` naming an unusable character set are refused before evaluation, as problem documents.

A 400 arrives in **two different body shapes**, decided by how far the request got before it was refused. Branch on the `Content-Type`: `application/json` is the `messages` envelope, and `application/problem+json` is a problem document.

**A value this service evaluated and rejected** carries the `messages` envelope, with `Content-Type: application/json` and the code `VALIDATION_ERROR`.

| Property             | Description                                                                                                  | Type   |
|----------------------|----------------------------------------------------------------------------------------------------------------|--------|
| messages             | (present) Always an array, never null. Carries one message whose `code` is `VALIDATION_ERROR`.                | array  |
| messages[].code      | (present) Always `VALIDATION_ERROR` on this shape.                                                            | string |
| messages[].text      | (present) Human-readable description of what was wrong. Never machine-parsed &mdash; the wording is not contractual. | string |
| messages[].timestamp | (present) RFC 3339 UTC with a trailing `Z`. Fractional seconds appear only when they are not zero, so parse this value rather than slicing it. | string |

Example:
```json
{
  "messages": [
    {
      "code": "VALIDATION_ERROR",
      "text": "this workflow's product does not accept the outcome LOAN_RESCINDED; it accepts: LOAN_FUNDED, LOAN_ORIGINATED, APPLICATION_DENIED, APPLICATION_WITHDRAWN",
      "timestamp": "2026-08-07T15:01:42Z"
    }
  ]
}
```

**A request refused before any value was evaluated** carries a problem document instead, with `Content-Type: application/problem+json` and **no `messages` key and no `VALIDATION_ERROR` code**. Its `type` tells you which case it is:

| Cause | `type` |
|-------|--------|
| A body that could not be read as an edit request at all: malformed JSON, a literal `null` body, a JSON array or bare value where an object was expected, or a field that can't be read as its type | `about:blank` |
| A `Content-Type` header naming a character set this service cannot use. It is refused before the body is read, so it says nothing about what you put in it. | `https://pointservices.com/problems/invalid-request` |

Example:
```json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Malformed JSON",
  "instance": "urn:pps:request:0000000000000000000000000000000000000000000000000000"
}
```

> **Warning:** A client that reads `messages[0].code` on every 400 without checking that `messages` exists will throw on the problem-document shape. Branch on the `Content-Type`, or on the presence of `messages`, not on the status.

Nothing is recorded, and the workflow is left exactly as it was.

**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 it lacks the permission to report outcomes, or it may not read this workflow. Nothing is recorded.

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. A workflow that exists but isn't yours answers 403 instead.

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.

**409**

A `concurrent-modification` problem document (`application/problem+json`, with no `messages` key): another request changed the workflow while yours was in flight, and nothing from yours was recorded. You don't need to re-read anything first, because a report only adds to the workflow: send the same call again.

Example:
```json
{
  "type": "https://pointservices.com/problems/concurrent-modification",
  "title": "Concurrent modification",
  "status": 409,
  "detail": "The resource was modified by another request while this one was being processed. This request was not applied; re-read the resource before resubmitting.",
  "instance": "urn:pps:request:0000000000000000000000000000000000000000000000000000"
}
```

**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.** If the first attempt was recorded, sending the same outcome again records it a second time, which changes no answer. Read the workflow afterwards to see the report in `outcomes`.
