Reference
Data API reference
The public /v1 data API: pull records, change streams, and intelligence events with scoped keys, cursor pagination, a stable envelope, and transparent data-credit metering.
Base URL: https://api.ballastos.com (production) or http://localhost:8000 (development). The current schema version is 2026-07-01and is returned in every response's meta.schema_version.
Authentication
Authenticate with a scoped data-API key in the Authorization header (or X-API-Key):
curl "https://api.ballastos.com/v1/entities?entity_type=project&confidence_min=0.8&limit=25" \
-H "Authorization: Bearer blst_live_..."
- Keys are created in the console under API & developers → Create key. The secret is shown once, stored as a hash, and revocable instantly.
blst_live_…keys read your live data;blst_test_…keys are sandboxed to synthetic demo records (is_demo: true) so you can integrate safely.- Keys carry a scope list; a request without the required scope returns
403naming the missing scope. Optional per-key controls: expiry, IP allowlist (CIDR), monthly request and data-credit allowances. - Console/SDK
aos_…keys are not valid on/v1, and sessions/cookies are never accepted here.
| Field | Type | Default | Description |
|---|---|---|---|
| feeds:read | scope | — | List and read feeds and their specifications |
| entities:read | scope | — | Read records and the /v1/changes stream |
| events:read | scope | — | Read intelligence events |
| sources:read | scope | — | Read the source network and health |
| usage:read | scope | — | Read month-to-date usage and allowances |
| exports:create | scope | — | Create JSON/CSV exports |
| webhooks:read | scope | — | List webhook endpoints |
| webhooks:write | scope | — | Create and delete webhook endpoints |
The response envelope
Every JSON response uses the same shape:
{
"data": [ ... ],
"pagination": { "next_cursor": "MjAyNi0wNy0yMFQxODowMjoxMXw..." },
"meta": {
"request_id": "req_9c2f1ab4d0e3",
"schema_version": "2026-07-01",
"credits_consumed": 25
}
}
pagination.next_cursor— pass it back as?cursor=to fetch the next page.nullmeans the end.limitaccepts 1–100 (default 25); an unparseable cursor returns400.meta.credits_consumed— exactly what this call cost in data credits, so metering is never a surprise.meta.request_id— quote it when contacting support about a specific request.
Data credits
Reads are metered in data credits against your plan's monthly allowance (defaults shown; enterprise pricing can vary):
| Field | Type | Default | Description |
|---|---|---|---|
| basic record | 1 credit | — | Each entity returned by GET /v1/entities |
| enriched record | 5 credits | — | GET /v1/entities/{id} |
| full history | 20 credits | — | GET /v1/entities/{id}?include=history |
| event | 3 credits | — | Each event returned (unverified/machine verified) |
| verified event | 10 credits | — | Events that are multi-source verified or human reviewed |
| change row | 1 credit | — | Each row from GET /v1/changes |
| export row | 1 credit | — | Each row in a POST /v1/exports result |
Note
Allowance exhaustion fails before data is returned:429 when the monthly request allowance is spent, 402 when the data-credit allowance is spent (organization plan or per-key cap). Every call also meters one api_request.Endpoints
/v1/feedsList your organization's feeds (non-archived) with status, freshness, schema, and geography. Scope: feeds:read.
/v1/feeds/{feed_id}One feed, including its full monitoring specification. Scope: feeds:read.
/v1/entitiesCanonical records, newest first, with cursor pagination. Scope: entities:read. 1 credit per record.
| Field | Type | Default | Description |
|---|---|---|---|
| feed_id | string | — | Restrict to one feed |
| entity_type | string | — | project, permit, company, property, contract, product, listing, … |
| geography | string | — | Matches city, region (state), or country |
| confidence_min | number | — | Only records at or above this confidence (0–1) |
| updated_since | ISO 8601 | — | Records updated at or after this instant |
| created_since | ISO 8601 | — | Records first observed at or after this instant |
| limit | int | 25 | Page size, 1–100 |
| cursor | string | — | Opaque cursor from pagination.next_cursor |
{
"id": "a1b2c3d4e5f6",
"feed_id": "fd_123",
"entity_type": "project",
"canonical_name": "Lakeview Medical Office Expansion",
"aliases": [],
"attributes": {
"project_name": "Lakeview Medical Office Expansion",
"property_address": "4200 Lakeview Dr, Orlando, FL 32803",
"developer": "Meridian Health Partners",
"permit_number": "BLD-2026-041877",
"project_value": 2400000,
"project_stage": "permit_approved",
"general_contractor": null,
"contractor_status": "not_yet_identified",
"opportunity_score": 87
},
"external_ids": { "permit_number": "BLD-2026-041877" },
"geography": { "city": "Orlando", "region": "FL", "country": "US", "postal_code": "32803" },
"status": "active",
"verification_status": "multi_source_verified",
"confidence": 0.95,
"version": 3,
"source_evidence": [
{
"source_id": "src_88a1",
"source_name": "Orange County Building Permits (Demo)",
"url": "https://demo.ballastos.com/sources/orange-county-permits/bld-2026-041877",
"retrieved_at": "2026-07-20T18:02:11Z",
"synthetic": true
}
],
"first_observed_at": "2026-07-14T09:12:44Z",
"last_observed_at": "2026-07-20T18:02:11Z",
"last_verified_at": "2026-07-20T18:02:11Z",
"is_demo": true
}
/v1/entities/{id}One record with full detail (5 credits). Add ?include=history for the complete version history (20 credits).
/v1/changesThe attribute-level change stream: every versioned change across your records, newest first. Scope: entities:read. 1 credit per row.
| Field | Type | Default | Description |
|---|---|---|---|
| feed_id | string | — | Restrict to one feed |
| updated_since | ISO 8601 | — | Changes recorded at or after this instant |
| limit | int | 25 | Page size, 1–100 |
| cursor | string | — | Opaque cursor |
/v1/eventsIntelligence events, newest first. Scope: events:read. 3 credits per event, 10 per verified event. Returned events are marked delivered.
| Field | Type | Default | Description |
|---|---|---|---|
| feed_id | string | — | Restrict to one feed |
| event_type | string | — | permit_filed, permit_approved, bid_opened, contract_awarded, contractor_identified, price_reduced, … |
| confidence_min | number | — | Minimum confidence (0–1) |
| created_since | ISO 8601 | — | Detected at or after this instant |
| limit | int | 25 | Page size, 1–100 |
| cursor | string | — | Opaque cursor |
{
"id": "ev_71c9d2",
"feed_id": "fd_123",
"event_type": "permit_approved",
"occurred_at": "2026-07-20T18:02:12Z",
"detected_at": "2026-07-20T18:02:12Z",
"primary_entity_id": "a1b2c3d4e5f6",
"summary": "Permit approved: Lakeview Medical Office Expansion in Orlando, FL ($2,400,000)",
"relevance_score": 0.95,
"opportunity_score": 87.5,
"confidence": 0.95,
"verification_status": "multi_source_verified",
"match_reason": "Matched because signal 'approved' is in your watch list; value $2,400,000 meets your $500,000 threshold; located in Orlando, FL; contractor not yet identified.",
"source_evidence": [ { "source_id": "src_88a1", "...": "..." } ],
"is_demo": true
}
/v1/events/{id}One event with evidence and match reason. Scope: events:read.
/v1/sourcesYour source network: type, jurisdiction, cadence, permission and license status, reliability score, and health. Scope: sources:read. No credits.
/v1/usageMonth-to-date usage (requests, records, credits, webhook deliveries, source checks) against plan allowances, plus this key's own usage. Scope: usage:read.
/v1/exportsSynchronous export of up to 10,000 rows as JSON or CSV (text/csv attachment). Scope: exports:create. 1 credit per row; fires export.completed to subscribed webhooks.
{
"kind": "entities", // or "events"
"format": "json", // or "csv"
"feed_id": "fd_123" // optional
}
/v1/webhooksList webhook endpoints. Scope: webhooks:read.
/v1/webhooksCreate an endpoint: {url, events: [...]}. Returns the whsec_ signing secret exactly once. Scope: webhooks:write.
/v1/webhooks/{id}Delete an endpoint and its pending deliveries. Scope: webhooks:write.
Webhooks
Ballast pushes intelligence to your systems as it lands. Event types: entity.created, entity.updated, intelligence_event.created, intelligence_event.verified, feed.status_changed, source.failed, source.recovered, usage.threshold_reached, export.completed — plus ping for tests.
{
"id": "evt_ab12cd34ef56",
"type": "intelligence_event.created",
"created_at": "2026-07-20T18:02:12+00:00",
"data": {
"feed_id": "fd_123",
"intelligence_event_id": "ev_71c9d2",
"event_type": "permit_approved",
"summary": "Permit approved: Lakeview Medical Office Expansion in Orlando, FL ($2,400,000)",
"opportunity_score": 87.5,
"confidence": 0.95
}
}
Every delivery is signed. The X-Ballast-Signature header has the form t=<unix>,v1=<hex> where v1 is the HMAC-SHA256 of {t}.{raw_body} using your endpoint's whsec_… secret:
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts = int(parts["t"])
if abs(time.time() - ts) > tolerance:
return False # replay window exceeded
expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
- Delivery is at-least-once per endpoint per event id; use the
X-Ballast-Deliveryheader (stable across retries) for idempotency. - Non-2xx responses and timeouts retry with exponential backoff (30s doubling, capped at 1 hour) up to 8 attempts, then dead-letter. The console shows delivery history with manual retry, and secrets are rotatable.
- Five consecutive failures mark the endpoint
failing; it recovers automatically on the next success.
Errors
| Field | Type | Default | Description |
|---|---|---|---|
| 400 | status | — | Invalid cursor or parameters |
| 401 | status | — | Missing, invalid, revoked, or expired key (or a non-blst_ credential) |
| 402 | status | — | Monthly data-credit allowance exhausted |
| 403 | status | — | Missing scope, or request IP not in the key's allowlist |
| 404 | status | — | Not found — including ids that belong to another organization |
| 422 | status | — | Invalid body (exports, webhooks) |
| 429 | status | — | Monthly request allowance exhausted, or platform rate limit |
Error bodies are {"detail": "human-readable reason"}.
The operations API is separate
The console and orchestration surface (/api/… — feeds administration, monitoring pipelines, approvals, audit) uses session or role-key auth and is not part of this public contract. In development the backend serves its interactive OpenAPI docs at http://localhost:8000/docs.Note
SDKs for the data API are planned. The existing Python/TypeScript SDKs target the operations API (pipelines and runs), not/v1.