DEVELOPER GUIDE

Beta

Sessions and Workflows

Stage your data, start a workflow against it, and find out when the result is ready.

Two surfaces do this work. A session holds the data a product runs against: you stage it, and you can re-stage it as often as you like without starting anything. A workflow is one run of a product against that data: it reads what the session holds, runs the product, and produces a result. Staging is free. Starting a workflow is billed.

The whole loop

You do three things: start a workflow, wait for PPS to tell you a result is ready, then read it.

sequenceDiagram
    autonumber
    participant You
    participant PPS
    participant Endpoint as Your endpoint
    You->>PPS: POST /v1/sessions/{id}/workflows
    PPS-->>You: 202 workflowId
    Note over PPS: the product runs
    PPS->>Endpoint: workflow.updated, data.status "ready"
    You->>PPS: GET /v1/findings/workflows/{workflowId}/findings
    PPS-->>You: the findings

No event fires when you start a workflow. The delivery arrives later, when there is something to read.

Two of these come from a person and can take days, so ask before you write any code. A PPS access token — see Obtaining An Access Token. Your org unit GUID, a string like OU41502, which your PPS account manager supplies and you need to register a webhook endpoint. An authorized destination domain — PPS must authorize the domain your endpoint lives on before it will send anything there; ask your PPS account manager. The service and model values your account can start workflows for, which your PPS account manager also supplies, because no endpoint lists them.

Register your webhook endpoint before you start any workflows. There is no back-fill. An endpoint registered later hears about future events only, and nothing replays what you missed.

Step 1: Authenticate

Every call carries a bearer token in the Authorization header.

curl -X POST https://api.pointservices.com/user-management-services-ws/oauth2/002/signInWithPassword \
  -H "Content-Type: application/json" \
  -d '{
    "username": "customerUsername",
    "password": "customerPassword"
  }'

Keep the access_token. Every call below sends it as Authorization: Bearer <token>. It expires after an hour, so build the refresh in now.

Treat a 401 and a 403 alike: get a fresh token and retry once. Both mean your credential did not work, and they do not separate cleanly enough to justify different recovery paths. A wall of 403s on calls that worked a minute ago is far more often an expired token than a change to your entitlements. If a retry with a new token also fails, stop and surface the error rather than looping.

Obtaining An Access Token covers refreshing and signing in again.

Step 2: Register your webhook endpoint

Do this first, before you start any workflows. The endpoint is how PPS tells you a result is ready.

curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/endpoints \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/pps/workflows",
    "organizationalUnitGuid": "OU41502",
    "displayName": "Workflow results",
    "acceptedEventTypes": [
      {
        "serviceName": "ADV",
        "modelName": "ADV-120",
        "eventTypes": ["workflow.updated", "workflow.failed"]
      }
    ],
    "signingSecret": "whsec_Kv9mQ3xT7pYbN2sW8gLdR5hJ4cF6nZaE1uXoP0iVtB0="
  }'

A successful register returns 200, not 201.

{
  "id": 4711,
  "organizationalUnitGuid": "OU41502",
  "displayName": "Workflow results",
  "url": "https://hooks.example.com/pps/workflows",
  "enabled": true,
  "circuitState": "CLOSED",
  "receiving": true,
  "hasNoContactsRegistered": false,
  "signatureMode": "SYMMETRIC",
  "acceptedEventTypes": [
    {
      "serviceName": "ADV",
      "modelName": "ADV-120",
      "eventTypes": ["workflow.updated", "workflow.failed"]
    }
  ],
  "warnings": []
}

Check receiving, not enabled. A new endpoint is always enabled. receiving: true is what tells you an event fired right now would actually reach you.

Declare both topics. An endpoint that accepts workflow.updated without workflow.failed registers successfully and then never hears about a failed run.

Getting Started with Webhooks covers choosing a signing secret and verifying what arrives.

Step 3: Choose a route

There are two ways to start a workflow and only one way to follow one.

  Direct Session
Call POST /v1/workflows with terms inline start a session, stage, then start a workflow
Calls to start one workflow one three
Starting two workflows against one set of data sends it twice sends it once
Building your data up over time not possible re-stage as often as you need

Use Start a Workflow for a single fire-and-forget workflow. Use a session when you will start more than one workflow against the same data, or when you assemble it across several steps. Both return the same workflow envelope, and everything after the start is identical.

The rest of this guide follows the session route.

