Skip to content
Main site Contact

Integration examples

Documentation

These examples use neutral sample fields such as `document_number`, `document_type`, `material`, `region`, and `weight(kg)`. They follow the current controller behavior, including the legacy JSON-string request bodies still required by `PUT /api/File` and `PUT /api/Search`.

import base64
import hashlib
import json
import urllib.parse
from pathlib import Path
import requests
SERVER = "https://your-server.example.com"
CLIENT_ID = "YOUR_CLIENT_ID"
USERNAME = "[email protected]"
PASSWORD = "YourPlainTextPassword"
def sha1_hex(text):
return hashlib.sha1(text.encode("utf-8")).hexdigest()
def encode_filter_str(raw_filter):
return urllib.parse.quote(raw_filter, safe="")
def encode_ft_expression(expression):
encoded_expression = urllib.parse.quote(expression, safe="")
return encode_filter_str(f"FT={encoded_expression}")
def file_extension_for_api(path):
suffixes = Path(path).suffixes
if not suffixes:
raise ValueError(f"{path!r} has no file extension")
return "".join(suffixes).lower()
def json_base64_body(path):
return json.dumps(base64.b64encode(Path(path).read_bytes()).decode("ascii"))
def json_base64_array_body(*paths):
encoded_files = [
base64.b64encode(Path(path).read_bytes()).decode("ascii")
for path in paths
]
return json.dumps(json.dumps(encoded_files))
def auth_headers(token, content_type="application/json"):
return {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Content-Type": content_type,
}
def get_user_token():
response = requests.post(
f"{SERVER}/api/Token",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={
"grant_type": "password",
"client_id": CLIENT_ID,
"username": USERNAME,
"password": sha1_hex(PASSWORD),
},
timeout=30,
)
response.raise_for_status()
return response.json()["access_token"]

Choose the right file-based search workflow

Section titled “Choose the right file-based search workflow”

VizSeek supports three file-based search workflows. Select one based on the requirement for results in a single call, a reusable indexed input, or a non-blocking upload for large files:

  • Single call — PUT /api/Search. Upload the search input and receive results in the same response. Best for one-off searches.
  • Two step, synchronous — PUT /api/File?isSearchInput=true. Returns a fileUID that is fully indexed and immediately searchable, then reused across one or more GET /api/Search calls. Best when the same input is searched more than once (see Example 4).
  • Two step, asynchronous — PUT /api/File?isSearchInput=true&indexAsync=true. Returns the fileUID immediately, before indexing completes, so large uploads do not risk a client or proxy timeout. Poll GET /api/FileStatus until it returns HTTP 200, then search by the fileUID (see the asynchronous variant in Example 4).

Any of the three also accepts several images of the same part as one search input, which is how the web interface lets a user add a second or third photo to a search. See Example 5 below and Search — Searching with multiple images.

Example 1: Add a file to the company database with metadata

Section titled “Example 1: Add a file to the company database with metadata”

The attributes query parameter is only applied when isSearchInput=false.

def put_file_to_database(token, local_path, stored_path, metadata):
raw_attributes = "&".join(f"{name}={value}" for name, value in metadata.items())
response = requests.put(
f"{SERVER}/api/File"
f"?file={urllib.parse.quote(stored_path, safe='/')}"
f"&isSearchInput=false"
f"&attributes={urllib.parse.quote(raw_attributes, safe='')}",
headers=auth_headers(token),
data=json_base64_body(local_path),
timeout=120,
)
response.raise_for_status()
return response.text.strip()
token = get_user_token()
file_uid = put_file_to_database(
token,
local_path="samples/example-assembly.step",
stored_path="Design/example-assembly.step",
metadata={
"document_number": "AX-1000",
"document_type": "Assembly",
"material": "Stainless Steel",
"revision": "4",
"weight(kg)": "12.7",
},
)
print(file_uid)

Example 2: Check file data with GET /api/File

Section titled “Example 2: Check file data with GET /api/File”
def get_file(token, file_uid, include_shapelets=False):
params = {"fileUID": file_uid}
if include_shapelets:
params["includeShapelets"] = "true"
response = requests.get(
f"{SERVER}/api/File",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {token}",
},
params=params,
timeout=30,
)
response.raise_for_status()
return response.json()
token = get_user_token()
file_record = get_file(token, file_uid)
print(
json.dumps(
{
"UID": file_record["UID"],
"FileName": file_record["FileName"],
"Attributes": file_record.get("Attributes", []),
"IndexingFinished": file_record.get("IndexingFinished"),
},
indent=2,
)
)

Example 3: Run a visual search in one call with PUT /api/Search

