# Stage Data

Source: https://docs.pitchpointsolutions.com/api/session/stage_session_sami/

**Beta**

Records a snapshot of your data into this session. Call it as often as you need to: the newest snapshot *is* what any workflow you start afterwards will read.

A session holds successive snapshots of **your data** &mdash; one set of data, restated, not a collection of separate ones. A [reconfirm](https://docs.pitchpointsolutions.com/api/workflow/reconfirm_workflow/) that carries `terms` records a snapshot the same way.

> **Before Starting:** You need an access token ([Obtaining An Access Token](https://docs.pitchpointsolutions.com/developer_guides/authentication/access_token/)) and a `sessionId` from [Start a Session](https://docs.pitchpointsolutions.com/api/session/start_session/). [Sessions and Workflows](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/index/) walks through the whole flow. Get the URL to submit against from your Pitchpoint Account Representative. To build the `terms` array, start with [Build a Loan Step by Step](https://docs.pitchpointsolutions.com/api/sami_terms/build_a_loan/).

### Sample

```bash
url="https://api.pointservices.com/riskinsight-services-ws/resources/v1/sessions/0000000000013500001/sami"
curl -X POST "${url}" \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "referenceNumber": "LN-0000001",
    "terms": [
      {
        "person": {
          "firstName": "Jane",
          "lastName": "Sample",
          "ssn": "000-00-0000",
          "dob": "01/31/1980",
          "homePhone": "5555550100",
          "residences": [
            {
              "currentIndicator": true,
              "address": {
                "addressLine1": "12 Example Avenue",
                "addressLine2": "Apt 4B",
                "city": "Sampleton",
                "state": "NY",
                "postalCode": "00000"
              }
            }
          ]
        }
      },
      {
        "property": {
          "propertyUsage": "PrimaryResidence",
          "propertyType": "Detached",
          "address": {
            "addressLine1": "1 Sample Street",
            "city": "Sampleton",
            "state": "NY",
            "postalCode": "00000"
          }
        }
      },
      {
        "loanTerms": {
          "noteAmount": "450000.00",
          "noteRatePercent": "6.375",
          "loanPurpose": "Purchase",
          "mortgageType": "Conventional"
        }
      }
    ]
  }'
```

#### 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     |
| Idempotency-Key           | *a V4 UUID*            | 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.

`Idempotency-Key` makes a retry of this call safe: send the same request again with the same key and you get the first response back, marked `X-Pps-Idempotent-Replay: true`, instead of the work being done twice. Use a new V4 UUID for each request. A key can be 1 to 255 characters; never put sensitive data in it. See [Retrying Safely](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/).

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   |
|-----------|---------------------------------------------------------------------------------|--------|
| sessionId | (mandatory) The session to stage into.                                       | string |

#### Request Data Properties

| Property | Description                                                                                                                              | Type  |
|----------|------------------------------------------------------------------------------------------------------------------------------------------|-------|
| terms    | (mandatory) The data to stage, as an array of terms. Must be present and non-empty &mdash; `[]` is rejected. Each element is an object carrying exactly one of the eleven branch keys. See [SAMI v2 Terms](https://docs.pitchpointsolutions.com/api/sami_terms/index/). | array |
| referenceNumber | (optional) Your own identifier for this loan: the loan or case number your staff will quote when they ask about it. It is recorded on the snapshot this stage writes and becomes the loan's reference number, which is what Pitchpoint's fulfillment staff see. [Read the Staged Data](https://docs.pitchpointsolutions.com/api/session/read_session_sami/) returns it beside `terms`, and a workflow ordered against this snapshot reports it on [Read a Workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/). Leading and trailing whitespace is trimmed, and at most 255 characters are accepted. A value that looks like SQL or markup is refused before it reaches this service, so encode such values (Base64, for instance) rather than sending them raw. It is independent of `correlationId`: this API does not copy one into the other. | string |

> **Tip:** **`referenceNumber` is part of the snapshot, just like `terms`.** Send it with every stage. Leaving it out, sending `null`, or sending only whitespace records a snapshot without one and clears the loan's reference number. [Read the Staged Data](https://docs.pitchpointsolutions.com/api/session/read_session_sami/) returns it beside `terms`, so sending a read-back body unchanged keeps it. A workflow already ordered keeps reporting the value of the snapshot it ran against. Starting a workflow with terms, or reconfirming one with terms, records a snapshot too, and sets it the same way.

> **Warning:** **Each snapshot is complete, not a patch.** Send your data as it now stands, in full. Anything you leave out is simply absent from the new snapshot rather than inherited from the previous one &mdash; which is exactly how you remove a borrower or a property. There is no way to add one field to an existing snapshot.

Keep your own copy of what you sent, and build each new snapshot from that rather than from memory. Reading the data back is not a substitute. [Read the Staged Data](https://docs.pitchpointsolutions.com/api/session/read_session_sami/) shows what the session holds, but it doesn't return every field as you sent it, and the terms echoed on [Read a Workflow](https://docs.pitchpointsolutions.com/api/workflow/get_workflow/) are also scoped to the product.

The body carries `terms` and, optionally, `referenceNumber`. Fields this call doesn't define are ignored. You choose the product when you [start a workflow](https://docs.pitchpointsolutions.com/api/session/start_session_workflow/), not when you stage.

Five things about `terms` are checked when you submit. Each one rejects the whole request with a 400 `VALIDATION_ERROR`, rather than dropping just the bad term:

1. `terms` must be present and a **non-empty array**. `"terms": []` is rejected.
2. Every element must be a **JSON object** &mdash; never `null`, never a bare string or number.
3. Every element must carry **exactly one key** &mdash; never zero and never two. `{}` and `{"person": {…}, "property": {…}}` are both rejected.
4. That key must be **one of the eleven branch keys**: `person`, `property`, `participant`, `loanTerms`, `loanToValue`, `transactionDetail`, `loanDetail`, `refinance`, `housingExpense`, `closing`, `company`.
5. Every value must be **readable as its field's type**. A number lands in a string field and `"true"` in a boolean one, but a value such as `"yes"` for an `…Indicator` fails the whole request.

Everything else is permissive. An absent field is accepted without complaint, and a field inside a term that this service does not recognize is ignored rather than refused, so a misspelled field name will not be reported back to you. Read the term descriptions as a statement of what is understood, not as a validator.

> **Warning:** **A product's own requirements are checked when it runs, not when you stage.** Staging data that a product will reject still returns 200; the workflow is then accepted with a 202 and the *workflow* fails. ADV, for example, needs at least one borrower carrying a residential address, a home phone and a date of birth. See [the flow guide](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/getting_started/#step-5-stage-the-data) for a worked example of that failure, and ask your Account Representative what your other products require.

### 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 snapshot was recorded and is now what the session holds. `dataStaged` is `true` from here on.

A retry under the same `Idempotency-Key` returns this same 200, byte for byte, with the header `X-Pps-Idempotent-Replay: true`, and records nothing. See [Retrying Safely](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/).

| Property | Description                                                                                       | Type   |
|----------|-------------------------------------------------------------------------------------------------------|--------|
| session  | (present) The session, with `dataStaged` now `true`. Never carries the staged data itself.                  | object |
| messages | (present) Always an array, never null. Carries one message whose `code` is `SESSION_UPDATED`.         | array  |

Example:
```json
{
  "session": {
    "sessionId": "0000000000013500001",
    "dataStaged": true,
    "correlationId": "alpha_numeric_string_to_help_identify_this_session"
  },
  "messages": [
    {
      "code": "SESSION_UPDATED",
      "text": "loan snapshot recorded",
      "timestamp": "2026-08-07T15:01:42Z"
    }
  ]
}
```

Staging costs nothing. Nothing is billed until you [start a workflow](https://docs.pitchpointsolutions.com/api/session/start_session_workflow/).

**400**

The request was refused and **the session is left exactly as it was** &mdash; nothing is kept from a rejected stage, including a previous snapshot, which remains the current one. Causes: an absent or blank body; `terms` absent, empty, or not an array; an element that is not a JSON object; an element carrying other than exactly one key; an element whose key is not one of the eleven branch keys; a value that cannot be read as its field's type; a `referenceNumber` longer than 255 characters. One bad term rejects the whole request rather than being dropped while the others proceed. A literal `null` body, malformed JSON, and a `Content-Type` naming an unusable character set are refused before evaluation, as problem documents. An `Idempotency-Key` that is blank, longer than 255 characters, or already used with a different body is also refused with `VALIDATION_ERROR`. A 400 is never stored. After fixing a problem in the body, send it again with the same key. After a key-reuse 400, see [Retrying Safely](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/#when-to-make-a-new-key).

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`. That includes a value inside `terms` that can't be read as its field's type, such as `"yes"` for an `…Indicator`: the body as a whole was readable, so the value was evaluated.

| 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": "every element of terms must carry exactly one term key; terms[0] carries 2",
      "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 a stage 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 outside `terms` | `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.

No snapshot is recorded, and the `session` object is omitted.

**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 stage into this session. No snapshot 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.

> **Warning:** This is the call most likely to meet the edge refusal described above, because terms carry borrower and property free text. A name or an address containing something that resembles SQL or markup is refused before this service sees it.

**404**

No session exists with this identifier, or the path you called is not served.

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": "Session[0000000000013500001] 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**

`IDEMPOTENCY_IN_PROGRESS` &mdash; an earlier request under the same `Idempotency-Key` is still running. **This is the one 409 to retry:** wait the number of seconds in the `Retry-After` header, then send the same request with the same key. If it persists for more than a few minutes, stop and follow [When the 409 doesn&rsquo;t go away](https://docs.pitchpointsolutions.com/developer_guides/sessions_and_workflows/retrying_safely/#when-the-409-doesnt-go-away).

Another change to this session was in flight and this one lost the race. This arrives as a `concurrent-modification` problem document, described below. Treat a `WORKFLOW_WRITE_CONFLICT` code in the envelope the same way. Either way the service does not retry it, so you decide whether to resubmit. Staging two snapshots of one session at the same time is the way to provoke this: stage them one after another instead.

**Branch on the `Content-Type` first, and only then on `messages[].code`.** A 409 here arrives in two body shapes, and the corrective action differs by cause. Never branch on the status alone, and never read `messages[0].code` without checking that `messages` is present.

**`application/json`** carries the `messages` envelope:

| Property             | Description                                                                                             | Type   |
|----------------------|-----------------------------------------------------------------------------------------------------------|--------|
| messages             | (present) Always an array, never null.                                                                   | array  |
| messages[].code      | (present) One of the codes described above for this status.                                              | string |
| messages[].text      | (present) Human copy. Never machine-parsed.                                                              | string |
| messages[].timestamp | (present) RFC 3339 UTC with a trailing `Z`, fractional seconds only when they are not zero.              | string |

**`application/problem+json`** carries a problem document with `type` `https://pointservices.com/problems/concurrent-modification` and no `messages` key. Another request changed the same resource while yours was in flight, and **nothing from yours was applied**. It carries no `Retry-After`, deliberately: read the resource again, decide whether your change still applies to what you now see, and resubmit only if it does. Don't resend the same request on a timer.

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"
}
```

In either shape the conflict is final for that attempt: the service does not retry it for you. The one exception is `IDEMPOTENCY_IN_PROGRESS`, which always arrives in the `messages` envelope and which you retry after the `Retry-After` delay.

**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.** Send the same request again with the same `Idempotency-Key`: a 500 on this call is not stored, so if the failed attempt's snapshot was kept you get its original response, and if not the stage runs again. Either way you end up with one new snapshot. Without a key, a retry can record the same snapshot twice. Re-staging is free.