Step 4: Start a session

curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/sessions \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "correlationId": "my-loan-0001"
  }'
{
  "session": {
    "sessionId": "0000000000013500001",
    "loanStaged": false,
    "correlationId": "my-loan-0001"
  },
  "messages": [
    {
      "code": "SESSION_STARTED",
      "text": "session started"
    }
  ]
}

Keep the sessionId. The next two calls take it. The session is empty and nothing is billed, so loanStaged is false until you stage.

Send Content-Type: application/json even though the body is optional. A POST that arrives without it is refused before it reaches the service, with a 403 nothing can explain to you.

Starting a session is not idempotent. A client that retries after a timeout holds two sessions rather than recovering the first. Unused sessions are inert and cost nothing, but do not build a client that starts more than it stages into.

Step 5: Stage the loan

Staging records a snapshot of your data. Call it as often as you need — the newest snapshot is what a workflow will read. You stage a loan through the /sami path, using SAMI, PitchPoint’s own format for describing one.

curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/sessions/0000000000013500001/sami \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "terms": [
      {
        "person": {
          "firstName": "Jane",
          "lastName": "Sample",
          "ssn": "000-00-0000",
          "dob": "01/31/1980",
          "homePhone": "5555550100",
          "residences": [
            {
              "currentIndicator": true,
              "address": {
                "addressLine1": "1 Sample Street",
                "city": "Sampleton",
                "state": "NY",
                "postalCode": "00000"
              }
            }
          ]
        }
      }
    ]
  }'
{
  "session": {
    "sessionId": "0000000000013500001",
    "loanStaged": true,
    "correlationId": "my-loan-0001"
  },
  "messages": [
    {
      "code": "SAMI_STAGED",
      "text": "loan staged"
    }
  ]
}

loanStaged is now true. That is the thing to check here.

Each snapshot is complete, not a patch. Send your data as it now stands, in full. Anything you omit is absent from the new snapshot rather than inherited — which is how you remove a party. Keep your own copy of what you sent and build the next snapshot from that.

A 200 means your JSON was well formed. It does not mean your submission is complete, or that a product will accept it. Staging checks shape, not meaning: a field name this service does not recognize is ignored rather than refused.

Each product needs particular facts, and it checks for them when it runs. ADV needs at least one borrower with a residential address, a home phone and a date of birth — the example above has all three. For any other product, ask your PPS account manager what it requires; those requirements are not published, and staging will not tell you.

For the term format and worked examples, see SAMI v2 Terms. For the request and response contract, see Stage a Loan.

Step 6: Start a workflow

Starting a workflow names a product and nothing else. The data comes from the session.

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" \
  -d '{
    "product": {
      "service": "ADV",
      "model": "ADV-120"
    },
    "correlationId": "my-workflow-0001"
  }'

You get back a 202. Starting a workflow does not wait for the product to run: the call returns a handle within milliseconds, and you collect the result afterwards.

{
  "workflow": {
    "workflowId": "wfpop_0000000000002080001",
    "runId": "wfrunpop_0000000000002080001",
    "sessionId": "0000000000013500001",
    "correlationId": "my-workflow-0001",
    "product": {
      "service": "ADV",
      "model": "ADV-120"
    },
    "status": "processing",
    "reconfirmAvailable": false
  },
  "messages": [
    {
      "code": "ACCEPTED",
      "text": "workflow accepted for processing",
      "timestamp": "2026-08-07T15:01:42Z"
    }
  ]
}

Keep the workflowIdwfpop_0000000000002080001 here. Every other call takes it. It is stable for the life of the workflow, including across reconfirms.

This is the call that costs money. Stage once and start as many workflows as you need, but each workflow is independently billed, not a retry of the last one. A reconfirm is billed as its own workflow too.

A 202 means the workflow was accepted for processing. Whether your submission satisfies the product is decided later, and one that does not fails the workflow rather than rejecting the request.

Start a Workflow in a Session has the full contract, what runId is for, and what a failed run records.

Step 7: Wait for the delivery

Nothing fired when you started the workflow. When the product finishes, PPS POSTs to the endpoint you registered in Step 2.

POST /pps/workflows HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
webhook-id: msg_9f1c2e4a-6b73-3d81-a0c5-4e2f7b9d1350
webhook-timestamp: 1787927163
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=