Section titled “Example 3: Run a visual search in one call with PUT /api/Search”

This is the synchronous workflow. The uploaded search input can be combined with free text, metadata filters, or both.

def put_search(token, local_path, filter_parts=None):
query_parts = [
f"fileExtension={urllib.parse.quote(file_extension_for_api(local_path), safe='')}"
]
if filter_parts:
query_parts.append(
f"filterStr={encode_filter_str('&'.join(filter_parts))}"
)
response = requests.put(
f"{SERVER}/api/Search?{'&'.join(query_parts)}",
headers=auth_headers(token),
data=json_base64_array_body(local_path),
timeout=120,
)
response.raise_for_status()
return response.json()
token = get_user_token()
results = put_search(
token,
"samples/search-input.png",
filter_parts=[
"FT=pump housing",
"FT=document_type=[Assembly]",
],
)
print(json.dumps(results, indent=2))

Example 4: Find similar files by fileUID with GET /api/Search

Section titled “Example 4: Find similar files by fileUID with GET /api/Search”

This is the reusable two-step flow:

  1. Upload a temporary search-input file with PUT /api/File?isSearchInput=true.
  2. Reuse the returned fileUID in GET /api/Search.
def put_search_input_file(token, local_path):
response = requests.put(
f"{SERVER}/api/File"
f"?file={urllib.parse.quote(file_extension_for_api(local_path), safe='')}"
f"&isSearchInput=true",
headers=auth_headers(token),
data=json_base64_body(local_path),
timeout=120,
)
response.raise_for_status()
return response.text.strip()
def search_by_file_uid(token, file_uid, filter_parts=None):
parts = [f"fileUID={file_uid}"]
if filter_parts:
parts.extend(filter_parts)
response = requests.get(
f"{SERVER}/api/Search?filterStr={encode_filter_str('&'.join(parts))}",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {token}",
},
timeout=60,
)
response.raise_for_status()
return response.json()
token = get_user_token()
input_file_uid = put_search_input_file(token, "samples/search-input.png")
input_record = get_file(token, input_file_uid)
similar_results = search_by_file_uid(
token,
input_file_uid,
filter_parts=[
"FT=pump housing",
"FT=document_type=[Assembly]",
],
)
print(input_record["UID"])
print(json.dumps(similar_results, indent=2))

Because indexAsync defaults to false, PUT /api/File?isSearchInput=true completes indexing before it returns, so the returned fileUID is ready to search immediately.

Large search inputs can take long enough to index that a synchronous upload risks a client or proxy timeout. Add indexAsync=true to return the fileUID immediately, then poll GET /api/FileStatus until indexing completes before searching. GET /api/FileStatus signals state through the HTTP status code: 204 while indexing is in progress, 200 with a timestamp once it is complete, and 422 if indexing failed (see Files — GET /api/FileStatus for the full contract).

import time
def put_search_input_file_async(token, local_path):
response = requests.put(
f"{SERVER}/api/File"
f"?file={urllib.parse.quote(file_extension_for_api(local_path), safe='')}"
f"&isSearchInput=true"
f"&indexAsync=true",
headers=auth_headers(token),
data=json_base64_body(local_path),
timeout=120,
)
response.raise_for_status()
return response.text.strip()
def wait_until_indexed(token, file_uid, timeout_seconds=600, poll_interval=3):
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
response = requests.get(
f"{SERVER}/api/FileStatus",
headers={"Accept": "application/json", "Authorization": f"Bearer {token}"},
params={"fileUID": file_uid},
timeout=30,
)
if response.status_code == 200:
# indexing complete; value is the index timestamp
return response.text.strip().strip('"')
if response.status_code == 204:
# indexing still in progress
time.sleep(poll_interval)
continue
if response.status_code == 422:
raise RuntimeError(f"Indexing failed for {file_uid}; re-upload the file")
if response.status_code == 404:
raise LookupError(f"Unknown fileUID {file_uid}; was it uploaded (or deleted)?")
response.raise_for_status() # 400 / 401 / unexpected statuses
raise TimeoutError(f"Indexing did not complete within {timeout_seconds}s for {file_uid}")
token = get_user_token()
input_file_uid = put_search_input_file_async(token, "samples/large-search-input.step")
wait_until_indexed(token, input_file_uid)
similar_results = search_by_file_uid(token, input_file_uid)
print(json.dumps(similar_results, indent=2))

Example 5: Multi-view search with several images of the same part

Section titled “Example 5: Multi-view search with several images of the same part”

Several images of the same part can be searched as one input. VizSeek treats them as different views of one subject, matches each view against a different view of every candidate, and returns a single ranked result list. Both routes below produce the same result.

