API REFERENCE

View as Markdown

Beta

Add an Attachment

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). The same permission covers Remove an Attachment.

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

Sample

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:

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

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

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

Responses

200

The finding after the upload: the same as a Get One Finding made right afterwards, except that category is left out. GET the finding when you need its categories. See List a Workflow’s Findings for the full Finding field table.

{
  "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, so hasHistory is true. The response signals hasAttachments only; it never embeds the attachment. Fetch List a Finding’s 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
{
  "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"
{
  "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"
}

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.
{
  "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
{
  "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"
}

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.

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

Copyright © Pitchpoint Solutions. All rights reserved.