Skip to main content
This page describes an example workflow of API calls that can be made to clear and triage a submission using the Ping.Vision API. The following is a specific example. There are more options available to the user that are documented in the Ping Vision pages under Ping.Vision. The code blocks allow the user to select from Python or cURL (via terminal) use cases. The Python script is available for download at the bottom of the page. Try filling in the blanks (e.g., {id}) to go through the workflow using cURL!

1. List User Teams

The first API call to make is List User Teams. This endpoint returns the teams associated with your user account, including the team_uuid, division_uuid, and available workflow statuses for each team. Example code:
clear_and_triage.py
import os
import time
from datetime import datetime, timezone
from pathlib import Path

import requests
from pingintel_api.pingvision import types as t

POLL_INTERVAL = 15  # seconds


def color_text(text: str, color: str) -> str:
    """Wrap text in ANSI color codes for terminal output."""
    colors = {"red": "\033[31m", "yellow": "\033[33m", "green": "\033[32m"}
    if color not in colors:
        return text
    reset = "\033[0m"
    return f"{colors[color]}{text}{reset}"


# Configuration
API_KEY = os.environ.get("PINGVISION_AUTH_TOKEN")
BASE_URL = "https://vision.pingintel.com/api/v1"
headers = {"Authorization": f"Token {API_KEY}"}

# File to submit
file_path = Path("demo_email.eml")

files = [("files", (file_path.name, open(file_path, "rb")))]

## ...

# 1. List User Teams
# https://docs.pingintel.com/ping-vision/user-memberships/list-user-teams
list_teams_url = f"{BASE_URL}/user/teams/"
list_teams_response = requests.get(list_teams_url, headers=headers)
if list_teams_response.status_code not in (200, 201):
    raise RuntimeError(f"Failed to list teams: {list_teams_response.status_code}")

teams = list_teams_response.json()
team = teams[0]  # Select team by index, or filter by team_name/company_name
team_uuid = team["team_uuid"]
division_uuid = team["division_uuid"]
status_uuids = {s["name"]: s["uuid"] for s in team.get("statuses", [])}

## ...
Example response:
JSON output
[
  {
    "company_name": "Ping Intel",
    "company_short_name": "PING",
    "company_uuid": "12345678-abc1-cde2-efg3-123456789abc",
    "division_name": "Ping Intel",
    "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
    "id": 1,
    "membership_type": "admin",
    "statuses": [
      {
        "category": "Received",
        "color": "#06A77C",
        "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
        "iconName": "PlaceholderPing",
        "id": 1001,
        "is_valid_for_user_transition": false,
        "name": "Received",
        "uuid": "019c6d26-4468-7b89-8328-d840dc9b5e3b"
      },
      {
        "category": "Initializing",
        "color": "#8239FC",
        "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
        "iconName": "PlaceholderPing",
        "id": 1002,
        "is_valid_for_user_transition": false,
        "name": "Initializing",
        "uuid": "019c6d26-4471-7e1f-a9b3-12acce3b0ba5"
      },
      {
        "category": "Ready",
        "color": "#F16704",
        "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
        "iconName": "PlaceholderPing",
        "id": 1003,
        "is_valid_for_user_transition": true,
        "name": "Waiting for Scrubbing",
        "uuid": "019c6d26-4473-71f0-b372-b2a9349128f4"
      },
      {
        "category": "In progress",
        "color": "#0E85E0",
        "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
        "iconName": "PlaceholderPing",
        "id": 1004,
        "is_valid_for_user_transition": true,
        "name": "Waiting for Peer Review",
        "uuid": "019c6d26-4476-75c3-8a87-ed72a31a36b3"
      },
      {
        "category": "Closed",
        "color": "#FFE523",
        "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
        "iconName": "PlaceholderPing",
        "id": 1005,
        "is_valid_for_user_transition": true,
        "name": "Reject",
        "uuid": "019c6d26-4478-769b-9e91-fd7c2b961959"
      },
      {
        "category": "Completed",
        "color": "#0E85E0",
        "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
        "iconName": "PlaceholderPing",
        "id": 1006,
        "is_valid_for_user_transition": true,
        "name": "Completed",
        "uuid": "019c6d26-447a-7dc5-ac39-ea2251c2f5d5"
      }
    ],
    "team_name": "Ping Intel",
    "team_uuid": "9876543-abc1-cde2-efg3-123456789abcd"
  },
  "..."
]

2. Create Submission