{"type":"workflow.updated","eventId":"msg_9f1c2e4a-6b73-3d81-a0c5-4e2f7b9d1350","timestamp":"2026-08-07T15:03:11Z","data":{"workflowId":"wfpop_0000000000002080001","runId":"wfrunpop_0000000000002080001","correlationId":"my-workflow-0001","serviceName":"ADV","modelName":"ADV-120","status":"ready"}}

The same body, formatted:

{
  "type": "workflow.updated",
  "eventId": "msg_9f1c2e4a-6b73-3d81-a0c5-4e2f7b9d1350",
  "timestamp": "2026-08-07T15:03:11Z",
  "data": {
    "workflowId": "wfpop_0000000000002080001",
    "runId": "wfrunpop_0000000000002080001",
    "correlationId": "my-workflow-0001",
    "serviceName": "ADV",
    "modelName": "ADV-120",
    "status": "ready"
  }
}

data.workflowId is the same wfpop_0000000000002080001 the 202 gave you in Step 6. That is how you match a delivery to the workflow that produced it. correlationId is the value you sent when you started it, so you can match on that instead if you prefer.

data.status is ready. A result exists and is worth fetching.

Verify the signature before you trust the body. An endpoint that acts on unverified requests accepts event data from anyone who finds its URL. See Verifying Signatures.

Receiving Deliveries covers what to answer, handling the same event twice, and what PPS does when your server is down.

Step 8: Read the result

Two reads are available, and they are siblings rather than a chain. Findings give you the result. The workflow gives you its state and, on a failure, the reason.

Both are callable at any time. The delivery does not unlock them — it tells you the call is now worth making.

flowchart TD
    A[Delivery arrives] --> B{Which topic?}
    B -->|workflow.updated| C[GET the findings]
    B -->|workflow.failed| D[GET the workflow]
    C --> E[There is data]
    D --> F[messages says why]

After a workflow.updated delivery, read the findings:

curl -X GET "https://api.pointservices.com/riskinsight-services-ws/resources/v1/findings/workflows/wfpop_0000000000002080001/findings" \
  -H "Authorization: Bearer your_access_token_here"
{
  "workflowId": "wfpop_0000000000002080001",
  "findings": [
    {
      "findingId": "fndg_0000000000006051384",
      "code": "HP.ST",
      "displayName": "High-priority stated income mismatch",
      "category": ["Income"],
      "severity": "HIGH",
      "alertState": "Alert",
      "userMessage": ["Stated income does not match the verified source document."],
      "userSuggestion": ["Confirm the applicant's income against the attached document before proceeding."],
      "reviewStatus": "PENDING",
      "reviewNote": null,
      "hasAttachments": true,
      "hasHistory": false
    }
  ]
}

Pass the workflowId exactly as you received it, wfpop_ prefix and all.

Calling findings early is fine. Before a result exists it returns nothing. That is the correct answer to a question asked early, not an error and not a reason to retry hard. Once a ready delivery has arrived, there will be data.

Do not poll the workflow waiting for status: complete. The workflow resource and a delivery use different vocabularies: after a delivery carrying data.status: "ready", reading the workflow returns status: "processing". Both are correct. complete reflects a PPS-side action, not your result being ready. The delivery is the signal to act on.

Getting Started with Findings covers reading, filtering and resolving findings.

When a run fails

If workflow.failed arrives instead, read the workflow to see why.

curl -X GET "https://api.pointservices.com/riskinsight-services-ws/resources/v1/workflows/wfpop_0000000000002080001" \
  -H "Authorization: Bearer your_access_token_here"
{
  "workflow": {
    "workflowId": "wfpop_0000000000002080001",
    "runId": "wfrunpop_0000000000002080001",
    "sessionId": "0000000000013500001",
    "correlationId": "my-workflow-0001",
    "product": {
      "service": "ADV",
      "model": "ADV-120"
    },
    "status": "failed",
    "serviceable": false
  },
  "messages": [
    { "code": "FAILED", "text": "/Inputs[1]/Input[1]/Loan[1]/Borrower[1]/Residence is required." },
    { "code": "FAILED", "text": "/Inputs[1]/Input[1]/Loan[1]/Borrower[1]/ContactPoint is required." }
  ]
}

status is the answer; messages is the explanation. Branch on status, never on a message code or its text.

Those paths name internal elements, not the keys you sent. Residence means the residences array on a person, and ContactPoint means contact details such as homePhone. Read them as a description of the missing fact. Fix the loan, stage it again, and start another workflow.

