# Retrying Safely

Source: https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/

**Beta**

A request can time out, or the connection can drop, after the service has done the work but before you get the response. Retry it blindly and you can start a second session, stage the loan a second time, or start and pay for a second workflow.

An **idempotency key** makes the retry safe. You send a key of your choosing in the `Idempotency-Key` header. If you send the same request again with the same key, the service returns the response it gave the first time instead of doing the work again. That returned response is called a **replay**.

## Which calls accept a key

Every POST on the session and workflow APIs accepts an optional `Idempotency-Key` header. Without the header, each call behaves as its reference page describes.

| Call | Without a key, a retry | With a key, a retry |
|------|------------------------|---------------------|
| [Start a Session](https://docs.pitchpointsolutions.com/api/session/start_session/) | Starts a second session. | Returns the first session. |
| [Stage a Loan](https://docs.pitchpointsolutions.com/api/session/stage_session_sami/) | Stages the loan again. | Returns the first response and records nothing. |
| [Start a Workflow in a Session](https://docs.pitchpointsolutions.com/api/session/start_session_workflow/) | Starts and bills a second workflow. | Returns the first workflow. Nothing is billed twice. |
| [Start a Workflow](https://docs.pitchpointsolutions.com/api/workflow/start_workflow/) | Starts and bills a second workflow. | Returns the first workflow. Nothing is billed twice. |
| [Reconfirm a Workflow](https://docs.pitchpointsolutions.com/api/workflow/reconfirm_workflow/) | Starts and bills a second run. | Returns the first run. Nothing is billed twice. |

GET requests don't need a key. Reading never changes anything, so a GET is always safe to retry.

## Step 1: Make a key

Use a V4 UUID. A key can be 1 to 255 characters, not all spaces.

* **Make a new key for every new request.** Starting two different workflows needs two keys.
* **Don't put sensitive data in a key.** Keys are stored as you send them, and unlike responses they are not encrypted. Never build a key from a borrower's name, email address or loan number.
* **Keys are matched ignoring case and surrounding spaces.** `abc` and `ABC` are the same key. A UUID never runs into this.

## Step 2: Send it

Add the key as a header. This starts a workflow with a key:

```bash
idempotency_key="$(uuidgen)"
curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/sessions/0000000000013500001/workflows \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ${idempotency_key:?set idempotency_key first}" \
  -d '{
    "product": { "service": "ADV", "model": "ADV-120" },
    "correlationId": "my-workflow-0001"
  }'
```

## Step 3: Retry with the same key

If the request times out, or you get no response, send it again **exactly as before**: the same key, the same URL and the same body.

The service compares each retry with the first request sent under that key:

* **The same request** gets a replay. The order of fields inside a JSON object and whitespace don't matter, and numbers compare by value, so `100` and `100.0` are the same. The order of elements in an array, such as `terms`, does matter.
* **A different body under the same key** is refused with `400 VALIDATION_ERROR` once the first request has finished. While the first is still running, it gets `409 IDEMPOTENCY_IN_PROGRESS` instead. It never gets a replay.
* **The same key on a different call, or on a different session or workflow in the URL,** is a separate request. The key doesn't carry over. The URL is compared as sent, so a workflow ID with and without its `wfpop_` prefix are different URLs.

## Step 4: Read the response

The service **stores** the response it gives under a key and returns it to every retry: every 2xx, and a 5xx on the three workflow calls. A 4xx is never stored, and neither is a 5xx on Start a Session or Stage a Loan.

| You receive | It means | Do this |
|-------------|----------|---------|
| No response, a timeout, or a `502`, `503` or `504` | The request may or may not have reached the service. | Send the same request with the same key. This is the case keys exist for. |
| A 2xx with the header `X-Pps-Idempotent-Replay: true` | A replay. An earlier request under this key did the work. | Use it as the result. |
| A 2xx without that header | The request ran now. | Use it as the result. |
| `409` with `messages[0].code` `IDEMPOTENCY_IN_PROGRESS` | An earlier request under this key is still running. | Wait the number of seconds in the `Retry-After` header, then send the same request with the same key. Stop after a few minutes and follow [When the 409 doesn't go away](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/#when-the-409-doesnt-go-away). |
| Any other `409`, such as `SESSION_EMPTY` or `RECONFIRM_NOT_AVAILABLE` | The request was refused for a reason a retry won't fix. | Follow the call's reference page. |
| `400` | The request was refused. The body is either `VALIDATION_ERROR` in `messages` or a problem document with no `messages`. | Depends on the cause; see [When to make a new key](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/#when-to-make-a-new-key). |
| `401` or `403` | Your credential didn't work, or you aren't allowed to make this call. A `403` with an HTML body came from the edge (the protection in front of the service) and never reached it. | Fix the credential or the request, then send it with the same key. |
| `404` | The session or workflow in the URL doesn't exist. | Check the URL. |
| `429` | Too many requests. The edge refused the request, so it never reached the service. | Back off for a few seconds, then send the same request with the same key. |
| `500` on Start a Workflow, Start a Workflow in a Session or Reconfirm a Workflow | Unknown whether the work happened. The 500 is stored, so every retry under this key returns the same 500, marked `X-Pps-Idempotent-Replay: true`. | Check what happened (see [Checking what happened](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/#checking-what-happened)). Send the request again under a **new** key only if it didn't happen. |
| `500` on Start a Session or Stage a Loan | The failure is not stored. | Send the same request with the same key. It runs again. |

A replay is the original response, not the current state. A replayed Start a Session shows `loanStaged` as it was when the session started, and a replayed workflow start shows the workflow's first status. [Read the session](https://docs.pitchpointsolutions.com/api/session/get_session/) or [read the workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/) for how things stand now.

A 409 looks like this:

```json
{
  "messages": [
    {
      "code": "IDEMPOTENCY_IN_PROGRESS",
      "text": "A request with this Idempotency-Key is still running. Retry the same request with the same key after the Retry-After delay.",
      "timestamp": "2026-09-25T03:51:05.896Z"
    }
  ]
}
```

`Retry-After` is a whole number of seconds, such as `5`. It is never a date.

## Checking what happened

When you can't tell from the response whether the work happened, check before you send the request under a new key.

**Start a Workflow and Start a Workflow in a Session.** Look for the workflow with [Find Workflows](https://docs.pitchpointsolutions.com/api/workflow/list_workflows/), using the `correlationId` you sent:

```bash
curl "https://api.pointservices.com/riskinsight-services-ws/resources/v1/workflows?correlationId=my-workflow-0001" \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Accept: application/json"
```

If a workflow comes back, use it. If none does, start it again under a new key. Send a distinct `correlationId` on every workflow start: it is how you find a workflow when you have no response.

**Reconfirm a Workflow.** The workflow always exists, so read it with [Read a Workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/). If its `runId` differs from the one before you reconfirmed, or its `status` is `processing`, the reconfirm was accepted. If not, reconfirm again under a new key.

**Start a Session and Stage a Loan.** There is nothing to search for, and neither call is billed. If a 409 doesn't go away, send the request under a new key: a new stage replaces the loan the session holds, and an unused session costs nothing.

## When the 409 doesn't go away

If `IDEMPOTENCY_IN_PROGRESS` keeps coming back for more than a few minutes, the first request may have finished without its response being stored. Stop retrying, [check what happened](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/#checking-what-happened), and continue under a new key only if the work didn't happen.

## When to make a new key

* **Every new request.** A key belongs to one request.
* **After a stored 500**, once you have checked that the work didn't happen.
* **After a 409 that doesn't go away**, once you have checked.
* **After the key expires.** A stored response is kept for 30 days by default, and your account may be configured differently. After that, the service treats a retry as a new request. For a workflow start, that means a second, billed workflow.

A 400 isn't stored. What to send next depends on the cause:

* **A problem in the body**, such as a bad `terms` element: fix it and send it with the same key.
* **A blank key, or one longer than 255 characters**: send the request with a valid key.
* **A key already used with a different body**: the first request's result is still stored under that key. Send the original body to get its replay, or [check what happened](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/#checking-what-happened) before using a new key.

## Rules

* **Keys belong to you.** A key is scoped to the user who sends it: another user sending the same value, even in your organization, makes a separate request, and nobody is ever sent another user's response.
* **Keys belong to one call.** The same value on a different call, or on a different session or workflow in the URL, is a separate request.
* **Keys pass through the edge.** Like any header, a key can be refused by the edge if it looks like an attack, with a `403` and an HTML body instead of JSON.
* **A replay has the same status, content type and body as the original.** `X-Pps-Idempotent-Replay: true` marks it as a replay. Other headers, such as request IDs, are new on each response.