The second API call to make is Initiate New Submission. This endpoint allows the user to upload files (such as .eml or .msg email files, SOVs, or other documents) to create a new submission for processing. Example code:
clear_and_triage.py
## ...

# 2. Create Submission
# https://docs.pingintel.com/ping-vision/create-submission/initiate-new-submission
payload = {
    "client_ref": "my_salesforce_id",  # Optional: external reference ID
    "insured_name": "Acme Corp",  # Optional: insured name
    "team_uuid": team_uuid,  # Required: team ID
}

create_submission_url = f"{BASE_URL}/submission"
create_submission_response = requests.post(create_submission_url, data=payload, files=files, headers=headers)
if create_submission_response.status_code not in (200, 201):
    raise RuntimeError(f"Failed to create submission: {create_submission_response.status_code}")

submission_id = create_submission_response.json()["id"]
print(f"Created submission: {color_text(submission_id, 'green')}")

## ...
Example response:
JSON output
{
  "id": "p-mk-ourke-esw5ab",
  "message": "OK",
  "url": "http://vision.pingintel.com/submission/i/p-mk-ourke-esw5ab"
}

3. Poll for Status Changes via List Submission Events

The third API call to make is List Submission Events. This endpoint allows the user to monitor status changes on the submission by polling for events. Filter by pingid to track a specific submission. To query on page size, team, and division, include those parameters as well. This URL will poll for events related to the provided submission, team, and division starting from February 20, 2026 (the date of this example submission): https://vision.pingintel.com/api/v1/submission-events?pingid={id}&start=20260220000000&page_size=50&team={team_uuid}&division={division_uuid} Example code:
clear_and_triage.py
## ...

# 3. Poll for Status Changes via List Submission Events
# https://docs.pingintel.com/ping-vision/get-submission-data/list-submission-events
earliest_allowed_time = datetime.now(timezone.utc)
last_cursor_id = None
current_status = None
submission_events_url = f"{BASE_URL}/submission-events"
submission_activity_url = f"{BASE_URL}/submission"

print(f"Polling for status changes on submission {color_text(submission_id, 'yellow')}...")
while True:
    events_params = {
        "pingid": submission_id,
        "start": earliest_allowed_time.strftime("%Y%m%d%H%M%S"),
        "page_size": 50,
        "team": team_uuid,
        "division": division_uuid,
    }
    if last_cursor_id:
        events_params["cursor_id"] = last_cursor_id

    events_response = requests.get(submission_events_url, params=events_params, headers=headers)
    if events_response.status_code not in (200, 201):
        print(color_text(f"Error polling events: {events_response.status_code}", "red"))
        time.sleep(POLL_INTERVAL)
        continue

    events_json = events_response.json()
    last_cursor_id = events_json.get("cursor_id")

    # Process status change events
    for event in events_json.get("results", []):
        if event.get("event_type") == t.SUBMISSION_EVENT_LOG_TYPE.SUBMISSION_STATUS_CHANGE:
            old_status = event.get("old_value", "")
            new_status = event.get("new_value", "")
            print(
                color_text(f"Status change: {event.get('message', '')}", "green"),
                old_status,
                "->",
                new_status,
            )
            current_status = new_status

## ...
Example response:
JSON output
{
  "cursor_id": "019c7d25-8290-7e4b-a094-1c880f73c534",
  "results": [
    {
      "created_time": "2026-02-20T22:22:16.111319Z",
      "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
      "event_type": "SFRR",
      "message": "SOV Fixer results were received",
      "new_value": null,
      "old_value": null,
      "pingid": "p-mk-ourke-esw5ab",
      "user_id": 28,
      "uuid": "019c7d25-822f-72b6-a78c-0eafb4040206"
    },
    {
      "created_time": "2026-02-20T22:22:16.208950Z",
      "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
      "event_type": "SSC",
      "message": "Submission status was changed from Initializing to Pending Clearance",
      "new_value": "019c6d26-4e1d-7b8a-bad1-c05ed808dc4d",
      "old_value": "019c6d26-4e1f-763c-ba7e-b6149e693a23",
      "pingid": "p-mk-ourke-esw5ab",
      "user_id": 34,
      "uuid": "019c7d25-8290-7e4b-a094-1c880f73c534"
    },
    "..."
  ]
}

4. Change Submission Status