def put_search_multi(token, local_paths, filter_parts=None):
"""Route 1: upload every image in one PUT /api/Search call (max 10 files).
All the files must share one extension, because fileExtension is a single
query parameter applied to the whole request.
"""
extensions = {file_extension_for_api(path) for path in local_paths}
if len(extensions) != 1:
raise ValueError(
"PUT /api/Search takes one fileExtension for all files; "
"use search_by_file_uids() for mixed formats"
)
query_parts = [f"fileExtension={urllib.parse.quote(extensions.pop(), safe='')}"]
if filter_parts:
query_parts.append(f"filterStr={encode_filter_str('&'.join(filter_parts))}")
response = requests.put(
f"{SERVER}/api/Search?{'&'.join(query_parts)}",
headers=auth_headers(token),
data=json_base64_array_body(*local_paths),
timeout=300,
)
response.raise_for_status()
return response.json()
def search_by_file_uids(token, file_uids, filter_parts=None):
"""Route 2: search with search-input UIDs already uploaded via PUT /api/File.
The fileUID key inside filterStr takes a comma-separated list. Formats may
differ between the inputs, and each upload can use indexAsync=true.
"""
parts = [f"fileUID={','.join(file_uids)}"]
if filter_parts:
parts.extend(filter_parts)
response = requests.get(
f"{SERVER}/api/Search?filterStr={encode_filter_str('&'.join(parts))}",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {token}",
},
timeout=300,
)
response.raise_for_status()
return response.json()
token = get_user_token()
# Route 1 - one call, one extension for all files.
results = put_search_multi(
token,
[
"samples/bracket-front.jpg",
"samples/bracket-side.jpg",
"samples/bracket-top.jpg",
],
filter_parts=["FT=document_type=[Assembly]"],
)
# InputFileUID is a comma-separated list, one UID per uploaded image.
input_uids = results["InputFileUID"].split(",")
print(input_uids, results["TotalResults"])
# Re-run or refine the same multi-image search later, without re-uploading.
refined = search_by_file_uids(
token,
input_uids,
filter_parts=["FT=material=[Stainless Steel]"],
)
print(refined["TotalResults"])
# Route 2 from scratch - mixed formats are fine here.
uids = [
put_search_input_file(token, "samples/bracket-front.jpg"),
put_search_input_file(token, "samples/bracket-side.png"),
]
print(search_by_file_uids(token, uids)["TotalResults"])

To add a photo to a search a user already ran, send only the new image to PUT /api/Search and pass the existing UIDs in filterStr — the endpoint appends the upload to the fileUID list rather than replacing it:

results = put_search_multi(
token,
["samples/bracket-top.jpg"],
filter_parts=[f"fileUID={','.join(input_uids)}"],
)
print(results["InputFileUID"]) # previous inputs plus the one just uploaded

Every image must show the same part and be the same kind of file (all images, all 2D, or all 3D). An unrelated image is not an OR — it is one more view each candidate has to match. See Search — Searching with multiple images for the full rules.

For metadata filters inside FT=:

  • Regular search: document_number=AX-1000
  • Exact match: document_number=[AX-1000]
  • Wildcard search: document_number=AX-*
def search_by_metadata_expression(token, expression):
response = requests.get(
f"{SERVER}/api/Search?filterStr={encode_ft_expression(expression)}",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {token}",
},
timeout=60,
)
response.raise_for_status()
return response.json()
token = get_user_token()
regular_results = search_by_metadata_expression(token, "document_number=AX-1000")
exact_results = search_by_metadata_expression(token, "document_number=[AX-1000]")
wildcard_results = search_by_metadata_expression(token, "document_number=AX-*")
print(len(regular_results.get("ResultsList", [])))
print(len(exact_results.get("ResultsList", [])))
print(len(wildcard_results.get("ResultsList", [])))

To add more metadata filters in the same request, repeat FT= inside the nested filterStr, for example FT=document_type=[Assembly]&FT=material=[Stainless Steel].

The PMI field in the response is itself a JSON string, so parse it a second time if you want structured PMI objects.

def get_pmi_data(token, file_uid):
response = requests.get(
f"{SERVER}/api/PMIData",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {token}",
},
params={"fileUID": file_uid},
timeout=60,
)
response.raise_for_status()
data = response.json()
data["PMI"] = json.loads(data["PMI"]) if data.get("PMI") else []
return data
token = get_user_token()
pmi_data = get_pmi_data(token, file_uid)
print(json.dumps(pmi_data["ExtractedAttributes"], indent=2))
print(json.dumps(pmi_data["PMI"], indent=2))