Read a Workflow has the full status vocabulary and the message-code rules.

Trigger a reconfirm

Triggering a reconfirm runs a workflow again under the same handle: same workflowId, new runId. It is its own action, distinct from starting a workflow.

curl -X POST "https://api.pointservices.com/riskinsight-services-ws/resources/v1/workflows/wfpop_0000000000002080001" \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "correlationId": "my-reconfirm-0001"
  }'

A reconfirm fires its own events, exactly as the first run did. The delivery carries the same workflowId and a new runId, so your handler needs no special case. A correlationId belongs to the run and is not inherited, so send a new one if you match on it.

Check reconfirmAvailable, never the presence of links.reconfirm — that link is published on every workflow envelope whether or not a reconfirm is possible. Compare the flag against true explicitly; it is optional on the wire, and absent means “not available”.

Send no body to re-run against the data the workflow already has, or terms to run against a revised set. Do not name a product; it is inherited.

Reconfirm a Workflow has the request contract and when reconfirmAvailable is false.

If you lose the handle

Searching by the correlationId you sent when you started the workflow finds it again.

curl -X GET "https://api.pointservices.com/riskinsight-services-ws/resources/v1/workflows?correlationId=my-workflow-0001" \
  -H "Authorization: Bearer your_access_token_here"

Make your correlation ids distinctive. Matching is on the whole value, never a prefix, and there is nothing to fall back on.

This is also your recovery path after a 500 or a timeout when starting a workflow. The handle is issued before any work begins, so a lost connection never leaves you unsure whether a workflow started — search for the id you sent before starting another.

Find Workflows covers which correlationId is searched, which are not, and the result limits.

Error handling

Four rules cover this surface.

  1. Branch on the status code before you parse anything. Responses do not share one body shape. Some carry a messages envelope, some a JSON problem document, some nothing usable at all — and a 400 can be either of the first two depending on how far the request got.
  2. Check that messages exists before reading messages[0].code. On several endpoints the problem-document shape is the common case, not the exception.
  3. Treat an unrecognized message code as non-retryable. The vocabulary is closed but additive; meeting a new code means stop, not retry.
  4. A 500 means the outcome is unknown, not that nothing happened. A failure after a write has committed leaves the write. Re-read, or search by correlationId, before retrying — retrying blind is how one workflow becomes two, and nothing de-duplicates them for you.

A complete run

host="https://api.pointservices.com/riskinsight-services-ws/resources/v1"
tok="your_access_token_here"

# 1. register the webhook endpoint, once, before you start any workflows
curl -s -X POST "${host}/webhooks/endpoints" \
  -H "Authorization: Bearer ${tok}" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.example.com/pps/workflows","organizationalUnitGuid":"OU41502","acceptedEventTypes":[{"serviceName":"ADV","modelName":"ADV-120","eventTypes":["workflow.updated","workflow.failed"]}],"signingSecret":"whsec_Kv9mQ3xT7pYbN2sW8gLdR5hJ4cF6nZaE1uXoP0iVtB0="}'

# 2. start a session -> returns sessionId
curl -s -X POST "${host}/sessions" \
  -H "Authorization: Bearer ${tok}" \
  -H "Content-Type: application/json" \
  -d '{"correlationId":"my-loan-0001"}'

# 3. stage the loan -> 200, loanStaged becomes true
curl -s -X POST "${host}/sessions/0000000000013500001/sami" \
  -H "Authorization: Bearer ${tok}" \
  -H "Content-Type: application/json" \
  -d @loan.json

# 4. start a workflow -> 202, returns the wfpop_ workflowId. THIS IS BILLED.
curl -s -X POST "${host}/sessions/0000000000013500001/workflows" \
  -H "Authorization: Bearer ${tok}" \
  -H "Content-Type: application/json" \
  -d '{"product":{"service":"ADV","model":"ADV-120"},"correlationId":"my-workflow-0001"}'

# 5. workflow.updated arrives at your endpoint, then read the findings
curl -s -X GET "${host}/findings/workflows/wfpop_0000000000002080001/findings" \
  -H "Authorization: Bearer ${tok}"

# 6. lost the handle? search the correlationId you sent when you started it
curl -s -X GET "${host}/workflows?correlationId=my-workflow-0001" \
  -H "Authorization: Bearer ${tok}"

Copyright © Pitchpoint Solutions. All rights reserved.