Geosetta API Documentation


Welcome to the Geosetta API Documentation. Our platform offers unparalleled access to a wealth of public geotechnical data, empowering your projects with accurate and comprehensive subsurface information. By becoming a sponsor of Geosetta, you not only contribute to the advancement and accessibility of geotechnical data but also gain exclusive API access. This access allows for seamless integration of our extensive data repository into your systems, facilitating more informed decision-making and innovative solutions in your field. Sponsoring Geosetta is more than just a partnership; it's an opportunity to be at the forefront of geotechnical data utilization and to support the ongoing mission of making this valuable data openly accessible to professionals and researchers worldwide. Join us in this endeavor and unlock the full potential of geotechnical data with Geosetta.


Quick Navigation

DIGGS API

In addition to the endpoints below, Geosetta provides a dedicated DIGGS (Data Interchange for Geotechnical and Geoenvironmental Specialists) API for standardized geotechnical data exchange. The DIGGS API allows you to query, retrieve, and work with borehole data in the industry-standard DIGGS XML format.

Full DIGGS API documentation is available at: https://diggs.geosetta.org/api/docs


πŸ†• VLM Page Classification API

Classify pages in a PDF as boring log or not, without running extraction. Returns a session_id and classification results. No credits are charged. Use this for a two-stage workflow where you want to review classification results before committing to extraction.

POST /api/vlm/classify

Authentication: Required β€” X-API-Key header must be provided.

Rate Limit: 30 requests/minute per API key. Duplicate submissions are rejected with 409.

Request: multipart/form-data

Parameter Type Required Description
file File (PDF) Yes PDF file to classify (max 50MB, max 100 pages)

Example Request (Python):


import requests

api_key = "your_api_key_here"
pdf_file_path = "boring_log_report.pdf"

with open(pdf_file_path, "rb") as f:
    response = requests.post(
        "https://diggs.geosetta.org/api/vlm/classify",
        headers={"X-API-Key": api_key},
        files={"file": f},
    )

if response.status_code == 200:
    result = response.json()
    print(f"Session ID: {result['session_id']}")
    print(f"Boring log pages: {result['boring_log_pages']}")
    print(f"Non-boring pages: {result['non_boring_log_pages']}")
else:
    print(f"Error: {response.status_code}")
    print(response.text)

Example Request (cURL):


curl -X POST "https://diggs.geosetta.org/api/vlm/classify" \
  -H "X-API-Key: YOUR_API_KEY" \
  -F "file=@boring_log_report.pdf"

Response: application/json


{
    "session_id": "abc123-def456-ghi789",
    "total_pages": 63,
    "boring_log_pages": [34, 35, 36, 37, 38],
    "non_boring_log_pages": [0, 1, 2, 3, 4, ...],
    "message": "Classification complete. Pass session_id to /api/vlm/extract to skip re-classification."
}

Error Responses:

Status Code Condition
400 Non-PDF file, corrupt PDF, PDF too large (>50MB), too many pages (>100)
401 Missing or invalid API key
409 Duplicate submission β€” this PDF is already being processed for this API key
429 Rate limit exceeded (30 requests/minute)
500 Classifier model failed to load

VLM Boring Log Extraction API

The VLM (Vision Language Model) extraction service accepts geotechnical boring log PDFs and returns structured JSON data extracted by a fine-tuned vision language model. The service classifies each page as boring log or not, sends boring log pages to a GPU-powered extraction endpoint, and assembles multi-page results grouped by boring ID.

You can optionally pass a session_id from a prior /api/vlm/classify call to skip re-classification and go straight to extraction.

POST /api/vlm/extract

Authentication: Required β€” X-API-Key header must be provided.

Rate Limit: 30 requests/minute per API key. Duplicate submissions are rejected with 409.

Request: multipart/form-data

Parameter Type Required Default Description
file File (PDF) Yes β€” PDF file to process (max 50MB, max 100 pages)
output_mode string No both per_page, combined, or both
session_id string No β€” Session ID from a prior /api/vlm/classify call. Skips re-classification and goes straight to extraction. Without this, the full pipeline runs (backward compatible).

Example Request (Python):


import requests
import zipfile
import json
import os

api_key = "your_api_key_here"
pdf_file_path = "boring_log_report.pdf"

# Send PDF for extraction
# Optionally include session_id from a prior /api/vlm/classify call
with open(pdf_file_path, "rb") as f:
    response = requests.post(
        "https://diggs.geosetta.org/api/vlm/extract",
        headers={"X-API-Key": api_key},
        files={"file": f},
        data={
            "output_mode": "both",
            # "session_id": "abc123-def456-ghi789"  # optional, skips re-classification
        }
    )

if response.status_code == 200:
    # Save and extract the zip file
    with open("results.zip", "wb") as f:
        f.write(response.content)

    with zipfile.ZipFile("results.zip", "r") as z:
        z.extractall("results")

    # Read the combined boring data
    for boring_dir in os.listdir("results/borings"):
        if boring_dir == "grouping.json":
            continue
        data_path = f"results/borings/{boring_dir}/{boring_dir}.json"
        if os.path.exists(data_path):
            with open(data_path) as f:
                data = json.load(f)
            info = data.get("point_info", [{}])[0]
            print(f"{boring_dir}: {info.get('location', 'Unknown')}")
else:
    print(f"Error: {response.status_code}")
    print(response.text)

Example Request (cURL):


curl -X POST "https://diggs.geosetta.org/api/vlm/extract" \
  -H "X-API-Key: YOUR_API_KEY" \
  -F "file=@boring_log_report.pdf" \
  -F "output_mode=both" \
  --output results.zip

Response: application/zip file containing structured JSON results.

Zip structure for per_page mode:


pages/
β”œβ”€β”€ classification.json
β”œβ”€β”€ page_0/
β”‚   └── B-1.json
β”œβ”€β”€ page_3/        (non-boring pages are skipped)
β”‚   └── B-2.json

Zip structure for combined mode:


borings/
β”œβ”€β”€ grouping.json
β”œβ”€β”€ B-1/
β”‚   └── B-1.json
β”œβ”€β”€ B-2/
β”‚   └── B-2.json

Zip structure for both mode: Contains both pages/ and borings/ directories.

classification.json example:


{
    "total_pages": 63,
    "boring_log_pages": [34, 35, 36, 37, 38],
    "non_boring_log_pages": [0, 1, 2, 3, 4]
}

grouping.json example:


{
    "borings": {
        "B-1": {
            "pages": [34, 35, 36],
            "notes": []
        },
        "B-2": {
            "pages": [37, 38],
            "notes": ["page 38 had no boring_id, assigned by continuation"]
        }
    },
    "ungrouped_pages": []
}

B-1.json example:


{
    "point_info": [
        {
            "boring_id": "B-1",
            "project_name": "Highway Bridge Replacement",
            "location": "Stafford County, Virginia",
            "latitude": 38.348151,
            "longitude": -77.484592,
            "elevation": 145.6,
            "drilling_method": "3.25\" HSA w/ SPTs",
            "logged_by": "Russell Kanith/HDR",
            "date_started": "2017-04-27",
            "date_completed": "2017-04-27",
            "page_number": 1
        }
    ],
    "samples_spt": [ ... ],
    "samples_lab": [ ... ],
    "lithology": [ ... ],
    "groundwater": [ ... ]
}

Error Responses:

Status Code Condition
400 Invalid output_mode, non-PDF file, corrupt PDF, PDF too large (>50MB), too many pages (>100), invalid session_id
401 Missing or invalid API key
409 Duplicate submission β€” this PDF is already being processed for this API key
429 Rate limit exceeded (30 requests/minute)
500 Classifier model failed to load

