> ## Documentation Index
> Fetch the complete documentation index at: https://docs.truv.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Document Upload Scenarios

> Pair the files a user uploads with the paystubs, W-2s, 1099s, volunteer letters, and PCS orders Truv should return for them

Set what Truv returns for the documents a user uploads. A `docs` scenario pairs a set of filenames with the employment data those files should yield, and serves it when a user uploads files with those names in Truv Bridge or your backend sends them to a [document collection](https://docs.truv.com/developers/integration/document-processing.md). It drives the document upload path of Income & Employment and Employment History.

Use it when Truv's [test documents](https://docs.truv.com/developers/testing/test-documents.md) don't produce the result you need: a paystub with your own numbers, a 1099-NEC for a contractor, a volunteer hours log, a military permanent change of station (PCS) order, or a validation failure on a file of your own. For the shared concepts, read [Custom Sandbox Data](https://docs.truv.com/developers/testing/custom-sandbox-data.md) first.

***

## How it works

A document upload scenario has no login. Each part of the request has its own role:

* The uploaded **filenames** select the scenario.
* The uploaded **document types** decide which records come back. See [How uploads map to records](#how-uploads-map-to-records).
* The scenario's **`payload`** supplies the values of those records. Truv doesn't read them from the files.
* Each record's **`document_id`** picks the downloadable file served with it.

`filenames` and `document_id` both name entries in your [sandbox document library](https://docs.truv.com/developers/testing/custom-sandbox-data.md#document-library): test files you upload with `POST /v1/sandbox/documents/`. They don't have to name the same entry. A record can serve the file the user uploaded or any other file in your library.

**1. Upload the files to your library**

One entry per file the user will upload. The entry's `filename` is what the scenario matches on.

**2. Create the scenario**

Set `filenames` to those names. On each paystub, W-2, or other record, set `document_id` to the `id` of the library entry to serve as that record's file.

**3. Upload the files in a verification**

In Truv Bridge, or through the Document Collections API, upload files with those names. Truv serves your payload as the task result and attaches the library files.

***

## Create a scenario \[Server-side]

> [!WARNING]
>
> Truv's own [test document filenames](https://docs.truv.com/developers/testing/test-documents.md) are reserved, both in the library and in `filenames`, and the API refuses them with `reserved_credential`. Compared without regard to case, the reserved set includes: the paystubs `first.paystub.pdf`, `most.recent.paystub.pdf`, `next.recent.paystub.pdf`, `hi.first.paystub.pdf`, `hi.most.recent.paystub.pdf`, and `hi.next.recent.paystub.pdf`; the W-2s `w2.pdf` and `hi.w2.pdf`; the tax forms `1099div.pdf`, `1099g.pdf`, `1099int.pdf`, `1099misc.pdf`, `1099nec.pdf`, `1099r.pdf`, `ssa1099.pdf`, and `f1040.pdf`; and the volunteer documents `volunteer_letter.pdf` and `volunteer_timesheet.pdf`. Other names can be reserved as well, and the set grows as Truv publishes new test files, so use a distinctive prefix, such as `acme.`.

First, upload the paystub PDF to the library:

```bash theme={null}
curl --request POST \
     --url https://prod.truv.com/v1/sandbox/documents/ \
     --header 'X-Access-Client-Id: YOUR_TRUV_CLIENT_ID' \
     --header 'X-Access-Secret: YOUR_TRUV_SANDBOX_SECRET' \
     --form 'filename=acme.paystub.pdf' \
     --form 'file=@./acme.paystub.pdf'
```

The response is the library entry. Keep its `id`; the scenario below references it as the paystub's `document_id`. `uploaded_file` and `documents` are abbreviated here.

```json theme={null}
{
  "id": "a41d7e9c05b24f6b8e3c1d5a7f92b604",
  "uploaded_file": { "file_id": "a41d7e9c05b24f6b8e3c1d5a7f92b604", "filename": "acme.paystub.pdf", "...": "..." },
  "documents": ["..."],
  "created_at": "2026-09-17T10:11:02.104882Z",
  "updated_at": "2026-09-17T10:11:02.104901Z"
}
```

Then create the scenario, matching on that filename. The paystub values come from `payload`; Truv doesn't read them from the PDF, and `document_id` only names the library file served as the paystub. Replace `YOUR_DOCUMENT_ID` with the `id` the upload returned:

```bash theme={null}
curl --request POST \
     --url https://prod.truv.com/v1/sandbox/scenarios/ \
     --header 'X-Access-Client-Id: YOUR_TRUV_CLIENT_ID' \
     --header 'X-Access-Secret: YOUR_TRUV_SANDBOX_SECRET' \
     --header 'Content-Type: application/json' \
     --data '{
  "label": "Acme — one paystub",
  "filenames": ["acme.paystub.pdf"],
  "payload": {
    "employments": [
      {
        "profile": { "full_name": "John Doe", "ssn": "991-91-9991" },
        "company": { "name": "Acme Inc." },
        "pay_frequency": "BW",
        "paystubs": [
          {
            "pay_date": "2026-08-14",
            "period_start": "2026-08-01",
            "period_end": "2026-08-14",
            "gross_pay": "2384.62",
            "net_pay": "1789.03",
            "gross_pay_ytd": "38153.92",
            "net_pay_ytd": "28624.48",
            "earnings": [{ "name": "Regular", "amount": "2384.62", "rate": "29.81", "units": "80" }],
            "deductions": { "Federal Tax": "381.54" },
            "document_id": "YOUR_DOCUMENT_ID"
          }
        ]
      }
    ]
  }
}'
```

`data_source` is omitted here because `filenames` fits only the document upload data source, so Truv infers it. The response is the stored scenario with `data_source: "docs"`.

### Use it in Bridge

[Start a sandbox verification](https://docs.truv.com/developers/testing/test-credentials.md) and choose to upload documents instead of connecting a payroll provider. Upload a file named `acme.paystub.pdf` as a paystub. Only the name selects the scenario, and only the document type you pick for the upload decides which records it receives; the paystub Truv serves is the library file you referenced. The task completes with your data, and the paystub is downloadable through [Pay Statements](https://docs.truv.com/api-reference/statements/object.md) like any other connection.

### Use it with the Document Collections API

Upload a file named `acme.paystub.pdf` to a [document collection](https://docs.truv.com/developers/integration/document-processing.md) under a sandbox key. The library entry stands in for Truv's classification, so the collection reports the documents the entry declares, and [finalizing](https://docs.truv.com/developers/integration/document-processing.md) the collection runs the same document upload task, which resolves your scenario by filename. See [Document Collections API](#document-collections-api) for what each piece of library metadata controls.

***

## Matching fields

| Field | Type | Description |
| - | - | - |
| `filenames` | array | The names of the files the user uploads, compared without regard to case. Required. Each must be a library entry. |
| `is_refresh` | boolean | `true` matches data refreshes only, `false` initial verifications only. `null` (the default) matches both. |

A scenario matches an upload that includes **every** file it names; extra uploaded files don't disqualify it. When several scenarios match, a scenario whose names are exactly the uploaded files wins over one that names only some of them, and among partial matches the one naming more files wins. The match is exact apart from case: the `suspicious.` prefix that flags Truv's own test files is not stripped here, so a user who uploads `suspicious.acme.paystub.pdf` doesn't reach a scenario naming `acme.paystub.pdf`. Use `flags.is_suspicious` to author a suspicious result instead.

`products` restricts the scenario to `income`, `employment`, or both. Empty, the default, matches both.

### Behavior

Document upload has no login step, so a scenario can only end the way a parse ends. `behavior_status` accepts `done`, `error`, `no_data`, `config_error`, and `unavailable`; `behavior_mfa` is refused. `behavior_duration_seconds` holds the task before it finishes. See [Behavior](https://docs.truv.com/developers/testing/custom-sandbox-data.md#behavior) for the fields.

The payload's `flags` add two outcomes a parse can reach with data:

| Flag | Effect |
| - | - |
| `is_suspicious` | The connection is flagged suspicious and the task still completes with your data. |
| `is_fraudulent` | The documents are marked fraudulent and the task ends with `config_error` and the message `Possible tampering detected`. |

***

## How uploads map to records

Truv serves your payload against what the user actually uploaded, counting the uploads by document type: the type the user picked for each file in Truv Bridge, or the type the collection recognized from your library entry's `documents`. One scenario can answer a one-paystub upload and a three-paystub upload alike.

| Upload | What is served |
| - | - |
| Paystubs | One `paystubs[]` entry per uploaded paystub, most recent first. An upload named `first.paystub`, `most.recent.paystub`, or `next.recent.paystub` (any extension) takes the oldest, newest, or second-newest instead. |
| W-2s | The most recent `w2s[]` entries, as many as W-2s were uploaded. |
| 1099s and 1040s | For each type and subtype uploaded, the most recent `tax_documents[]` entries of that type, as many as were uploaded. |
| Volunteer letters | The `volunteer_letters[]` entry whose `document_subtype` matches each uploaded volunteer document. |
| PCS orders | `pcs_orders[]` entries handed out in upload order and cycled, so every uploaded order comes back parsed. |

Only the first employment is served, so a document upload scenario returns one employer.

> [!NOTE]
>
> Support for multiple employments in document upload scenarios is in progress and coming soon.

Truv reads the other employments only to compare them with the first. That is how you set up the inconsistency cases that Truv's test files cover:

| Payload | Outcome |
| - | - |
| A second employment with a different `company.name` | `config_error`: "The documents contain different company names". This changes once multiple employments are supported. |
| A second employment with a different full name | `config_error`: "The documents contain different full names" |
| A second employment with a different `profile.ssn` | The connection is flagged suspicious and the task ends with `config_error`: "The documents contain different SSN's" |
| An employment missing `profile` or `company` | `no_data` |
| Only volunteer letters uploaded | Each letter's employment is created from that employment's `profile` and `company`, and the letters are served on it. |
| Only PCS orders uploaded | No employment is created; the orders are still served. |

> [!NOTE]
>
> Truv matches the employer name against its company catalog, as it does for production uploads, and reports the catalog's name when it finds a match. A scenario with `"company": { "name": "Acme Inc." }` can come back as `ACME`. Don't assert on the exact `company.name` in your tests. The match is fuzzy, so a made-up name can still match a real employer.

***

## Document Collections API

A document collection under a sandbox key looks up every uploaded file in your library by filename. When it finds an entry, the entry's test metadata replaces what Truv would otherwise have found in the file:

| Library field | What the collection does with it |
| - | - |
| `status: successful` (the default) | The file runs the real validation, then is classified as the entry's `documents` declare. |
| `status: invalid`, `duplicate`, or `failed` | Validation is skipped and the file takes that status, with the entry's `validations` overlaid on an all-passed set and `is_valid: false`, and `last_error` as its message. |
| `documents` | One recognized document per entry, with its `document_type`, `document_subtype`, `identity`, `company_name`, `start_page`, and `end_page`. Declaring an `identity` that doesn't match the collection's applicant is how you provoke a name or SSN mismatch. |

A file with no library entry gets Truv's filename-driven [test behavior](https://docs.truv.com/developers/testing/test-documents.md). Set the metadata on upload or with `PATCH /v1/sandbox/documents/{id}/`; see [Test metadata](https://docs.truv.com/developers/testing/custom-sandbox-data.md#test-metadata).

***

## Payload reference

The `payload` object contains an `employments` array, a `pcs_orders` array, and optional `flags`. Every field is optional unless noted: send only what your test needs, and leave the rest out.

> [accordion] employments\[]
>
> | Field | Type | Description |
> | - | - | - |
> | `profile` | object | `full_name`, `ssn` (`NNN-NN-NNNN` with an area number of 900–999), `home_address` (`street`, `city`, `state`, `zip`, `country`). All optional, so you can author a document the reader found no name on. |
> | `company` | object | `name`, `phone`, `address`. All optional. `name` can be replaced by a matching catalog employer's name; see [How uploads map to records](#how-uploads-map-to-records). |
> | `pay_frequency` | string | `M` monthly, `SM` semimonthly, `W` weekly, `BW` biweekly, `A` annually, `SA` semiannually, `C` commission. The only employment field a document yields. |
> | `paystubs` | array | Pay statements. |
> | `w2s` | array | W-2s. |
> | `tax_documents` | array | 1099s and 1040s. |
> | `volunteer_letters` | array | Volunteer transcripts and hours logs. |

> [accordion] employments\[].paystubs\[]
>
> | Field | Type | Description |
> | - | - | - |
> | `pay_date` | date | **Required.** |
> | `period_start` | date | |
> | `period_end` | date | |
> | `currency` | string | ISO 4217 code. |
> | `gross_pay` | decimal string | |
> | `net_pay` | decimal string | |
> | `gross_pay_ytd` | decimal string | |
> | `net_pay_ytd` | decimal string | |
> | `earnings` | array | Objects of `name` (required), `amount`, `rate`, `units`. |
> | `earnings_ytd` | array | Same shape as `earnings`. |
> | `deductions` | object | Name-keyed amounts, e.g. `{"Federal Tax": "381.54"}`. |
> | `deductions_ytd` | object | Same shape as `deductions`. |
> | `document_id` | string | **Required.** Library entry served as the paystub file. |

> [accordion] employments\[].w2s\[]
>
> | Field | Type | Description |
> | - | - | - |
> | `year` | string | **Required**, 4 characters. |
> | `fields` | object | Per-box values, e.g. `{"wages": "61996.12"}`. |
> | `document_id` | string | **Required.** Library entry served as the W-2 file. |

> [accordion] employments\[].tax\_documents\[]
>
> | Field | Type | Description |
> | - | - | - |
> | `document_type` | string | **Required.** `F1099` or `F1040`. A W-2 goes in `w2s`. |
> | `document_subtype` | string | For a 1099: `F1099_B`, `F1099_DIV`, `F1099_G`, `F1099_INT`, `F1099_K`, `F1099_MISC`, `F1099_NEC`, `F1099_R`, `F_SSA1099`. |
> | `year` | string | **Required**, 4 characters. |
> | `fields` | object | Per-box values, e.g. `{"nonemployee_compensation": "12400.00"}`. |
> | `document_id` | string | **Required.** Library entry served as the form's file. |

> [accordion] employments\[].volunteer\_letters\[]
>
> | Field | Type | Description |
> | - | - | - |
> | `document_subtype` | string | **Required.** `VOL_TRANSCRIPT` or `VOL_HOURS_LOG`. Matched against the uploaded volunteer document's subtype. |
> | `start_date` | date | |
> | `end_date` | date | |
> | `total_hours` | decimal string | |
> | `time_entries` | array | Objects of `date` (required), `hours`, `description`. |
> | `document_id` | string | **Required.** Library entry served as the letter's file. |

> [accordion] pcs\_orders\[]
>
> | Field | Type | Description |
> | - | - | - |
> | `branch` | string | `Army`, `Navy`, `USMC`, `Air Force`, `Coast Guard`, `Space Force`. |
> | `rank` | string | As a pay grade, e.g. `E-5`. |
> | `current_duty_station` | string | |
> | `future_duty_station` | string | |
> | `report_no_later_than_date` | date | |
> | `tour_length` | string | e.g. `36 months`. |
> | `dependents` | object | `count`, `names` (array), `accompanied` (boolean). |
> | `active_duty_service_commitment` | string | |
> | `document_id` | string | **Required.** Library entry served as the order's file. |

> [accordion] flags
>
> | Field | Type | Description |
> | - | - | - |
> | `is_suspicious` | boolean | Default `false`. Flags the connection as suspicious; the task still completes with your data. |
> | `is_fraudulent` | boolean | Default `false`. Marks the documents fraudulent and ends the task with `config_error`. |

> [!NOTE]
>
> Money and quantities are decimal **strings**, so nothing is lost to floating point: `"2384.62"`, not `2384.62`.

***

## Examples

Each example is a request body for `POST /v1/sandbox/scenarios/` and creates a new scenario. Send it with the same `curl` command as [the first scenario](#create-a-scenario-server-side). Every `document_id` is a library entry you uploaded first, and every name in `filenames` is an entry's `filename`.

### Two paystubs and a W-2

Name every file the user will upload. The scenario matches only when all three files are uploaded. It then serves one paystub per uploaded paystub, most recent first, and the W-2.

```json theme={null}
{
  "label": "Acme — two paystubs and a W-2",
  "filenames": ["acme.paystub.1.pdf", "acme.paystub.2.pdf", "acme.w2.pdf"],
  "payload": {
    "employments": [
      {
        "profile": { "full_name": "John Doe", "ssn": "991-91-9991" },
        "company": { "name": "Acme Inc." },
        "pay_frequency": "BW",
        "paystubs": [
          {
            "pay_date": "2026-08-14",
            "period_start": "2026-08-01",
            "period_end": "2026-08-14",
            "gross_pay": "2384.62",
            "net_pay": "1789.03",
            "gross_pay_ytd": "38153.92",
            "net_pay_ytd": "28624.48",
            "document_id": "a41d7e9c05b24f6b8e3c1d5a7f92b604"
          },
          {
            "pay_date": "2026-07-31",
            "period_start": "2026-07-18",
            "period_end": "2026-07-31",
            "gross_pay": "2384.62",
            "net_pay": "1789.03",
            "gross_pay_ytd": "35769.30",
            "net_pay_ytd": "26835.45",
            "document_id": "b52e8f0d16c35a7c9f4d2e6b8a03c715"
          }
        ],
        "w2s": [
          { "year": "2025", "fields": { "wages": "61996.12" }, "document_id": "c7a3f1e9b2d84c6fa0e5d9b1c3f7a2e4" }
        ]
      }
    ]
  }
}
```

### Contractor with a 1099-NEC

A 1099 goes in `tax_documents` with its subtype. The employment can carry no paystubs at all.

```json theme={null}
{
  "label": "Contractor — 1099-NEC",
  "filenames": ["acme.1099nec.pdf"],
  "payload": {
    "employments": [
      {
        "profile": { "full_name": "John Doe", "ssn": "991-91-9991" },
        "company": { "name": "Acme Inc." },
        "tax_documents": [
          {
            "document_type": "F1099",
            "document_subtype": "F1099_NEC",
            "year": "2025",
            "fields": { "nonemployee_compensation": "12400.00" },
            "document_id": "c63f9a1e27d46b8da05e3f7c9b14d826"
          }
        ]
      }
    ]
  }
}
```

### Volunteer hours log

Volunteer letters are matched to uploads by subtype, so upload a library entry whose `documents` declare `VOLUNTEER_LETTER` with subtype `VOL_HOURS_LOG`, then reference it.

```json theme={null}
{
  "label": "Volunteer — hours log",
  "filenames": ["acme.volunteer.hours.pdf"],
  "payload": {
    "employments": [
      {
        "profile": { "full_name": "John Doe" },
        "company": { "name": "Community Food Bank" },
        "volunteer_letters": [
          {
            "document_subtype": "VOL_HOURS_LOG",
            "start_date": "2026-02-20",
            "end_date": "2026-08-14",
            "total_hours": "48.0",
            "time_entries": [
              { "date": "2026-08-06", "hours": "4.0", "description": "Food bank" },
              { "date": "2026-08-13", "hours": "4.0", "description": "Food bank" }
            ],
            "document_id": "5c2e9b1d7a4f4e0c8b6d3a9f1e7c2b50"
          }
        ]
      }
    ]
  }
}
```

### Military PCS order

PCS orders sit at the top level of the payload. No employment is created for them, so an upload of only PCS orders returns the parsed orders and nothing else.

```json theme={null}
{
  "label": "PCS order — Army E-5",
  "filenames": ["acme.pcs.orders.pdf"],
  "payload": {
    "pcs_orders": [
      {
        "branch": "Army",
        "rank": "E-5",
        "current_duty_station": "Fort Liberty, NC",
        "future_duty_station": "Joint Base Lewis-McChord, WA",
        "report_no_later_than_date": "2026-07-15",
        "tour_length": "36 months",
        "dependents": { "count": 2, "names": ["Jane Doe", "Sam Doe"], "accompanied": true },
        "active_duty_service_commitment": "24 months",
        "document_id": "8d4f2a6c1b9e4c7d9a0b5e3f7c1d6a28"
      }
    ]
  }
}
```

### Documents that disagree on the name

A second employment whose full name differs ends the task with `config_error`, the way Truv's "different applicant names" test files do.

```json theme={null}
{
  "label": "Paystubs for two different people",
  "filenames": ["acme.paystub.p1.pdf", "acme.paystub.p2.pdf"],
  "payload": {
    "employments": [
      {
        "profile": { "full_name": "John Doe", "ssn": "991-91-9991" },
        "company": { "name": "Acme Inc." },
        "paystubs": [{ "pay_date": "2026-08-14", "gross_pay": "2384.62", "document_id": "d74a0b2f38e57c9eb16f4a8d0c25e937" }]
      },
      {
        "profile": { "full_name": "Jane Roe", "ssn": "991-91-9991" },
        "company": { "name": "Acme Inc." },
        "paystubs": [{ "pay_date": "2026-07-31", "gross_pay": "2384.62", "document_id": "e85b1c3a49f68dafc27a5b9e1d36fa48" }]
      }
    ]
  }
}
```

### Tampered document

`flags.is_fraudulent` marks the documents fraudulent and ends the task with `config_error`. The payload still needs the record the file would have yielded.

```json theme={null}
{
  "label": "Tampered paystub",
  "filenames": ["acme.paystub.tampered.pdf"],
  "payload": {
    "flags": { "is_fraudulent": true },
    "employments": [
      {
        "profile": { "full_name": "John Doe", "ssn": "991-91-9991" },
        "company": { "name": "Acme Inc." },
        "paystubs": [{ "pay_date": "2026-08-14", "gross_pay": "2384.62", "document_id": "f96c2d4b5a079eb0d38b6c0f2e47ab59" }]
      }
    ]
  }
}
```

### Nothing readable

A failure status needs no payload. `no_data` is how an upload Truv can't read anything from ends. The library refuses an unreadable file, so upload any valid PDF as `acme.blank.pdf`; the scenario, not the file, produces the outcome.

```json theme={null}
{
  "label": "Unreadable upload",
  "filenames": ["acme.blank.pdf"],
  "behavior_status": "no_data",
  "behavior_error_message": "Not enough data to complete the task"
}
```

***

## Limits

These are the default limits for document upload payloads. The account-wide limits are listed on [Custom Sandbox Data](https://docs.truv.com/developers/testing/custom-sandbox-data.md#limits).

| Limit | Default |
| - | - |
| Employments per scenario | 10 |
| Paystubs per employment | 60 |
| W-2s per employment | 5 |
| Tax documents per employment | 25 |
| Volunteer letters per employment | 10 |
| PCS orders per scenario | 10 |

***

## Next steps

> [card] [Test Documents](https://docs.truv.com/developers/testing/test-documents.md)
>
> Truv's built-in test files and the filename rules behind them

> [card] [Document Processing](https://docs.truv.com/developers/integration/document-processing.md)
>
> The Document Collections API your library entries stand in for

> [card] [Payroll scenarios](https://docs.truv.com/developers/testing/custom-sandbox-data/payroll.md)
>
> Serve the same employment data from a payroll login

> [card] [Sandbox API reference](https://docs.truv.com/api-reference/sandbox/object.md)
>
> Full request and response schemas for every sandbox endpoint


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.