The fourth API call to make is Change Submission Status. This endpoint allows the user to programmatically transition the submission through workflow stages by providing the submission ID and the target workflow_status_uuid. E.g., https://vision.pingintel.com/api/v1/submission/pv-acme-abc123/change_status Example code:
clear_and_triage.py
## ...

    # 4. Change Submission Status
    # https://docs.pingintel.com/ping-vision/update-submission/change-submission-status
    change_status_url = f"{BASE_URL}/submission/{submission_id}/change_status"

    if current_status == status_uuids.get("Pending Clearance"):
        # In a real workflow, the clearance team would evaluate the submission
        # for factors like incumbents or duplicate submissions from other brokerages.
        # For this example, we will just automatically clear it to demonstrate the workflow.
        print(color_text("Changing status to Cleared...", "yellow"))
        response = requests.patch(
            change_status_url,
            json={"workflow_status_uuid": status_uuids["Cleared"]},
            headers=headers,
        )
        if response.status_code not in (200, 201):
            raise RuntimeError(f"Failed to change status: {response.status_code}")
        print(color_text("Status changed to: Cleared", "green"))

    if current_status == status_uuids.get("Cleared"):
        # In a real workflow, this transition happens when the DTI (Days To Inception) date
        # is reached, or when manually requested to begin the scrubbing/data entry process.
        # For this example, we will just automatically move it to Data Entry to demonstrate the workflow.
        print(color_text("Changing status to Data Entry...", "yellow"))
        response = requests.patch(
            change_status_url,
            json={"workflow_status_uuid": status_uuids["Data Entry"]},
            headers=headers,
        )
        if response.status_code not in (200, 201):
            raise RuntimeError(f"Failed to change status: {response.status_code}")
        print(color_text("Status changed to: Data Entry", "green"))
        break

    time.sleep(POLL_INTERVAL)
    current_status_name = next((k for k, v in status_uuids.items() if v == current_status), None)
    print(f"Waiting... (current status: {color_text(current_status_name, 'yellow')})")

## ...
Example response:
JSON output
{
  "status": "success"
}

5. Download Scrubber for Human Certification

The fifth API call to make is List Recent Submission Activity. This endpoint retrieves submission details including the documents list. Use this to find and download the SOVFIXER_SCRUBBER document for human certification. E.g., https://vision.pingintel.com/api/v1/submission?id=pv-acme-abc123 Example code:
clear_and_triage.py
## ...

# 5. Download Scrubber for Human Certification
# https://docs.pingintel.com/ping-vision/get-submission-data/list-recent-submission-activity
print(color_text("\nPress enter to download the scrubber for certification.", "red"))
input("> ")

activity_params = {
    "id": submission_id,
    "page_size": 1,
    "team_uuid": team_uuid,
    "division_uuid": division_uuid,
}
activity_response = requests.get(submission_activity_url, params=activity_params, headers=headers)
if activity_response.status_code not in (200, 201):
    raise RuntimeError(f"Failed to get submission activity: {activity_response.status_code}")

activity_json = activity_response.json()
submission_data = activity_json.get("results", [])[0]
documents = submission_data.get("documents", [])

scrubber_doc = None
for doc in documents:
    if doc.get("document_type") == "SOVFIXER_SCRUBBER":
        scrubber_doc = doc
        break
if not scrubber_doc:
    raise RuntimeError("Scrubber document not found")

scrubber_filename = f"{submission_id}_{scrubber_doc['filename']}"
scrubber_response = requests.get(scrubber_doc["url"], headers=headers)
if scrubber_response.status_code not in (200, 201):
    raise RuntimeError(f"Failed to download scrubber: {scrubber_response.status_code}")

with open(scrubber_filename, "wb") as f:
    f.write(scrubber_response.content)
print(color_text(f"Downloaded scrubber to: {scrubber_filename}", "green"))
print(
    color_text(
        "Open in Excel and certify the scrubber in the Ping menu, then return here.",
        "yellow",
    )
)