Performance Notes:

GET /api/vlm/health

Authentication: Not required.

Example Request:


curl "https://diggs.geosetta.org/api/vlm/health"

Response:


{
    "status": "ok",
    "classifier_loaded": true,
    "modal_endpoint": "https://ross-cutts--boring-log-vlm-boringlogvlm-extract.modal.run",
    "modal_status": "ok"
}
Field Values Description
status ok / degraded degraded if classifier not loaded
classifier_loaded boolean Whether the ResNet page classifier is ready
modal_status ok / cold / unreachable GPU endpoint status. cold means it will need warm-up time on next request


πŸ†• Async PDFβ†’DIGGS Extraction API

The async endpoints let you submit a PDF and walk away β€” either polling for status or receiving a signed webhook callback when extraction completes. The underlying extraction is identical to the sync POST /api/vlm/extract; only the delivery mechanism differs. Use async when:

Authentication: All four endpoints require X-API-Key (same key as the sync endpoint).

Credits: Charged when the worker extracts β€” not at submission time. If a key's balance hits zero between submit and pickup the job fails with error_code: no_credits and zero credits are charged.

Queue: Jobs are processed globally in submission order (one at a time). There is no per-key concurrency cap; credits are the only spend limit.

POST /api/vlm/extract/async

Submit a PDF for async extraction. Returns 202 Accepted immediately.

Request: multipart/form-data

Parameter Type Required Description
file File Yes PDF or image (PDF / JPG / PNG / TIFF / BMP / WebP). Same 50 MB / 100-page caps as the sync endpoint.
all_pages string No Pass "true" to skip page classification and treat every page as a boring log.
callback_url string No Must be https://. We POST results here when extraction completes or fails.
callback_meta string No Opaque string (≀1024 chars) echoed back in the callback payload β€” useful for caller-side ticket IDs.

Response β€” 202 Accepted:


{
  "job_id": "8c1f...",
  "status": "queued",
  "queue_position": 3,
  "poll_url": "/api/vlm/jobs/8c1f...",
  "filename": "borings.pdf",
  "submitted_at": "2026-05-13T15:04:05Z"
}

Error Responses:

Status Code Condition
400 Bad PDF, invalid callback_url (non-https or malformed), file too large (>50 MB), too many pages (>100)
401 Missing X-API-Key header
403 Invalid API key or insufficient credits

GET /api/vlm/jobs/{job_id}

Poll the status of a submitted job. Returns one of four shapes depending on current state. A job_id that belongs to a different API key returns 404 (existence is not disclosed across keys).

Queued:


{"job_id": "...", "status": "queued", "filename": "...", "submitted_at": "...", "queue_position": 2}

Processing:


{"job_id": "...", "status": "processing", "filename": "...", "submitted_at": "...", "started_at": "..."}

Complete:


{
  "job_id": "...",
  "status": "complete",
  "filename": "...",
  "submitted_at": "...",
  "started_at": "...",
  "completed_at": "...",
  "total_pages": 12,
  "boring_pages": 4,
  "modal_pages_billed": 4,
  "download_url": "https://diggs.geosetta.org/api/vlm/jobs/.../download?exp=...&sig=...",
  "expires_at": "2026-05-14T15:04:05Z",
  "callback_status": "sent",
  "callback_attempts": 1
}

callback_status and callback_attempts are only present when a callback_url was supplied at submit time. The download_url is a 24-hour signed URL that works without an API key.

Failed:


{
  "job_id": "...",
  "status": "failed",
  "filename": "...",
  "submitted_at": "...",
  "completed_at": "...",
  "error_code": "no_credits | extraction_error | pdf_missing",
  "error_message": "..."
}

GET /api/vlm/jobs

List the caller's submissions from the last 30 days, most recent first. Useful for reconciling results if a webhook was missed.

Query parameters: limit (optional, default 100).

Response: Each element uses a uniform flattened shape β€” nullable fields are always present regardless of status. download_url, expires_at, and queue_position are not included; poll the single-job endpoint to get a download URL.


{
  "jobs": [
    {
      "job_id": "...",
      "status": "complete",
      "filename": "borings.pdf",
      "submitted_at": "2026-05-13T15:04:05Z",
      "started_at": "2026-05-13T15:06:10Z",
      "completed_at": "2026-05-13T15:08:11Z",
      "total_pages": 12,
      "boring_pages": 4,
      "modal_pages_billed": 4,
      "error_code": null,
      "error_message": null,
      "callback_status": "sent",
      "callback_attempts": 1
    },
    ...
  ]
}

GET /api/vlm/jobs/{job_id}/download

Stream the result ZIP for a completed job. The 24-hour signed URL returned in download_url works without an API key. Direct authenticated access also works using the X-API-Key header.

Authentication β€” either of:

Returns 404 if the 24-hour cache TTL has elapsed or the job is not complete.

Callback delivery

When a callback_url was supplied at submit time, we POST the following JSON to that URL when the job reaches complete or failed:


POST <callback_url>
Content-Type: application/json
X-Geosetta-Signature: t=<unix_ts>,v1=<hex>
User-Agent: Geosetta-VLM-Callback/1.0

{
  "job_id": "...",
  "status": "complete",
  "filename": "borings.pdf",
  "callback_meta": "caller-supplied opaque string",
  "completed_at": "2026-05-13T15:08:11Z",
  "download_url": "https://diggs.geosetta.org/api/vlm/jobs/.../download?exp=...&sig=...",
  "expires_at": "2026-05-14T15:08:11Z",
  "total_pages": 12,
  "boring_pages": 4,
  "modal_pages_billed": 4,
  "error_code": null,
  "error_message": null
}

Retry policy: 3 attempts at 1 s, 10 s, 60 s. Any non-2xx response or transport error retries. The job remains retrievable via the poll endpoint even if all retries fail.

Signature verification β€” the X-Geosetta-Signature header uses the same pattern as inbound Stripe webhooks, so any caller already handling Stripe will recognize the format. The HMAC secret is your API key itself β€” no separate webhook secret registration needed:


import hmac, hashlib

