# Add an Attachment

Source: https://docs.pitchpointsolutions.com/api/findings/create_workflow_finding_attachment/

**Beta**

Uploads a document and attaches it to a finding. Unlike every other operation on this API, the request body is `multipart/form-data`, not JSON, with two parts: `name` and `file`.

Needs `riskinsight:JournalResolvedValueUpdateAttachment` in the finding's `authorization.allow` (see [List a Workflow's Findings](https://docs.pitchpointsolutions.com/api/findings/get_workflow_findings_list#permissions)). The same permission covers [Remove an Attachment](https://docs.pitchpointsolutions.com/api/findings/delete_workflow_finding_attachment).

This operation isn't in the OpenAPI specification or the API Explorer, because its body isn't JSON.

### Sample

```bash
url="https://api.pointservices.com/riskinsight-services-ws/resources/v1/findings/workflows/wfpop_0000000000002082611/findings/fndg_0000000000006051384/attachments"
curl -X POST "${url}" \
  -H "Authorization: Bearer your_access_token_here" \
  -F "name=paystub-august.pdf" \
  -F "file=@paystub-august.pdf;type=application/pdf"
```

The raw multipart body `curl -F` builds for you:

```text
--boundary
Content-Disposition: form-data; name="name"

paystub-august.pdf
--boundary
Content-Disposition: form-data; name="file"; filename="paystub-august.pdf"
Content-Type: application/pdf

<binary bytes>
--boundary--
```

#### Header Properties

| Property | Value | Required? |
|----------|-------|-----------|
| Authorization | Bearer *your token* | true |
| Content-Type | multipart/form-data; boundary=... | true |

Set `Content-Type` to `multipart/form-data`, not `application/json`. `curl -F` sets it, and the boundary, for you.

#### Path Parameters

| Property | Description | Type |
|----------|-------------|------|
| workflowId | The workflow's id, exactly as this API returned it. | string |
| findingId | The id of the finding to attach the document to, exactly as published in `findingId`. | string |

> **Warning:** `workflowId`, `findingId` and `attachmentId` are opaque. Send each one back exactly as this API returned it: don't parse it, build one yourself, or add or remove its prefix. A `findingId` or `attachmentId` that isn't exactly as published, including one with its prefix removed, returns a 404 as if it didn't exist.

#### Multipart Parts

| Part | Description | Required |
|------|-------------|----------|
| name | The document's file name, published back as `Attachment.name`. Must not be blank. **255 UTF-8 bytes or fewer.** | yes |
| file | The document's raw bytes. Must not be empty, and **must not exceed 10 MiB (10,485,760 bytes)**. Set this part's own `Content-Type` header to the document's actual media type: it is published as `Attachment.contentType`, and must be 255 UTF-8 bytes or fewer. | yes |

> **Warning:** **An oversized file is a 400, not a silent truncation.** A file over 10 MiB isn't stored cut short: the whole request is rejected and no attachment is created. Check the file size before uploading if you want to fail fast without spending the upload.

Set the `file` part's `Content-Type` header accurately. It is stored exactly as sent and returned as `Attachment.contentType` on every later read. The service doesn't check it against the bytes or correct a wrong value; it detects a type from the bytes only when it has no usable recorded one (see [Download an Attachment](https://docs.pitchpointsolutions.com/api/findings/get_workflow_finding_attachment_content)).

### Responses

**200**

The finding after the upload: the same as a [Get One Finding](https://docs.pitchpointsolutions.com/api/findings/get_workflow_finding) made right afterwards, except that **`category` is left out**. GET the finding when you need its categories. See [List a Workflow's Findings](https://docs.pitchpointsolutions.com/api/findings/get_workflow_findings_list) for the full `Finding` field table.

```json
{
  "workflowId": "wfpop_0000000000002082611",
  "finding": {
    "findingId": "fndg_0000000000006051384",
    "code": "HP.ST",
    "displayName": "High-priority stated income mismatch",
    "references": [
      { "correlationId": "borrower-1", "type": "person" },
      { "correlationId": "subject-property", "type": "property" }
    ],
    "severity": "HIGH",
    "result": "Fail",
    "resultOrOverriddenResult": "Fail",
    "userMessage": ["Stated income does not match the verified source document."],
    "userSuggestion": ["Confirm the applicant's income against the attached document before proceeding."],
    "reviewed": false,
    "reviewRequired": true,
    "updatedAt": "2026-08-07T15:01:12Z",
    "hasAttachments": true,
    "hasHistory": true,
    "authorization": {
      "allow": [
        "riskinsight:JournalResolvedValueReview",
        "riskinsight:JournalResolvedValueUpdateNote",
        "riskinsight:JournalResolvedValueUpdateAttachment"
      ]
    }
  }
}
```

The upload adds an `edited` entry to the finding's [history](https://docs.pitchpointsolutions.com/api/findings/get_workflow_finding_history), so `hasHistory` is `true`. The response signals `hasAttachments` only; it never embeds the attachment. Fetch [List a Finding's Attachments](https://docs.pitchpointsolutions.com/api/findings/get_workflow_finding_attachments) afterwards for the new attachment's `attachmentId` and download link.

**400**

`name` is missing, blank or over 255 UTF-8 bytes; `file` is missing, empty or over 10 MiB; or the `file` part's `Content-Type` is over 255 UTF-8 bytes. No attachment is created.

The body is an RFC 9457 problem document, served as `application/problem+json`, with `type` set to `about:blank`. `detail` names the field at fault, for a person reading your logs.

| Property | Description | Type |
|----------|-------------|------|
| type | An absolute URI identifying the problem type. This is the stable value to match on; compare it as a string. | string |
| title | A short summary, written for a person. | string |
| status | The HTTP status code, repeated in the body. | number |
| detail | An explanation of this occurrence, written for your logs. Never match on it. | string |
| instance | A URI identifying this occurrence, written as `urn:pps:request:<id>`. The id is the same value as the `x-pps-request-id` response header. Quote it when you report a problem to PPS. | string |

```json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "file must not exceed 10485760 bytes",
  "instance": "urn:pps:request:0000000000000000000000000000000000000000000000000000"
}
```

**401**

No credential the service could accept reached it. The body is an RFC 9457 problem document, served as `application/problem+json`, and the response carries a `WWW-Authenticate` challenge. The request body is never read, so a 401 says nothing about whether the rest of your request would have been accepted.

Two problem types tell the cases apart:

| `type` | Meaning | `WWW-Authenticate` |
|--------|---------|--------------------|
| `about:blank` | No credential was presented. | `Bearer realm="Helix"` |
| `https://pointservices.com/problems/invalid-token` | A credential was presented and rejected. The response doesn't say why: an expired, revoked, badly signed and malformed token all look the same. | `Bearer realm="Helix", error="invalid_token"` |

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

> **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, so a fresh token is the fix here and never the fix for a 403.

**403**

You may not read this workflow, or you lack permission to update this finding's attachments or this workflow.

You are authenticated, and the answer is still no. This is an authorization decision, not a statement about your token, so refreshing the token won't turn it into a 200. Nothing is changed: a refused request is all or nothing, so there is nothing to clean up on your side.

The body is an RFC 9457 problem document, served as `application/problem+json`. Two problem types tell the cases apart:

| `type` | Meaning |
|--------|---------|
| `about:blank` | The request was refused before the operation ran. |
| `https://pointservices.com/problems/not-authorized` | The operation ran, and policy won't let you act on the workflow or finding you named. |

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

A 403 can also come from the edge, before the service is reached: for example, when a request value resembles SQL injection or cross-site scripting. Those refusals carry no problem document. Write your client to handle an error response it can't parse, rather than assuming every failure has a JSON body.

**404**

Unknown `workflowId` or unknown `findingId`.

The body is an RFC 9457 problem document, served as `application/problem+json`. Two problem types tell the cases apart:

| `type` | Meaning |
|--------|---------|
| `https://pointservices.com/problems/not-found` | An id in the path isn't one this API knows. Check that you sent it exactly as the API returned it. |
| `about:blank` | The URL or method isn't one this API serves at all. Check the path. |

| Property | Description | Type |
|----------|-------------|------|
| type | An absolute URI identifying the problem type. This is the stable value to match on; compare it as a string. | string |
| title | A short summary, written for a person. | string |
| status | The HTTP status code, repeated in the body. | number |
| detail | An explanation of this occurrence, written for your logs. Never match on it. | string |
| instance | A URI identifying this occurrence, written as `urn:pps:request:<id>`. The id is the same value as the `x-pps-request-id` response header. Quote it when you report a problem to PPS. | string |

```json
{
  "type": "https://pointservices.com/problems/not-found",
  "title": "No such resource",
  "status": 404,
  "detail": "Finding[fndg_0000000000006051384] not found",
  "instance": "urn:pps:request:8d3a1f56-6c94-4e20-b7f8-0a5e9c2d4b73"
}
```

> **Warning:** Branch on `type`, never on the text of `title` or `detail`.

**409**

Another request changed the finding while yours was in flight, so **nothing was applied**. The body is an RFC 9457 problem document, served as `application/problem+json`, with the type `https://pointservices.com/problems/concurrent-modification`.

The service doesn't retry it for you, and the response carries no `Retry-After`. Read the finding again, decide whether your change still makes sense against what you now see, and resubmit only if it does. Don't retry blindly: you may overwrite a change someone else just made.

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