## ...
Example response:
JSON output
{
  "cursor_id": "p-mk-ourke-esw5ab",
  "has_remaining": false,
  "results": [
    {
      "actions": {
        "claim": true,
        "download_documents": true,
        "transition_to": {
          "019c6d26-4e1b-7c15-84d5-a3d5a3f7583f": "Delivered",
          "019c6d26-4e1d-7b8a-bad1-c05ed808dc4d": "Pending Clearance",
          "019c6d26-4e1f-763c-ba7e-b6149e693a23": "Initializing",
          "019c6d26-4e24-73d5-a2f0-ac50f422ba51": "Underwriting",
          "019c6d26-4e26-751a-ab3f-9ae93b0ce114": "Blocked",
          "019c77df-548f-7628-952e-eee975776593": "Cleared"
        },
        "view_map": true
      },
      "automated_processing_failed": false,
      "automated_processing_failed_reason": null,
      "broker_assistant_name": null,
      "...": "...",
      "documents": [
        {
          "actions": ["download", "archive"],
          "archived_on": null,
          "archived_reason": null,
          "created_time": "2026-02-20T22:21:22.216034Z",
          "document_type": "MSG",
          "filename": "demo_email.eml",
          "id": 625,
          "is_archived": false,
          "label": "MSG",
          "preview_url": null,
          "url": "http://vision.pingintel.com/api/v1/submission/p-mk-ourke-esw5ab/document/demo_email.eml"
        },
        "..."
      ],
      "is_submission_ping_certified": true,
      "jobs": [
        {
          "created_time": "2026-02-20T22:21:26.655911Z",
          "filenames": ["parse_sov_testfile.xlsx", "Email - 02-20-2026.content.html"],
          "job_id": "7f3444de-0eaa-11f1-ba93-befce6b35b80",
          "job_type": "SOVFIXER",
          "job_type_details": {
            "job_type_label": "Ping.Extraction",
            "sovfixer_request_type": null,
            "sovfixer_result_message": null,
            "sovfixer_result_status": null,
            "sovfixer_sovid": "s-mk-ourke-g1bnkd"
          },
          "processing_last_message": "Processing complete.",
          "processing_pct_complete": 100.0,
          "processing_status": "C",
          "sovid": "s-mk-ourke-g1bnkd",
          "updated_time": "2026-02-20T22:22:16.102475Z",
          "user_id": 28
        }
      ],
      "...": "..."
    }
  ]
}

6. Certify Scrubber in Excel

After downloading the scrubber, open it in Excel. Use the Ping menu in Excel to review the extracted data and certify the submission.

7. Wait for Underwriting Status (Human Certification Complete)

After the human operator reviews and certifies the scrubber in Excel using the Ping menu, continue polling List Submission Events until the submission reaches the “Underwriting” status. To query on page size, team, and division, include those parameters as well. This URL will poll for events related to the provided submission, team, and division starting from February 20, 2026 (the date of this example submission): https://vision.pingintel.com/api/v1/submission-events?pingid={id}&start=20260220000000&page_size=50&team={team_uuid}&division={division_uuid} Example code:
clear_and_triage.py
## ...

# 7. Wait for Underwriting Status (Human Certification Complete)
print(color_text("\nWaiting for human certification (Underwriting status)...", "yellow"))

while True:
    events_params = {
        "pingid": submission_id,
        "start": earliest_allowed_time.strftime("%Y%m%d%H%M%S"),
        "page_size": 50,
        "team": team_uuid,
        "division": division_uuid,
    }
    if last_cursor_id:
        events_params["cursor_id"] = last_cursor_id

    events_response = requests.get(submission_events_url, params=events_params, headers=headers)
    if events_response.status_code not in (200, 201):
        print(color_text(f"Error polling events: {events_response.status_code}", "red"))
        time.sleep(POLL_INTERVAL)
        continue

    events_json = events_response.json()
    last_cursor_id = events_json.get("cursor_id")

    for event in events_json.get("results", []):
        if event.get("event_type") == t.SUBMISSION_EVENT_LOG_TYPE.SUBMISSION_STATUS_CHANGE:
            print(color_text(f"Status change: {event.get('message', '')}", "green"))
            current_status = event.get("new_value", "")

    if current_status == status_uuids.get("Underwriting"):
        print(color_text("Certification complete. Underwriting status reached.", "green"))
        break

    time.sleep(POLL_INTERVAL)
    current_status_name = next((k for k, v in status_uuids.items() if v == current_status), None)
    print(f"Waiting for certification... (current status: {color_text(current_status_name or 'unknown', 'yellow')})")

## ...
Example response:
JSON output
{
  "cursor_id": "019c7d3e-dd84-74f1-a7a1-cc41ee75ad8e",
  "results": [
    {
      "created_time": "2026-02-20T22:49:57.562391Z",
      "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
      "event_type": "SFUR",
      "message": "SOV Fixer update received",
      "new_value": null,
      "old_value": null,
      "pingid": "p-mk-ourke-esw5ab",
      "user_id": 34,
      "uuid": "019c7d3e-dc3a-7870-be37-c9ddedf31142"
    },
    {
      "created_time": "2026-02-20T22:49:57.661324Z",
      "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
      "event_type": "SSC",
      "message": "Submission status was changed from Data Entry to Underwriting",
      "new_value": "019c6d26-4e24-73d5-a2f0-ac50f422ba51",
      "old_value": "019c6d26-4e21-784d-bd26-9d7d70991d91",
      "pingid": "p-mk-ourke-esw5ab",
      "user_id": 34,
      "uuid": "019c7d3e-dc9d-73a1-9d3f-241126fef6b6"
    },
    {
      "created_time": "2026-02-20T22:49:57.758105Z",
      "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
      "event_type": "ROUT",
      "message": "Ran outputters",
      "new_value": null,
      "old_value": null,
      "pingid": "p-mk-ourke-esw5ab",
      "user_id": 34,
      "uuid": "019c7d3e-dcfe-79c0-bf51-5e4a76ef0462"
    },
    {
      "created_time": "2026-02-20T22:49:57.892794Z",
      "division_uuid": "12345678-abc1-cde2-efg3-123456789abcd",
      "event_type": "SFUR",
      "message": "SOV Fixer update received",
      "new_value": null,
      "old_value": null,
      "pingid": "p-mk-ourke-esw5ab",
      "user_id": 34,
      "uuid": "019c7d3e-dd84-74f1-a7a1-cc41ee75ad8e"
    }
  ]
}

8. Wait for the RUN_OUTPUTTERS Job to Complete

After certification, poll List Recent Submission Activity until the RUN_OUTPUTTERS job reaches 100% completion. This job generates the final output documents. Example code:
clear_and_triage.py
## ...

# 8. Wait for the RUN_OUTPUTTERS job to complete
print(color_text("Waiting for final outputs...", "yellow"))
while True:
    activity_params = {
        "id": submission_id,
        "page_size": 1,
        "team_uuid": team_uuid,
        "division_uuid": division_uuid,
    }
    activity_response = requests.get(submission_activity_url, params=activity_params, headers=headers)
    if activity_response.status_code not in (200, 201):
        print(color_text(f"Error checking activity: {activity_response.status_code}", "red"))
        time.sleep(POLL_INTERVAL)
        continue

    results = activity_response.json().get("results", [])
    if not results:
        time.sleep(POLL_INTERVAL)
        continue

    submission = results[0]
    jobs = submission.get("jobs", [])
    outputters_job = next((j for j in jobs if j.get("job_type") == "RUN_OUTPUTTERS"), None)

    if outputters_job and outputters_job.get("processing_pct_complete") == 100:
        print(color_text("Final outputs ready for download.", "green"))
        break

    time.sleep(POLL_INTERVAL)

## ...
Example response:
JSON output
{
  "cursor_id": "p-mk-ourke-esw5ab",
  "has_remaining": false,
  "results": [
    {
      "actions": {
        "claim": true,
        "download_documents": true,
        "transition_to": {
          "019c6d26-4e1b-7c15-84d5-a3d5a3f7583f": "Delivered",
          "019c6d26-4e1d-7b8a-bad1-c05ed808dc4d": "Pending Clearance",
          "019c6d26-4e1f-763c-ba7e-b6149e693a23": "Initializing",
          "019c6d26-4e24-73d5-a2f0-ac50f422ba51": "Underwriting",
          "019c6d26-4e26-751a-ab3f-9ae93b0ce114": "Blocked",
          "019c77df-548f-7628-952e-eee975776593": "Cleared"
        },
        "view_map": true
      },
      "automated_processing_failed": false,
      "automated_processing_failed_reason": null,
      "broker_assistant_name": null,
      "...": "...",
      "documents": [
        {
          "actions": ["download", "archive"],
          "archived_on": null,
          "archived_reason": null,
          "created_time": "2026-02-20T22:21:22.216034Z",
          "document_type": "MSG",
          "filename": "demo_email.eml",
          "id": 625,
          "is_archived": false,
          "label": "MSG",
          "preview_url": null,
          "url": "http://vision.pingintel.com/api/v1/submission/p-mk-ourke-esw5ab/document/demo_email.eml"
        },
        "..."
      ],
      "is_submission_ping_certified": true,
      "jobs": [
        {
          "created_time": "2026-02-20T22:21:26.655911Z",
          "filenames": ["parse_sov_testfile.xlsx", "Email - 02-20-2026.content.html"],
          "job_id": "7f3444de-0eaa-11f1-ba93-befce6b35b80",
          "job_type": "SOVFIXER",
          "job_type_details": {
            "job_type_label": "Ping.Extraction",
            "sovfixer_request_type": null,
            "sovfixer_result_message": null,
            "sovfixer_result_status": null,
            "sovfixer_sovid": "s-mk-ourke-g1bnkd"
          },
          "processing_last_message": "Processing complete.",
          "processing_pct_complete": 100.0,
          "processing_status": "C",
          "sovid": "s-mk-ourke-g1bnkd",
          "updated_time": "2026-02-20T22:22:16.102475Z",
          "user_id": 28
        }
      ],
      "...": "..."
    }
  ]
}

9. Download Final Output Documents

The final API call to make is Download Submission Document. This endpoint allows the user to download the processed output documents by providing the submission ID and the document filename. Use the documents list from the polling response to identify which documents to download, filtering by document_type. E.g., https://vision.pingintel.com/api/v1/submission/pv-acme-abc123/document/My Example SOV.json Example code:
clear_and_triage.py
## ...

# 9. Download Final Output Documents
# https://docs.pingintel.com/ping-vision/get-submission-data/download-submission-document
os.makedirs("workflow_example_results", exist_ok=True)

for doc in submission.get("documents", []):
    doc_type = doc.get("document_type", "")
    filename = doc.get("filename", "")

    if doc_type in ["SOVFIXER_OUTPUT", "SOVFIXER_JSON"] and not doc.get("is_archived", False):
        download_url = f"{BASE_URL}/submission/{submission_id}/document/{filename}"
        download_response = requests.get(download_url, headers=headers)
        if download_response.status_code not in (200, 201):
            raise RuntimeError(f"Failed to download {filename}: {download_response.status_code}")

        output_path = f"workflow_example_results/{submission_id}_{filename}"
        with open(output_path, "wb") as outfile:
            outfile.write(download_response.content)
        print(color_text(f"Saved {doc_type}: {output_path}", "green"))

print(color_text(f"\nWorkflow complete for submission {submission_id}", "green"))

##
Example response:
Python output
Saved SOVFIXER_OUTPUT: workflow_example_results/p-mk-ourke-esw5ab_Acme Corp 2026-02-PING.xlsm
Saved SOVFIXER_OUTPUT: workflow_example_results/p-mk-ourke-esw5ab_Ping Marketing SOV - Acme Corp.xlsx
Saved SOVFIXER_JSON: workflow_example_results/p-mk-ourke-esw5ab_Acme Corp 2026-02.json

Workflow complete for submission p-mk-ourke-esw5ab

Python Demo

                 Download Python Script here ||| Download Example Email here
clear_and_triage_api.py
import os
import time
from datetime import datetime, timezone
from pathlib import Path

import requests
from pingintel_api.pingvision import types as t

POLL_INTERVAL = 15  # seconds


def color_text(text: str, color: str) -> str:
    """Wrap text in ANSI color codes for terminal output."""
    colors = {"red": "\033[31m", "yellow": "\033[33m", "green": "\033[32m"}
    if color not in colors:
        return text
    reset = "\033[0m"
    return f"{colors[color]}{text}{reset}"


# Configuration
API_KEY = os.environ.get("PINGVISION_AUTH_TOKEN")
BASE_URL = "https://vision.pingintel.com/api/v1"
headers = {"Authorization": f"Token {API_KEY}"}

# File to submit
file_path = Path("demo_email.eml")

files = [("files", (file_path.name, open(file_path, "rb")))]

## ...

# 1. List User Teams
# https://docs.pingintel.com/ping-vision/user-memberships/list-user-teams
list_teams_url = f"{BASE_URL}/user/teams/"
list_teams_response = requests.get(list_teams_url, headers=headers)
if list_teams_response.status_code not in (200, 201):
    raise RuntimeError(f"Failed to list teams: {list_teams_response.status_code}")

teams = list_teams_response.json()
team = teams[0]  # Select team by index, or filter by team_name/company_name
team_uuid = team["team_uuid"]
division_uuid = team["division_uuid"]
status_uuids = {s["name"]: s["uuid"] for s in team.get("statuses", [])}

## ...

# 2. Create Submission
# https://docs.pingintel.com/ping-vision/create-submission/initiate-new-submission
payload = {
    "client_ref": "my_salesforce_id",  # Optional: external reference ID
    "insured_name": "Acme Corp",  # Optional: insured name
    "team_uuid": team_uuid,  # Required: team ID
}

create_submission_url = f"{BASE_URL}/submission"
create_submission_response = requests.post(create_submission_url, data=payload, files=files, headers=headers)
if create_submission_response.status_code not in (200, 201):
    raise RuntimeError(f"Failed to create submission: {create_submission_response.status_code}")

submission_id = create_submission_response.json()["id"]
print(f"Created submission: {color_text(submission_id, 'green')}")

## ...

# 3. Poll for Status Changes via List Submission Events
# https://docs.pingintel.com/ping-vision/get-submission-data/list-submission-events
earliest_allowed_time = datetime.now(timezone.utc)
last_cursor_id = None
current_status = None
submission_events_url = f"{BASE_URL}/submission-events"
submission_activity_url = f"{BASE_URL}/submission"

print(f"Polling for status changes on submission {color_text(submission_id, 'yellow')}...")
while True:
    events_params = {
        "pingid": submission_id,
        "start": earliest_allowed_time.strftime("%Y%m%d%H%M%S"),
        "page_size": 50,
        "team": team_uuid,
        "division": division_uuid,
    }
    if last_cursor_id:
        events_params["cursor_id"] = last_cursor_id

    events_response = requests.get(submission_events_url, params=events_params, headers=headers)
    if events_response.status_code not in (200, 201):
        print(color_text(f"Error polling events: {events_response.status_code}", "red"))
        time.sleep(POLL_INTERVAL)
        continue

    events_json = events_response.json()
    last_cursor_id = events_json.get("cursor_id")

    # Process status change events
    for event in events_json.get("results", []):
        if event.get("event_type") == t.SUBMISSION_EVENT_LOG_TYPE.SUBMISSION_STATUS_CHANGE:
            old_status = event.get("old_value", "")
            new_status = event.get("new_value", "")
            print(
                color_text(f"Status change: {event.get('message', '')}", "green"),
                old_status,
                "->",
                new_status,
            )
            current_status = new_status

    # 4. Change Submission Status
    # https://docs.pingintel.com/ping-vision/update-submission/change-submission-status
    change_status_url = f"{BASE_URL}/submission/{submission_id}/change_status"

    if current_status == status_uuids.get("Pending Clearance"):
        # In a real workflow, the clearance team would evaluate the submission
        # for factors like incumbents or duplicate submissions from other brokerages.
        # For this example, we will just automatically clear it to demonstrate the workflow.
        print(color_text("Changing status to Cleared...", "yellow"))
        response = requests.patch(
            change_status_url,
            json={"workflow_status_uuid": status_uuids["Cleared"]},
            headers=headers,
        )
        if response.status_code not in (200, 201):
            raise RuntimeError(f"Failed to change status: {response.status_code}")
        print(color_text("Status changed to: Cleared", "green"))

    if current_status == status_uuids.get("Cleared"):
        # In a real workflow, this transition happens when the DTI (Days To Inception) date
        # is reached, or when manually requested to begin the scrubbing/data entry process.
        # For this example, we will just automatically move it to Data Entry to demonstrate the workflow.
        print(color_text("Changing status to Data Entry...", "yellow"))
        response = requests.patch(
            change_status_url,
            json={"workflow_status_uuid": status_uuids["Data Entry"]},
            headers=headers,
        )
        if response.status_code not in (200, 201):
            raise RuntimeError(f"Failed to change status: {response.status_code}")
        print(color_text("Status changed to: Data Entry", "green"))
        break

    time.sleep(POLL_INTERVAL)
    current_status_name = next((k for k, v in status_uuids.items() if v == current_status), None)
    print(f"Waiting... (current status: {color_text(current_status_name, 'yellow')})")

## ...

# 5. Download Scrubber for Human Certification
# https://docs.pingintel.com/ping-vision/get-submission-data/list-recent-submission-activity
print(color_text("\nPress enter to download the scrubber for certification.", "red"))
input("> ")

activity_params = {
    "id": submission_id,
    "page_size": 1,
    "team_uuid": team_uuid,
    "division_uuid": division_uuid,
}
activity_response = requests.get(submission_activity_url, params=activity_params, headers=headers)
if activity_response.status_code not in (200, 201):
    raise RuntimeError(f"Failed to get submission activity: {activity_response.status_code}")

activity_json = activity_response.json()
submission_data = activity_json.get("results", [])[0]
documents = submission_data.get("documents", [])

scrubber_doc = None
for doc in documents:
    if doc.get("document_type") == "SOVFIXER_SCRUBBER":
        scrubber_doc = doc
        break
if not scrubber_doc:
    raise RuntimeError("Scrubber document not found")

scrubber_filename = f"{submission_id}_{scrubber_doc['filename']}"
scrubber_response = requests.get(scrubber_doc["url"], headers=headers)
if scrubber_response.status_code not in (200, 201):
    raise RuntimeError(f"Failed to download scrubber: {scrubber_response.status_code}")

with open(scrubber_filename, "wb") as f:
    f.write(scrubber_response.content)
print(color_text(f"Downloaded scrubber to: {scrubber_filename}", "green"))
print(
    color_text(
        "Open in Excel and certify the scrubber in the Ping menu, then return here.",
        "yellow",
    )
)

## ...

# 6. After downloading the scrubber, open it in Excel. Use the Ping menu in Excel to review the extracted data and certify the submission.

## ...

# 7. Wait for Underwriting Status (Human Certification Complete)
print(color_text("\nWaiting for human certification (Underwriting status)...", "yellow"))

while True:
    events_params = {
        "pingid": submission_id,
        "start": earliest_allowed_time.strftime("%Y%m%d%H%M%S"),
        "page_size": 50,
        "team": team_uuid,
        "division": division_uuid,
    }
    if last_cursor_id:
        events_params["cursor_id"] = last_cursor_id

    events_response = requests.get(submission_events_url, params=events_params, headers=headers)
    if events_response.status_code not in (200, 201):
        print(color_text(f"Error polling events: {events_response.status_code}", "red"))
        time.sleep(POLL_INTERVAL)
        continue

    events_json = events_response.json()
    last_cursor_id = events_json.get("cursor_id")

    for event in events_json.get("results", []):
        if event.get("event_type") == t.SUBMISSION_EVENT_LOG_TYPE.SUBMISSION_STATUS_CHANGE:
            print(color_text(f"Status change: {event.get('message', '')}", "green"))
            current_status = event.get("new_value", "")

    if current_status == status_uuids.get("Underwriting"):
        print(color_text("Certification complete. Underwriting status reached.", "green"))
        break

    time.sleep(POLL_INTERVAL)
    current_status_name = next((k for k, v in status_uuids.items() if v == current_status), None)
    print(f"Waiting for certification... (current status: {color_text(current_status_name or 'unknown', 'yellow')})")

## ...

# 8. Wait for the RUN_OUTPUTTERS job to complete
print(color_text("Waiting for final outputs...", "yellow"))
while True:
    activity_params = {
        "id": submission_id,
        "page_size": 1,
        "team_uuid": team_uuid,
        "division_uuid": division_uuid,
    }
    activity_response = requests.get(submission_activity_url, params=activity_params, headers=headers)
    if activity_response.status_code not in (200, 201):
        print(color_text(f"Error checking activity: {activity_response.status_code}", "red"))
        time.sleep(POLL_INTERVAL)
        continue

    results = activity_response.json().get("results", [])
    if not results:
        time.sleep(POLL_INTERVAL)
        continue

    submission = results[0]
    jobs = submission.get("jobs", [])
    outputters_job = next((j for j in jobs if j.get("job_type") == "RUN_OUTPUTTERS"), None)

    if outputters_job and outputters_job.get("processing_pct_complete") == 100:
        print(color_text("Final outputs ready for download.", "green"))
        break

    time.sleep(POLL_INTERVAL)

## ...

# 9. Download Final Output Documents
# https://docs.pingintel.com/ping-vision/get-submission-data/download-submission-document
os.makedirs("workflow_example_results", exist_ok=True)

for doc in submission.get("documents", []):
    doc_type = doc.get("document_type", "")
    filename = doc.get("filename", "")

    if doc_type in ["SOVFIXER_OUTPUT", "SOVFIXER_JSON"] and not doc.get("is_archived", False):
        download_url = f"{BASE_URL}/submission/{submission_id}/document/{filename}"
        download_response = requests.get(download_url, headers=headers)
        if download_response.status_code not in (200, 201):
            raise RuntimeError(f"Failed to download {filename}: {download_response.status_code}")

        output_path = f"workflow_example_results/{submission_id}_{filename}"
        with open(output_path, "wb") as outfile:
            outfile.write(download_response.content)
        print(color_text(f"Saved {doc_type}: {output_path}", "green"))

print(color_text(f"\nWorkflow complete for submission {submission_id}", "green"))

##