def verify_callback(request_body_bytes: bytes, signature_header: str, api_key: str) -> bool:
    """signature_header is the X-Geosetta-Signature value."""
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    ts, v1 = parts["t"], parts["v1"]
    signed = f"{ts}.".encode() + request_body_bytes
    expected = hmac.new(api_key.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(v1, expected)

End-to-end example


import requests

# 1. Submit
r = requests.post(
    "https://diggs.geosetta.org/api/vlm/extract/async",
    headers={"X-API-Key": "<your-key>"},
    files={"file": open("borings.pdf", "rb")},
    data={"callback_url": "https://your.app/webhooks/vlm", "callback_meta": "ticket-1234"},
)
job_id = r.json()["job_id"]  # HTTP 202

# 2a. Poll for status
import time
while True:
    status = requests.get(
        f"https://diggs.geosetta.org/api/vlm/jobs/{job_id}",
        headers={"X-API-Key": "<your-key>"},
    ).json()
    if status["status"] in ("complete", "failed"):
        break
    time.sleep(15)

# Download when complete (signed URL β€” no API key needed)
if status["status"] == "complete":
    zip_bytes = requests.get(status["download_url"]).content
    with open("results.zip", "wb") as f:
        f.write(zip_bytes)

# 2b. Or just wait for the callback to POST to https://your.app/webhooks/vlm
#     (see signature verification above)


πŸ†• DIGGS Viewer API

Wraps a DIGGS XML file in a self-contained interactive HTML viewer. The returned HTML works completely offline β€” no server, no dependencies, just open in any browser.

Viewer features:

Also available as a web UI at diggs.geosetta.org/?app=diggs_viewer (no API key required).

POST /api/viewer/wrap

Authentication: Not required.

Request: multipart/form-data

Parameter Type Required Description
file File (.xml or .diggs) Yes DIGGS XML file (max 50 MB, UTF-8 encoded)
logo Image file No Logo to display in the viewer header. Embedded as a base64 PNG β€” see requirements below.

Logo Requirements:

ConstraintValue
Accepted formats.png, .jpg, .jpeg, .gif, .webp
Max file size256 KB
Max dimensions512 Γ— 512 px
SVGNot accepted (security)

The logo is validated, re-encoded to PNG (stripping all metadata), and embedded as a data:image/png;base64,... URI. Non-square logos are preserved via object-fit: contain in a 32Γ—32 header slot.

Response: text/html β€” self-contained HTML viewer downloaded as {filename}_viewer.html

Example Request β€” with logo (Python):


import requests

with open("my_borings.diggs.xml", "rb") as xml, open("company_logo.png", "rb") as logo:
    response = requests.post(
        "https://diggs.geosetta.org/api/viewer/wrap",
        files={"file": xml, "logo": logo},
    )

if response.status_code == 200:
    with open("my_borings_viewer.html", "wb") as f:
        f.write(response.content)
    print("Viewer saved! Open my_borings_viewer.html in any browser.")
else:
    print(f"Error: {response.status_code}")
    print(response.json())

Example Request β€” with logo (cURL):


curl -X POST "https://diggs.geosetta.org/api/viewer/wrap" \
  -F "file=@boring_log.xml" \
  -F "logo=@company_logo.png" \
  -o report_viewer.html

Example Request β€” without logo (unchanged behavior):


curl -X POST "https://diggs.geosetta.org/api/viewer/wrap" \
  -F "file=@my_borings.diggs.xml" \
  -o my_borings_viewer.html

Security Notes:

Error Responses:

Status Code Condition
400 File must be .xml or .diggs
400 File too large (max 50MB)
400 File must be valid UTF-8 encoded XML
400 File does not appear to be a DIGGS XML document
500 Failed to build viewer

πŸ†• POST /api/viewer/template

Returns the same self-contained HTML viewer as /wrap, but with no embedded DIGGS data. Useful when you want to cache the viewer shell (optionally logo-branded) and splice in DIGGS XML on your side per render β€” without re-uploading to the API each time.

Authentication: Not required.

Request: multipart/form-data

Parameter Type Required Description
logo Image file No PNG / JPG / GIF / WebP, max 256 KB, max 512Γ—512 px. Re-encoded to PNG with metadata stripped and embedded as a base64 data URI in the viewer header. SVG is not accepted. (There is no file parameter β€” that is the point of this endpoint.)

Response: 200 OK, Content-Type: text/html, downloaded as diggs_viewer_template.html. Approximately 1.2 MB self-contained HTML with Plotly, CSS, and JS all inlined.

The response preserves a stable placeholder where DIGGS data goes:


<script type="application/xml" id="embedded-diggs"><!-- DIGGS_XML --></script>

Splice your own DIGGS XML in with a single string replace:


import requests

# 1. Fetch the template once and cache it
template_html = requests.post(
    "https://diggs.geosetta.org/api/viewer/template",
    files={"logo": open("company_logo.png", "rb")},
).text

# 2. Per render, drop in your DIGGS XML
def render_viewer(diggs_xml: str) -> str:
    return template_html.replace("<!-- DIGGS_XML -->", diggs_xml)

# 3. Write to disk or serve it directly
open("output_viewer.html", "w").write(render_viewer(my_diggs_xml))

Security warning β€” validate before splicing: The DIGGS XML embeds inside <script type="application/xml">…</script>. If your payload contains </script (case-insensitive) it will break out of the script block and could execute arbitrary JavaScript when the resulting file is opened. The /wrap endpoint rejects such payloads automatically; when splicing on your side you must do the same check:


import re

def is_safe(diggs_xml: str) -> bool:
    return re.search(r'</\s*script', diggs_xml, re.IGNORECASE) is None

# Always check before substituting
assert is_safe(my_diggs_xml), "DIGGS payload contains 

Error Responses:

Status Code Condition
400 Logo too large (>256 KB), invalid logo format, or logo dimensions exceed 512Γ—512 px
500 Viewer build assets missing on server

GET /api/viewer/health

Authentication: Not required.


{
    "status": "ok",
    "src_exists": true,
    "plotly_cached": true,
    "index_template": true
}


πŸ†• Geosetta Subsurface Summary API

Generate comprehensive Geosetta Subsurface Summary reports for any user-defined polygon. These reports compile geological data, soil information, nearby boreholes, and ML predictions into a professional PDF format.

Features:

  • Professional report format with comprehensive geotechnical data
  • Preliminary site characterization with geological and soil data
  • ML predictions for SPT N-values and groundwater depth
  • Nearby borings (5-mile radius)
  • USGS geology and SoilsGrid data integration
  • Clear disclaimers emphasizing preliminary nature
  • Two output formats: JSON data or PDF (base64 encoded)

import requests
import json

api_key = "your_api_key_here"

# Define your polygon (GeoJSON format)
polygon_geojson = {
    "type": "Polygon",
    "coordinates": [[
        [-78.4300, 38.1150],  # Northwest corner
        [-78.4250, 38.1150],  # Northeast corner
        [-78.4250, 38.1100],  # Southeast corner
        [-78.4300, 38.1100],  # Southwest corner
        [-78.4300, 38.1150]   # Close polygon
    ]]
}

# Request body
request_body = {
    "geojson": json.dumps(polygon_geojson),
    "format": "pdf"  # Options: "pdf" for PDF report, "json" for raw data
}

# Make the request
response = requests.post(
    "https://geosetta.org/web_map/api/generate_site_report/",
    json=request_body,
    headers={
        "Authorization": f"Bearer {api_key}",  # Or use "X-API-Key": api_key
        "Content-Type": "application/json"
    }
)

# Check the response
if response.status_code == 200:
    result = response.json()
    if result['status'] == 'success':
        if request_body['format'] == 'pdf':
            # Decode base64 PDF
            import base64
            pdf_data = base64.b64decode(result['pdf'])
            
            # Save to file
            with open('site_assessment_appendix.pdf', 'wb') as f:
                f.write(pdf_data)
            print(f"Appendix PDF saved! Size: {result['metadata']['size_bytes']} bytes")
            print(f"Filename: {result['metadata']['filename']}")
        else:
            # JSON data format
            print("Report data received:")
            print(f"Polygon area: {result['data']['polygon']['area']['acres']:.2f} acres")
            print(f"Geology: {result['data']['geology']['formation']}")
            print(f"Nearby boreholes: {result['data']['boreholes']['total_count']}")
else:
    print(f"Error: {response.status_code}")
    print(response.json())

Response Format (PDF mode):


{
    "status": "success",
    "pdf": "JVBERi0xLjQKJcfs...",  // Base64 encoded PDF
    "metadata": {
        "filename": "geosetta_site_assessment_appendix_20241205_143022.pdf",
        "size_bytes": 98765,
        "generation_time": 6.23,
        "report_version": "1.0"
    },
    "disclaimer": {
        "text": "This report is for preliminary planning purposes only...",
        "terms_url": "https://geosetta.org/terms",
        "must_accept": true
    }
}

Response Format (JSON mode):


{
    "status": "success",
    "data": {
        "polygon": {
            "centroid": {"lat": 38.1125, "lon": -78.4275},
            "area": {"acres": 123.45, "hectares": 49.98, "square_meters": 499776},
            "address": "Near Charlottesville, VA 22903",
            "elevation_ft": 485.2
        },
        "geology": {
            "formation": "Granite",
            "age": "Paleozoic",
            "major_components": ["Granite", "Gneiss"],
            "usgs_url": "https://ngmdb.usgs.gov/..."
        },
        "soils": {
            "soil_type": "Alfisols",
            "wrb_class": "Luvisols",
            "properties": {
                "clay_content": 25.5,
                "sand_content": 35.2,
                "organic_carbon": 1.8
            },
            "usda_soil_survey_url": "https://websoilsurvey.sc.egov.usda.gov/..."
        },
        "predictions": {
            "predictions_by_depth": [
                {
                    "depth_ft": 0,
                    "spt_n": "10-25",
                    "soil_description": "Medium dense sandy CLAY"
                },
                // ... more depth predictions to 50 ft
            ],
            "groundwater": {
                "depth_label": "Medium (10-50 ft)",
                "deviation_label": "Moderate (5-20 ft variation)"
            }
        },
        "boreholes": {
            "total_count": 15,
            "closest_distance_miles": 0.8,
            "all_boreholes": [
                {
                    "functional_key": "38.1234;-78.4321",
                    "distance_miles": 0.8,
                    "provider": "VDOT",
                    "depth_ft": 45,
                    "map_url": "https://geosetta.org/web_map/map/#18/38.1234/-78.4321"
                }
                // ... more boreholes
            ]
        },
        "metadata": {
            "collection_time": 5.89,
            "timestamp": "2024-12-05T14:30:22Z"
        }
    },
    "disclaimer": {
        "text": "This data is for preliminary planning only...",
        "terms_url": "https://geosetta.org/terms",
        "must_accept": true
    }
}

Appendix Contents Include:

  • Site Location: Area calculations, nearest address, centroid elevation
  • Anticipated Geology: USGS formation data, rock types, age
  • Anticipated Surficial Soils: SoilsGrid WRB classification, engineering properties
  • Predicted Subsurface Profile: SPT N-values and soil descriptions to 50 ft
  • Groundwater Predictions: Depth category and seasonal variation
  • Nearby Borings: Within 5-mile radius with clickable map links
  • Data Sources & References: Complete citations and contact information


API Endpoint to retrieve Historic Data around Radius:


  api_key = "your_api_key_here"

  # Your payload and API key
  json_payload = {
      "deliverableType": "Return_Historic_Data_In_Radius",
      "data": {
          "points": [
              {
                  "latitude":   39.21884440935613,
                  "longitude": -76.84346423232522,
                  "radius_m": 1000
              }
          ]
      }
  }
  
  # Make the request
  response = requests.post(
      "https://geosetta.org/web_map/api_key/",
      json=json_payload,  # Directly send the json_payload
      headers={
          "Authorization": f"Bearer {api_key}"  # Set the API key in the Authorization header
      }
  )
  
  # Check the response
  if response.status_code == 200:
      print("API request successful:")
      print(response.json())
      response_data = response.json()  # Convert the Response object to a dictionary
      points_in_radius = response_data['results']['points_in_radius']
      print(points_in_radius)
  
  else:
      print("API request failed:")
      print(f"Status code: {response.status_code}")
      print(response.text)
  

The server will respond with a JSON object like this:


{'message': 'Data processed successfully.', 'results': {'points_in_radius': {'type': 'FeatureCollection', 'features': [{'type': 'Point', 'coordinates': [-76.8445259, 39.22082777], 'properties': {'content': '

Geosetta History

\n

Source: MDOT

(lat,lng): 39.2208278 , -76.8445259 elev ft: 335.20\n

Total Depth: 25.0 ft

Available Deliverables:
\n

Generate Pointcloud

\n

Monthly Precipitation History NOAA

\n

Open Soils Map

\n \n

\n
\n View Borehole Log with RSLog\n

\n

Diggs File

'}}, {'type': 'Point', 'coordinates': [-76.84413791, 39.22100078], 'properties': {'content': '

Geosetta History

\n

Source: MDOT

(lat,lng): 39.2210008 , -76.8441379 elev ft: 346.00\n

Total Depth: 5.0 ft

Available Deliverables:
\n

Generate Pointcloud

\n

Monthly Precipitation History NOAA

\n

Open Soils Map

\n \n

\n
\n View Borehole Log with RSLog\n

\n

Diggs File

'}}, {'type': 'Point', 'coordinates': [-76.8453399, 39.22108078], 'properties': {'content': '

Geosetta History

\n

Source: MDOT

(lat,lng): 39.2210808 , -76.8453399 elev ft: 338.10\n

Total Depth: 20.0 ft

Available Deliverables:
\n

Generate Pointcloud

\n

Monthly Precipitation History NOAA

\n

Open Soils Map

\n \n

\n
\n View Borehole Log with RSLog\n

\n

Diggs File

'}}]}}, 'disclaimer': 'The information provided by this API has been made available by various DOTs and other public agencies. The predictive models presented herein are based on machine learning tools applied to the data. The predicted subsurface conditions are a probabilistic model and do not depict the actual conditions at the site. No claim as to the accuracy of the data and predictive model is made or implied. No other representation, expressed or implied, is included or intended and no warranty or guarantee is included or intended in any Geosetta data or models. The User understands these limitations and that Geosetta is a tool for planning subsurface investigations and not a substitute for it. In no event shall Geosetta or the Data Owners be liable to the User or another Party for any incidental, consequential, special, exemplary or indirect damages, lost business profits or lost data arising out of or in any way related to the use of information or data obtained from the Geosetta.org website or API.'} {'type': 'FeatureCollection', 'features': [{'type': 'Point', 'coordinates': [-76.8445259, 39.22082777], 'properties': {'content': '

Geosetta History

\n

Source: MDOT

(lat,lng): 39.2208278 , -76.8445259 elev ft: 335.20\n

Total Depth: 25.0 ft

Available Deliverables:
\n

Generate Pointcloud

\n

Monthly Precipitation History NOAA

\n

Open Soils Map

\n \n

\n
\n View Borehole Log with RSLog\n

\n

Diggs File

'}}, {'type': 'Point', 'coordinates': [-76.84413791, 39.22100078], 'properties': {'content': '

Geosetta History

\n

Source: MDOT

(lat,lng): 39.2210008 , -76.8441379 elev ft: 346.00\n

Total Depth: 5.0 ft

Available Deliverables:
\n

Generate Pointcloud

\n

Monthly Precipitation History NOAA

\n

Open Soils Map

\n \n

\n
\n View Borehole Log with RSLog\n

\n

Diggs File

'}}, {'type': 'Point', 'coordinates': [-76.8453399, 39.22108078], 'properties': {'content': '

Geosetta History

\n

Source: MDOT

(lat,lng): 39.2210808 , -76.8453399 elev ft: 338.10\n

Total Depth: 20.0 ft

Available Deliverables:
\n

Generate Pointcloud

\n

Monthly Precipitation History NOAA

\n

Open Soils Map

\n \n

\n
\n View Borehole Log with RSLog\n

\n

Diggs File

'}}]}




API Endpoint to retrieve data predictions (Note you can predict up to 50 points per request with a maximum depth of 100 feet.):

New Feature: Groundwater Predictions

Each point now includes groundwater depth and seasonal variation predictions powered by machine learning models:

  • Depth Categories:
    • 0 = Shallow (≀10 ft)
    • 1 = Medium (10-50 ft)
    • 2 = Deep (>50 ft)
  • Seasonal Deviation Categories:
    • 0 = Low variation (≀5 ft)
    • 1 = Moderate variation (5-20 ft)
    • 2 = High variation (>20 ft)
  • Probabilities: Confidence percentages for each category (shallow/medium/deep for depth, low/moderate/high for variation)

Profile Fields (per depth):

  • depth_ft β€” Depth in feet (0 to requested depth, 1-ft increments)
  • dominant_grain_size β€” Primary grain class: CLAY, SILT, SAND, or GRAVEL. Null when ROCK is predicted (use geomaterial to check for ROCK).
  • dominant_N_value β€” SPT N-value range: 0, 1-9, 10-25, 26-41, 42-50, or 50+
  • geomaterial β€” Simplified geomaterial type (same as dominant_grain_size; e.g. "CLAY")
  • grain_description β€” Plain grain-type description without consistency prefix (e.g. "Sandy CLAY"). Null when ROCK is predicted.
  • SPT_consistency β€” Standalone consistency/density label derived from N-value and grain type (e.g. "Soft", "Medium Dense", "Dense"). Cohesive soils use consistency terms; granular soils use density terms.
  • rock_type β€” Geologic formation name when ROCK is predicted (e.g. "Shale", "Limestone", "Sandstone"). Null for all non-rock predictions.
  • soil_description β€” Full enriched description combining grain type, consistency, and USCS class where available (e.g. "Soft Sandy CLAY [Lean Clay (CL)]"). Null when ROCK is predicted.
  • USCS_groupSymbol β€” USCS group symbol. Fine-grained soils (CLAY, SILT) return lab-model symbols (e.g. "CL", "CH", "ML"). Coarse-grained soils (SAND, GRAVEL) return symbols derived from fines content and grain distribution (e.g. "SP", "SM", "SC", "SP-SM", "GP", "GM", "GC", "GP-GM"). May be null if grain type is ROCK or predictions are unavailable.
  • USCS_groupName β€” Full USCS group name (e.g. "Lean Clay", "Silty Sand", "Poorly-graded Gravel with Silt"). Null whenever USCS_groupSymbol is null.
  • lab_predictions β€” Detailed USCS and fines classification probabilities (present when lab model fires)

  api_key = "your_api_key_here"

  # Your payload and API key
  json_payload = {
      "deliverableType": "SPT_Point_Prediction",
      "data": {
          "points": [
              {
                  "latitude": 39.387080,
                  "longitude": -76.813480,
                  "depth": 50,
                  "surfaceelevation": None  # Optional: if not provided, will be calculated automatically
              },
              {
                  "latitude": 39.4,
                  "longitude": -76.8,
                  "depth": 30
                  # surfaceelevation will be calculated automatically if not provided
              }
          ]
      }
  }
  
  
  # Make the request
  response = requests.post(
      "https://geosetta.org/web_map/api_key/",
      json=json_payload,  # Directly send the json_payload
      headers={
          "Authorization": f"Bearer {api_key}"  # Set the API key in the Authorization header
      }
  )
  
  # Check the response
  if response.status_code == 200:
      print("API request successful:")
      print(response.json())
  else:
      print("API request failed:")
      print(f"Status code: {response.status_code}")
      print(response.text)

The server will respond with a JSON object like this:


{
    "message": "Data processed successfully.",
    "results": [
        {
            "point": {
                "latitude": 39.387080,
                "longitude": -76.813480,
                "depth": 50,
                "elevation_ft": 485.2,
                "surfaceelevation": null,
                "minimum_distance_from_trained_point_(mi)": 2.13
            },
            "profiles": [
                {
                    "depth_ft": 0,
                    "dominant_grain_size": "CLAY",
                    "dominant_N_value": "1-9",
                    "geomaterial": "CLAY",
                    "grain_description": "Sandy CLAY",
                    "SPT_consistency": "Soft",
                    "soil_description": "Soft Sandy CLAY [Lean Clay (CL)]",
                    "USCS_groupSymbol": "CL",
                    "USCS_groupName": "Lean Clay",
                    "lab_predictions": {
                        "uscs_class": "CL",
                        "uscs_probs": {"CH": 0.164, "CL": 0.686, "ML": 0.147, "NP": 0.004},
                        "fines_class": "Fine-grained",
                        "fines_probs": {"Coarse-grained": 0.436, "Fine-grained": 0.564},
                        "fines_4class": "Fine-grained",
                        "fines_4class_probs": {"Clean": 0.058, "Fine-grained": 0.6, "Significant": 0.304, "Some": 0.038}
                    }
                },
                {
                    "depth_ft": 1,
                    "dominant_grain_size": "SAND",
                    "dominant_N_value": "10-25",
                    "geomaterial": "SAND",
                    "grain_description": "SAND",
                    "SPT_consistency": "Medium Dense",
                    "soil_description": "Medium dense SAND with some silt [Silty Sand (SM)]",
                    "USCS_groupSymbol": "SM",
                    "USCS_groupName": "Silty Sand"
                },
                // ... depth profiles continue for each foot up to the requested depth
                {
                    "depth_ft": 48,
                    "dominant_grain_size": "CLAY",
                    "dominant_N_value": "26-41",
                    "geomaterial": "CLAY",
                    "grain_description": "silty CLAY",
                    "SPT_consistency": "Stiff",
                    "rock_type": null,
                    "soil_description": "Stiff silty CLAY [Fat Clay (CH)]",
                    "USCS_groupSymbol": "CH",
                    "USCS_groupName": "Fat Clay"
                },
                {
                    "depth_ft": 50,
                    "dominant_grain_size": null,
                    "dominant_N_value": "50+",
                    "geomaterial": "ROCK",
                    "grain_description": null,
                    "SPT_consistency": null,
                    "rock_type": "Shale",
                    "soil_description": null,
                    "USCS_groupSymbol": null,
                    "USCS_groupName": null
                }
            ],
            "groundwater": {
                "depth_category": 1,
                "depth_label": "Medium (10-50 ft)",
                "depth_probabilities": {
                    "shallow": 23.7,
                    "medium": 55.9,
                    "deep": 20.4
                },
                "deviation_category": 1,
                "deviation_label": "Moderate (5-20 ft variation)",
                "deviation_probabilities": {
                    "low": 44.9,
                    "moderate": 50.2,
                    "high": 4.9
                }
            }
        },
        {
            "point": {
                "latitude": 39.4,
                "longitude": -76.8,
                "depth": 30,
                "elevation_ft": 312.7,
                "surfaceelevation": null,
                "minimum_distance_from_trained_point_(mi)": 1.87
            },
            "profiles": [
                {
                    "depth_ft": 0,
                    "dominant_grain_size": "SILT",
                    "dominant_N_value": "1-9",
                    "geomaterial": "SILT",
                    "grain_description": "SILT",
                    "SPT_consistency": "Soft",
                    "soil_description": "Soft SILT [Silt (ML)]",
                    "USCS_groupSymbol": "ML",
                    "USCS_groupName": "Silt"
                },
                {
                    "depth_ft": 1,
                    "dominant_grain_size": "SILT",
                    "dominant_N_value": "10-25",
                    "geomaterial": "SILT",
                    "grain_description": "SILT",
                    "SPT_consistency": "Medium Stiff",
                    "soil_description": "Medium stiff SILT [Silt (ML)]",
                    "USCS_groupSymbol": "ML",
                    "USCS_groupName": "Silt"
                },
                // ... depth profiles continue for each foot up to the requested depth
                {
                    "depth_ft": 30,
                    "dominant_grain_size": "SAND",
                    "dominant_N_value": "10-25",
                    "geomaterial": "SAND",
                    "grain_description": "SAND",
                    "SPT_consistency": "Medium Dense",
                    "soil_description": "Medium dense SAND",
                    "USCS_groupSymbol": null,
                    "USCS_groupName": null
                }
            ],
            "groundwater": {
                "depth_category": 0,
                "depth_label": "Shallow (≀10 ft)",
                "depth_probabilities": {
                    "shallow": 65.2,
                    "medium": 28.4,
                    "deep": 6.4
                },
                "deviation_category": 0,
                "deviation_label": "Low (≀5 ft variation)",
                "deviation_probabilities": {
                    "low": 72.1,
                    "moderate": 21.3,
                    "high": 6.6
                }
            }
        }
    ],
    "disclaimer": "The information provided by this API has been made available by various DOTs and other public agencies. The predictive models presented herein are based on machine learning tools applied to the data. The predicted subsurface conditions are a probabilistic model and do not depict the actual conditions at the site. No claim as to the accuracy of the data and predictive model is made or implied. No other representation, expressed or implied, is included or intended and no warranty or guarantee is included or intended in any Geosetta data or models. The User understands these limitations and that Geosetta is a tool for planning subsurface investigations and not a substitute for it. In no event shall Geosetta or the Data Owners be liable to the User or another Party for any incidental, consequential, special, exemplary or indirect damages, lost business profits or lost data arising out of or in any way related to the use of information or data obtained from the Geosetta.org website or API."
}





Description of image

  api_key = "your_api_key_here"

  # Your payload and API key
  json_payload = {
      "deliverableType": "Project_Link",
      "data": {
    "type": "FeatureCollection",
    "features": [
      {
        "type": "Feature",
        "geometry": {
          "coordinates": [
            [
              [
                -123.119842,
                49.323426
              ],
              [
                -123.117503,
                49.323421
              ],
              [
                -123.117524,
                49.322477
              ],
              [
                -123.118871,
                49.322239
              ],
              [
                -123.120083,
                49.322294
              ],
              [
                -123.119842,
                49.323426
              ]
            ]
          ],
          "type": "Polygon"
        },
        "properties": {
          "Project Title": "RSLog Example Project #1 (Imperial) ",
          "Project Number": "EX20-001",
          "Client": "ABC Client Company Ltd."
        }
      },
      {
        "type": "Feature",
        "geometry": {
          "coordinates": [
            -123.11768,
            49.32323
          ],
          "type": "Point"
        },
        "properties": {
          "Name": "AH20-4a",
          "Depth": "13 ft",
          "Groundwater Depth": "",
          "Project Ref.": "RSLog Example Project #1 (Imperial)  (EX20-001)"
        }
      },
      {
        "type": "Feature",
        "geometry": {
          "coordinates": [
            -123.11934,
            49.32298
          ],
          "type": "Point"
        },
        "properties": {
          "Name": "testy",
          "Depth": "1 ft",
          "Groundwater Depth": "",
          "Project Ref.": "RSLog Example Project #1 (Imperial)  (EX20-001)"
        }
      },
      {
        "type": "Feature",
        "geometry": {
          "coordinates": [
            -123.11843,
            49.3227
          ],
          "type": "Point"
        },
        "properties": {
          "Name": "AH21-01",
          "Depth": "38.5 ft",
          "Groundwater Depth": "",
          "Project Ref.": "RSLog Example Project #1 (Imperial)  (EX20-001)"
        }
      },
      {
        "type": "Feature",
        "geometry": {
          "coordinates": [
            -123.11978,
            49.32242
          ],
          "type": "Point"
        },
        "properties": {
          "Name": "AH20-3",
          "Depth": "30 ft",
          "Groundwater Depth": "16.2 ft",
          "Project Ref.": "RSLog Example Project #1 (Imperial)  (EX20-001)"
        }
      }
    ]
  }
  }
  
  
  # Make the request
  response = requests.post(
      "https://geosetta.org/web_map/api_key/",
      json={
          "json_data": json.dumps(json_payload)  # Encode the dictionary as a JSON string
      },
      headers={
          "Authorization": f"Bearer {api_key}"  # Set the API key in the Authorization header
      }
  )
  
  # Check the response
  if response.status_code == 200:
      print("API request successful:")
      print(response.json())
  else:
      print("API request failed:")
      print(f"Status code: {response.status_code}")
      print(response.text)

The server will respond with a JSON object like this:


{'message': 'Data processed successfully.', 'results': 'https://geosetta.org/web_map/map/jqM826', 'disclaimer': 'The information provided by this API has been made available by various DOTs and other public agencies. The predictive models presented herein are based on machine learning tools applied to the data. The predicted subsurface conditions are a probabilistic model and do not depict the actual conditions at the site. No claim as to the accuracy of the data and predictive model is made or implied. No other representation, expressed or implied, is included or intended and no warranty or guarantee is included or intended in any Geosetta data or models. The User understands these limitations and that Geosetta is a tool for planning subsurface investigations and not a substitute for it. In no event shall Geosetta or the Data Owners be liable to the User or another Party for any incidental, consequential, special, exemplary or indirect damages, lost business profits or lost data arising out of or in any way related to the use of information or data obtained from the Geosetta.org website or API.'}




API endpoint retrieves the distance from a pre-trained point. It's important to note that not all data on Geosetta has been included in the training process. Therefore, the distance provided is relative to the nearest location that has been used in our training dataset.


  api_key = "your_api_key_here"

  # Your payload and API key
  json_payload = {
      "deliverableType": "Distance_From_Trained_Point",
      "data": {
          "points": [
              {
                  "latitude":   39.387080,
                  "longitude": -76.813480,
                  "depth": 50
              },
              {
                  "latitude": 39.4,
                  "longitude": -76.8,
                  "depth": 30
              }
          ]
      }
  }
  
  
  # Make the request
  response = requests.post(
      "https://geosetta.org/web_map/api_key/",
      json={
          "json_data": json.dumps(json_payload)  # Encode the dictionary as a JSON string
      },
      headers={
          "Authorization": f"Bearer {api_key}"  # Set the API key in the Authorization header
      }
  )
  
  # Check the response
  if response.status_code == 200:
      print("API request successful:")
      print(response.json())
  else:
      print("API request failed:")
      print(f"Status code: {response.status_code}")
      print(response.text)

The server will respond with a JSON object like this:


{'message': 'Data processed successfully.', 'results': {'minimum_distance_from_trained_point_(mi)': 2.13}, 'disclaimer': 'The information provided by this API has been made available by various DOTs and other public agencies. The predictive models presented herein are based on machine learning tools applied to the data. The predicted subsurface conditions are a probabilistic model and do not depict the actual conditions at the site. No claim as to the accuracy of the data and predictive model is made or implied. No other representation, expressed or implied, is included or intended and no warranty or guarantee is included or intended in any Geosetta data or models. The User understands these limitations and that Geosetta is a tool for planning subsurface investigations and not a substitute for it. In no event shall Geosetta or the Data Owners be liable to the User or another Party for any incidental, consequential, special, exemplary or indirect damages, lost business profits or lost data arising out of or in any way related to the use of information or data obtained from the Geosetta.org website or API.'}

API endpoint to convert a pdf boring log to a DIGGS file.


  import requests
  import json
  
  api_key = "your_api_key_here"
  
  # Example PDF file path
  pdf_file_path = '/content/example_boring_log.pdf'
  
  # Your payload and API key
  json_payload = {
      "deliverableType": "Pdf_Extraction",
      "data": {
      }
  }
  
  # Make the request
  with open(pdf_file_path, 'rb') as pdf_file:
      response = requests.post(
          "https://geosetta.org/web_map/api_key/",
          files={'file': pdf_file},
          data={
              'json_data': json.dumps(json_payload)
          },
          headers={
              "Authorization": f"Bearer {api_key}"  # Set the API key in the Authorization header
          }
      )
  
  # Check the response
  if response.status_code == 200:
      print("API request successful:")
      # Save the zip file content to a file
      zip_file_path = '/content/processed_results.zip'
      with open(zip_file_path, 'wb') as zip_file:
          zip_file.write(response.content)
      print(f"Zip file saved to {zip_file_path}")
  else:
      print("API request failed:")
      print(f"Status code: {response.status_code}")
      print(response.text)

The server will respond with a JSON object like this:


  API request successful:
  Zip file saved to /content/processed_results.zip

πŸ†• VLM Extraction Issue Reporting API

Report incorrect VLM boring log extractions to help improve model accuracy. When the extraction service misreads values β€” wrong blow counts, shifted depths, missing layers β€” submit a report with the page images and extraction data. Reports are stored as training examples and used to fine-tune future model versions.

Issue reports can also be submitted directly from the Boring Log Extractor web interface using the Report Issue button next to each boring.

POST /api/vlm/report

Submit an issue report for an incorrect extraction. Attach the page image(s) for the boring and the per-page extraction JSON that the model produced. Use the same X-API-Key you used for the original extraction request.

Authentication: Required β€” X-API-Key header must be provided.

Request: multipart/form-data

Parameter Type Required Default Description
images File(s) (PNG) Yes β€” One or more page images, named page_{N}.png where N is the page number (max 20MB each)
extraction_jsons string (JSON) Yes β€” JSON object mapping page numbers to their extraction results: {"5": {...}, "6": {...}}
description string No β€” Description of what's wrong with the extraction
boring_id string No β€” Boring ID being reported (e.g. B-1)
filename string No β€” Original PDF filename

Example Request (Python):


import requests
import json

api_key = "your_api_key_here"

# Page images and extraction data from a previous /vlm/extract call
page_images = {
    5: open("pages/B-1_page_5.png", "rb").read(),
    6: open("pages/B-1_page_6.png", "rb").read(),
}

extraction_data = {
    "5": {"results": {"point_info": [{"boring_id": "B-1"}], "samples_spt": [...]}},
    "6": {"results": {"samples_spt": [...], "lithology": [...]}},
}

# Submit the report
response = requests.post(
    "https://diggs.geosetta.org/api/vlm/report",
    headers={"X-API-Key": api_key},
    files=[
        ("images", ("page_5.png", page_images[5], "image/png")),
        ("images", ("page_6.png", page_images[6], "image/png")),
    ],
    data={
        "extraction_jsons": json.dumps(extraction_data),
        "description": "SPT N-values are shifted down one row β€” 15 at 5ft should be at 10ft",
        "boring_id": "B-1",
        "filename": "geotech_report.pdf",
    },
)

if response.status_code == 200:
    report = response.json()
    print(f"Report submitted: {report['report']['report_id']}")
else:
    print(f"Error: {response.status_code}")
    print(response.text)

Example Request (cURL):


curl -X POST "https://diggs.geosetta.org/api/vlm/report" \
  -H "X-API-Key: YOUR_API_KEY" \
  -F "images=@pages/page_5.png" \
  -F "images=@pages/page_6.png" \
  -F 'extraction_jsons={"5": {"results": {}}, "6": {"results": {}}}' \
  -F "description=SPT N-values shifted down one row" \
  -F "boring_id=B-1" \
  -F "filename=geotech_report.pdf"

Response:


{
    "status": "created",
    "report": {
        "report_id": "20260316_022326_f637b054",
        "api_key_prefix": "lZnSEWjt...",
        "boring_id": "B-1",
        "filename": "geotech_report.pdf",
        "description": "SPT N-values are shifted down one row β€” 15 at 5ft should be at 10ft",
        "pages": [5, 6],
        "has_correction": false,
        "created_at": "2026-03-16T02:23:26.804417+00:00"
    }
}

Error Responses:

Status Code Condition
400 Missing images, invalid extraction_jsons JSON, image too large (>20MB), page number mismatch between images and JSON keys
401 Missing or invalid API key
403 Invalid API key or no credits remaining

PATCH /api/vlm/reports/{report_id}

Add or update a corrected JSON for a previously submitted report. Only the original submitter (matching API key) can update a report.

Authentication: Required β€” X-API-Key header must be provided (must match the key used to create the report).

Request: multipart/form-data

Parameter Type Required Description
report_id string (path) Yes The report ID returned from POST /api/vlm/report
corrected_json string (JSON) Yes The corrected extraction data

Example Request (Python):


import requests
import json

api_key = "your_api_key_here"
report_id = "20260316_022326_f637b054"

corrected = {
    "point_info": [{"boring_id": "B-1", "total_depth": "50 ft"}],
    "samples_spt": [
        {"depth_top": "5.0", "depth_bottom": "6.5", "n_value": "15"},
        {"depth_top": "10.0", "depth_bottom": "11.5", "n_value": "22"}
    ]
}

response = requests.patch(
    f"https://diggs.geosetta.org/api/vlm/reports/{report_id}",
    headers={"X-API-Key": api_key},
    data={"corrected_json": json.dumps(corrected)},
)

if response.status_code == 200:
    print("Correction submitted successfully")
else:
    print(f"Error: {response.status_code}")
    print(response.text)

Example Request (cURL):


curl -X PATCH "https://diggs.geosetta.org/api/vlm/reports/20260316_022326_f637b054" \
  -H "X-API-Key: YOUR_API_KEY" \
  -F 'corrected_json={"point_info": [{"boring_id": "B-1"}], "samples_spt": [...]}'

Response:


{
    "status": "updated",
    "report": {
        "report_id": "20260316_022326_f637b054",
        "api_key_prefix": "lZnSEWjt...",
        "boring_id": "B-1",
        "filename": "geotech_report.pdf",
        "description": "SPT N-values are shifted down one row",
        "pages": [5, 6],
        "has_correction": true,
        "corrected_json": "{...}",
        "status": "new",
        "created_at": "2026-03-16T02:23:26.804417+00:00"
    }
}

Error Responses:

Status Code Condition
401 Missing or invalid API key
404 Report not found, or API key does not match the original submitter

GET /api/vlm/reports/{report_id}

Retrieve a single report including metadata and per-page extraction JSONs.

Authentication: Required β€” X-API-Key header must be provided.

Example Request (cURL):


curl "https://diggs.geosetta.org/api/vlm/reports/20260316_022326_f637b054" \
  -H "X-API-Key: YOUR_API_KEY"

Response:


{
    "report_id": "20260316_022326_f637b054",
    "api_key_prefix": "lZnSEWjt...",
    "boring_id": "B-1",
    "filename": "geotech_report.pdf",
    "description": "SPT N-values are shifted down one row",
    "pages": [5, 6],
    "page_extractions": {
        "5": "{\"results\": {\"point_info\": [{\"boring_id\": \"B-1\"}]}}",
        "6": "{\"results\": {\"samples_spt\": [{\"depth\": \"10\"}]}}"
    },
    "has_correction": false,
    "corrected_json": null,
    "status": "new",
    "created_at": "2026-03-16T02:23:26.826979+00:00"
}

Response Fields:

Field Type Description
report_id string Unique report identifier
pages array Page numbers included in this report
page_extractions object Map of page number β†’ raw extraction JSON string
has_correction boolean Whether a corrected JSON has been submitted
corrected_json string or null The corrected data, if submitted via PATCH
status string new, reviewed, training, or resolved

Seismic Site Class Prediction (ASCE 7-22)

Predict the ASCE 7-22 seismic site class at any location in the continental US. The endpoint runs Geosetta's ML models to predict soil type and SPT N-values at 1-ft intervals from 0–100 ft, converts to shear wave velocity (Vs) using the Imai & Tonouchi (1982) correlation, computes VS30 via harmonic averaging, and assigns a site class ranging from A (hard rock) to E (soft soil).

Results include lower and upper bound site classes derived from the predicted N-value category ranges, plus a per-depth confidence score based on proximity to training data, geological context, and model agreement.

GET /web_map/api/site_class/<lat>/<lng>/

Authentication: Required — X-API-Key header or session login.

Parameter Type Description
lat float Latitude (decimal degrees)
lng float Longitude (decimal degrees)

Example Request (Python):


import requests

response = requests.get(
    "https://geosetta.org/web_map/api/site_class/38.307/-82.046/",
    headers={"X-API-Key": "your_api_key_here"}
)

data = response.json()
print(f"Site Class: {data['site_class']['lower_bound']} - {data['site_class']['upper_bound']}")
print(f"VS30: {data['vs30_ft_s']['average']} ft/s")
print(f"Confidence: {data['confidence']['average_percent']}%")

Example Request (cURL):


curl -H "X-API-Key: your_api_key_here" \
  "https://geosetta.org/web_map/api/site_class/38.307/-82.046/"

Example Response:


{
  "status": "success",
  "location": {
    "latitude": 38.307,
    "longitude": -82.046,
    "elevation_ft": 892.5,
    "formation": "Kanawha Formation",
    "soil_type": "Silt loam"
  },
  "site_class": {
    "lower_bound": "D",
    "upper_bound": "CD",
    "standard": "ASCE 7-22"
  },
  "vs30_ft_s": {
    "lower_bound": 723.45,
    "upper_bound": 1102.33,
    "average": 912.89
  },
  "profile": {
    "depth_ft": [0, 1, 2, "...", 100],
    "soil_type": ["CLAY", "CLAY", "SILT", "..."],
    "n_value_category": ["1-9", "1-9", "10-25", "..."],
    "n_lower": [1, 1, 10, "..."],
    "n_upper": [9, 9, 25, "..."],
    "vs_lower_ft_s": [318.37, 318.37, 636.12, "..."],
    "vs_upper_ft_s": [637.21, 637.21, 922.45, "..."]
  },
  "confidence": {
    "average_percent": 62,
    "per_depth": [65, 64, 63, "..."]
  },
  "disclaimer": "ML-predicted values for preliminary screening only. Not a substitute for site-specific investigation."
}

Response Fields:

Field Description
site_class.lower_bound Site class from lower-bound N-values (conservative)
site_class.upper_bound Site class from upper-bound N-values
vs30_ft_s Time-averaged shear wave velocity over top 100 ft (ft/s)
profile.depth_ft Depth array (0–100 ft at 1 ft intervals)
profile.soil_type Predicted soil type at each depth (CLAY, SAND, SILT, GRAVEL, ROCK)
profile.n_value_category Predicted SPT N-value range at each depth
profile.vs_lower_ft_s / vs_upper_ft_s Shear wave velocity bounds at each depth (Imai & Tonouchi 1982)
confidence.average_percent Overall prediction confidence (0–100)

Notes:


Vector Tiles — Borehole Data Streaming

Stream Geosetta's full borehole dataset (~500,000 points) into any mapping application using Mapbox Vector Tiles (MVT). This is the most efficient way to display Geosetta data in your own maps — the client only downloads visible tiles, and the binary protobuf format is far smaller than GeoJSON.

Compatible with: MapLibre GL JS, Mapbox GL JS, Deck.gl, QGIS, ArcGIS Online, Leaflet (via plugins), and any client that supports the MVT specification.

GET /web_map/tiles/{z}/{x}/{y}.pbf

Authentication: Required — API key via query parameter or Authorization header.

URL Pattern:


https://geosetta.org/web_map/tiles/{z}/{x}/{y}.pbf?api_key=YOUR_API_KEY

For map clients that set tile URLs as templates, append ?api_key=YOUR_API_KEY to the URL. Alternatively, pass Authorization: Bearer YOUR_API_KEY in request headers (supported by MapLibre GL JS via transformRequest).

Parameter Type Description
z integer (0–22) Zoom level
x integer Tile column
y integer Tile row

Tile Contents:

Each tile contains a single vector layer named boreholes with the following properties:

Property Type Zoom Description
fk string ≥8 Functional key (lat;lon) — use to query detailed data
src string ≥8 Data provider (e.g. VDOT, MDOT)
t string ≥8 trained or untrained
cnt integer <8 Cluster point count (low zoom only)
tcnt integer <8 Trained point count within cluster (low zoom only)

At zoom < 8, points are server-side clustered for performance. At zoom ≥ 8, individual borehole points are returned.

Example: MapLibre GL JS


var apiKey = 'YOUR_API_KEY';

// Add Geosetta borehole data as a vector tile source
map.addSource('geosetta-boreholes', {
    type: 'vector',
    tiles: ['https://geosetta.org/web_map/tiles/{z}/{x}/{y}.pbf?api_key=' + apiKey],
    minzoom: 0,
    maxzoom: 16
});

// Render individual boreholes (zoom >= 8)
map.addLayer({
    id: 'boreholes-points',
    type: 'circle',
    source: 'geosetta-boreholes',
    'source-layer': 'boreholes',
    minzoom: 8,
    paint: {
        'circle-radius': 5,
        'circle-color': ['match', ['get', 't'],
            'trained', '#2ecc71',
            'untrained', '#e67e22',
            '#999999'
        ],
        'circle-stroke-width': 1,
        'circle-stroke-color': '#ffffff'
    }
});

// Render clusters (zoom < 8)
map.addLayer({
    id: 'boreholes-clusters',
    type: 'circle',
    source: 'geosetta-boreholes',
    'source-layer': 'boreholes',
    maxzoom: 8,
    paint: {
        'circle-radius': ['step', ['get', 'cnt'],
            8, 10, 12, 50, 16, 200, 22
        ],
        'circle-color': '#3498db',
        'circle-opacity': 0.7
    }
});

// Click handler: fetch detailed data on click
map.on('click', 'boreholes-points', function(e) {
    var props = e.features[0].properties;
    var fk = props.fk;  // e.g. "37.658736;-78.124825"
    var parts = fk.split(';');

    // Fetch full borehole details from Geosetta
    fetch('https://geosetta.org/web_map/get-point-info/?lat=' +
          parts[0] + '&lng=' + parts[1])
        .then(r => r.json())
        .then(data => {
            new maplibregl.Popup()
                .setLngLat(e.lngLat)
                .setHTML('<b>' + props.src + '</b><br>' + fk)
                .addTo(map);
        });
});

Example: QGIS

Add a vector tile layer via Layer → Add Layer → Add Vector Tile Layer:


URL: https://geosetta.org/web_map/tiles/{z}/{x}/{y}.pbf?api_key=YOUR_API_KEY
Min Zoom: 0
Max Zoom: 16

Example: Python (requests)


import requests

api_key = "your_api_key_here"

# Fetch a single tile (zoom 12, covering central Virginia)
response = requests.get(
    "https://geosetta.org/web_map/tiles/12/1153/1585.pbf",
    headers={"Authorization": f"Bearer {api_key}"}
)

print(f"Tile size: {len(response.content)} bytes")
print(f"Content-Type: {response.headers['Content-Type']}")
# Parse with mapbox-vector-tile library:
# pip install mapbox-vector-tile
import mapbox_vector_tile
tile_data = mapbox_vector_tile.decode(response.content)
boreholes = tile_data.get('boreholes', {}).get('features', [])
print(f"Boreholes in tile: {len(boreholes)}")

Performance Notes:

Combining with Other Geosetta APIs:

Vector tiles provide the spatial index — use the fk (functional key) from tile features to query detailed data via other Geosetta endpoints: