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

# Send and Track SOV Submission

> Example workflow for submitting and tracking an SOV via the **Ping.Vision** API

This page describes an example workflow of API calls that can be made to send and track an SOV submission using the **Ping.Vision** API. Reach for this workflow when one of the following is true:

* You want to push a submission — an email with an SOV attachment, or the SOV file directly — into Ping.Vision and watch it advance through the workflow.
* You need to listen for status-change events and download the generated output documents once processing completes.
* You're building a minimal, single-submission integration before adding clearance, triage, or status-driven branching logic on your side.

For a full integration where Ping clears submissions against appetite and underwriting rules and your system reacts to those status transitions, see [How to: Integrate an External System](/workflows/ping-vision/clear-and-triage). It builds on this workflow with the additional event handling and decision points a production integration needs.

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!

This workflow demonstrates how to:

* Create a submission
* Poll for job completion
* Download the final output documents

### 1. List User Teams

The first API call to make is [List User Teams](/ping-vision/user-memberships/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:**

<CodeGroup dropdown>
  ```python send_and_track_submission.py lines icon='python' theme={null}
  import os
  import time
  from pathlib import Path

  import requests
  from pingintel_api.pingvision import types as t

  POLL_INTERVAL = 10  # 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()
  # select team by team_uuid or division_uuid
  team = teams[0]
  team_uuid = team["team_uuid"]  # Unique identifier for the team
  division_uuid = team["division_uuid"]  # Division within the team

  # Build a mapping of workflow status names to UUIDs for later use
  status_uuids = {s["name"]: s["uuid"] for s in team.get("statuses", [])}

  ## ...
  ```

  ```shell cURL output icon='square-terminal' theme={null}
  curl --request GET \
      --url "https://vision.pingintel.com/api/v1/user/teams/" \
      --header "Authorization: Token <your_api_key_here>"
  ```
</CodeGroup>

**Example response:**

<CodeGroup dropdown>
  ```json JSON output lines icon='json' theme={null}
  [
    {
      "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"
        },
        "..."
      ],
      "team_name": "Ping Intel",
      "team_uuid": "9876543-abc1-cde2-efg3-123456789abcd"
    },
    "..."
  ]
  ```
</CodeGroup>

### 2. Create Submission

The second API call to make is [Initiate New Submission](/ping-vision/create-submission/initiate-new-submission).
This endpoint allows the user to upload files to create a new submission for processing.

**Example code:**

<CodeGroup dropdown>
  ```python send_and_track_submission.py lines icon='python' theme={null}
  ## ...

  # Step 2. Create Submission
  # https://docs.pingintel.com/ping-vision/create-submission/initiate-new-submission

  payload = {
      "client_ref": "my_salesforce_id",  # Optional: your external reference ID
      "insured_name": "Acme Corp",  # Optional: name of the insured party
      "team_uuid": team_uuid,  # Required: which team should receive this submission
  }

  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')}")

  ## ...
  ```

  ```shell cURL output icon='square-terminal' theme={null}
  curl --request POST \
      --url "https://vision.pingintel.com/api/v1/submission" \
      --header "Authorization: Token <your_api_key_here>" \
      --header "Content-Type: multipart/form-data" \
      --form "files=@demo_email.eml" \
      --form "team_uuid=<your_team_uuid_here>" \
      --form "client_ref=my_salesforce_id" \
      --form "insured_name=Acme Corp"
  ```
</CodeGroup>

**Example response:**

<CodeGroup dropdown>
  ```json JSON output lines icon='json' theme={null}
  {
    "id": "p-mk-ourke-esw5ab",
    "message": "OK",
    "url": "http://vision.pingintel.com/submission/i/p-mk-ourke-esw5ab"
  }
  ```
</CodeGroup>

### 3. Poll for Job Completion

After submission, poll [List Recent Submission Activity](/ping-vision/get-submission-data/list-recent-submission-activity)
to monitor job progress. Ping Vision processes the document through multiple jobs:

* **SOVFIXER**: Parses and validates the SOV data, geocodes addresses, enriches with third-party data
* **RUN\_OUTPUTTERS**: Generates final output files in configured formats

**Example code:**

<CodeGroup dropdown>
  ```python send_and_track_submission.py lines icon='python' theme={null}
  ## ...

  # Step 3. Poll for Job Completion

  submission_activity_url = f"{BASE_URL}/submission"

  print(color_text("Polling for jobs to complete...", "yellow"))

  # track completed jobs
  completed_jobs = set()

  while True:
      # Query for the specific submission by ID
      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:
          print(color_text("Waiting for submission data...", "yellow"))
          time.sleep(POLL_INTERVAL)
          continue

      submission = results[0]
      jobs = submission.get("jobs", [])

      # Display progress for each job
      for j in jobs:
          job_id = j.get("job_id")
          job_type = j.get("job_type", "UNKNOWN")  # e.g., "SOVFIXER", "RUN_OUTPUTTERS"
          pct = j.get("processing_pct_complete", 0)
          status = j.get("processing_status", "?")
          message = j.get("processing_last_message", "")  # Human-readable status message

          if status == "C" and job_id not in completed_jobs:
              # job completed
              print(color_text(f"✓ {job_type}: {pct:.0f}% - {message}", "green"))
              completed_jobs.add(job_id)
          elif status != "C" and job_id not in completed_jobs:
              # job still in progress
              print(color_text(f"⋯ {job_type}: {pct:.0f}% - {message}", "yellow"))

      # Check if the final outputters job is complete
      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)

  ## ...
  ```

  ```shell cURL output icon='square-terminal' theme={null}
  # Poll until jobs complete
  curl --request GET \
      --url "https://vision.pingintel.com/api/v1/submission?id={id}&page_size=1&team_uuid={team_uuid}&division_uuid={division_uuid}" \
      --header "Authorization: Token <your_api_key_here>"
  ```
</CodeGroup>

**Example response:**

<CodeGroup dropdown>
  ```json JSON output lines icon='json' theme={null}
  {
    "cursor_id": "p-mk-ourke-esw5ab",
    "has_remaining": false,
    "results": [
      {
        "actions": {
          "claim": true,
          "download_documents": true,
          "view_map": true
        },
        "automated_processing_failed": false,
        "automated_processing_failed_reason": 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"
          },
          {
            "actions": ["download", "archive"],
            "document_type": "SOVFIXER_OUTPUT",
            "filename": "Ping Marketing SOV - Acme Corp.xlsx",
            "is_archived": false,
            "url": "http://vision.pingintel.com/api/v1/submission/p-mk-ourke-esw5ab/document/Ping%20Marketing%20SOV%20-%20Acme%20Corp.xlsx"
          },
          {
            "actions": ["download", "archive"],
            "document_type": "SOVFIXER_JSON",
            "filename": "parse_sov_testfile-20260220222216.json",
            "is_archived": false,
            "url": "http://vision.pingintel.com/api/v1/submission/p-mk-ourke-esw5ab/document/parse_sov_testfile-20260220222216.json"
          },
          "..."
        ],
        "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
          },
          {
            "job_id": "8a4555ef-1fbb-22g2-cb94-cgadf7c46c91",
            "job_type": "RUN_OUTPUTTERS",
            "processing_last_message": "Processing complete.",
            "processing_pct_complete": 100.0,
            "processing_status": "C"
          }
        ],
        "...": "..."
      }
    ]
  }
  ```
</CodeGroup>

### 4. Download Final Output Documents

The final API call to make is [Download Submission Document](/ping-vision/get-submission-data/download-submission-document).
This endpoint allows the user to download the processed output documents by providing the submission ID and the document filename.

Once processing is complete, the submission contains output documents:

* **SOVFIXER\_OUTPUT**: The scrubbed/normalized SOV file (Excel format)
* **SOVFIXER\_JSON**: Machine-readable JSON with all extracted and enriched data

E.g., `https://vision.pingintel.com/api/v1/submission/p-mk-ourke-esw5ab/document/parse_sov_testfile-20260220222216.json`

**Example code:**

<CodeGroup dropdown>
  ```python send_and_track_submission.py lines icon='python' theme={null}
  ## ...

  # Step 4. 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", "")

      # Download only the final output files
      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}")

          # Save with submission ID prefix to avoid filename collisions
          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"))

  ##
  ```

  ```shell cURL output icon='square-terminal' theme={null}
  # For each document in the documents list with document_type "SOVFIXER_OUTPUT" or "SOVFIXER_JSON":
  curl --request GET \
      --url "https://vision.pingintel.com/api/v1/submission/{id}/document/{filename}" \
      --header "Authorization: Token <your_api_key_here>" \
      --output "{filename}"
  ```
</CodeGroup>

**Example response:**

<CodeGroup dropdown>
  ```python Python output lines icon='python' theme={null}
  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_parse_sov_testfile-20260220222216.json

  Workflow complete for submission p-mk-ourke-esw5ab
  ```

  ```shell cURL output icon='square-terminal' theme={null}
  # Files downloaded to current directory:
  # - Ping Marketing SOV - Acme Corp.xlsx
  # - parse_sov_testfile-20260220222216.json
  ```
</CodeGroup>

### Python Demo

           
    
<a href="https://github.com/pingintel/pingintel-api/blob/main/examples/ping-vision/send_and_track_submission.py">Download Python Script here</a>
|||
<a href="https://github.com/pingintel/pingintel-api/blob/main/examples/data/demo_email.eml">Download Example Email here</a>

<CodeGroup dropdown>
  ```python send_and_track_submission.py lines icon='python' theme={null}
  import os
  import time
  from pathlib import Path

  import requests
  from pingintel_api.pingvision import types as t

  POLL_INTERVAL = 10  # 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()
  # select team by team_uuid or division_uuid
  team = teams[0]
  team_uuid = team["team_uuid"]  # Unique identifier for the team
  division_uuid = team["division_uuid"]  # Division within the team

  # Build a mapping of workflow status names to UUIDs for later use
  status_uuids = {s["name"]: s["uuid"] for s in team.get("statuses", [])}

  ## ...

  # Step 2. Create Submission
  # https://docs.pingintel.com/ping-vision/create-submission/initiate-new-submission

  payload = {
      "client_ref": "my_salesforce_id",  # Optional: your external reference ID
      "insured_name": "Acme Corp",  # Optional: name of the insured party
      "team_uuid": team_uuid,  # Required: which team should receive this submission
  }

  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')}")

  ## ...

  # Step 3. Poll for Job Completion

  submission_activity_url = f"{BASE_URL}/submission"

  print(color_text("Polling for jobs to complete...", "yellow"))

  # track completed jobs
  completed_jobs = set()

  while True:
      # Query for the specific submission by ID
      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:
          print(color_text("Waiting for submission data...", "yellow"))
          time.sleep(POLL_INTERVAL)
          continue

      submission = results[0]
      jobs = submission.get("jobs", [])

      # Display progress for each job
      for j in jobs:
          job_id = j.get("job_id")
          job_type = j.get("job_type", "UNKNOWN")  # e.g., "SOVFIXER", "RUN_OUTPUTTERS"
          pct = j.get("processing_pct_complete", 0)
          status = j.get("processing_status", "?")
          message = j.get("processing_last_message", "")  # Human-readable status message

          if status == "C" and job_id not in completed_jobs:
              # job completed
              print(color_text(f"✓ {job_type}: {pct:.0f}% - {message}", "green"))
              completed_jobs.add(job_id)
          elif status != "C" and job_id not in completed_jobs:
              # job still in progress
              print(color_text(f"⋯ {job_type}: {pct:.0f}% - {message}", "yellow"))

      # Check if the final outputters job is complete
      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)

  ## ...

  # Step 4. 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", "")

      # Download only the final output files
      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}")

          # Save with submission ID prefix to avoid filename collisions
          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"))

  ##
  ```
</CodeGroup>
