RevOpsQL
Home Documentation Pricing Contact us

HubStream API — Developer Reference

The HubStream API lets you submit RevOpsQL queries against your HubSpot portal, track execution progress, and retrieve results — all programmatically.

OpenAPI 3.0 specDownload YAML · View interactive reference (Redoc)


Table of contents

  1. Authentication
  2. Rate limits
  3. Endpoints
  4. Execution states
  5. Error reference
  6. Workflow example

Authentication

Every request requires an API key issued from the HubStream dashboard (Portals → API Keys → New API Key).

Authorization: Bearer hs_your_api_key_here

The key is tied to a specific portal. All requests are automatically scoped to that portal — there is no need to pass a portal ID.

Plan requirement — API access is available on Growth, Scale, and Enterprise plans only. Starter plan accounts receive a 403 plan_restriction on every API call. The API Keys menu is also hidden in the dashboard for Starter accounts.

Condition HTTP Error
Header absent 401 Authorization: Bearer <key> required
Key invalid / revoked / expired 401 Invalid or expired API key
Portal inactive 403 Portal is inactive
Account inactive 403 Account is inactive
Starter plan 403 plan_restriction + lien upgrade

Rate limits

Limits are enforced per portal (shared across all keys of the same portal).

Limit Default
Full-run submissions / hour 20
Dry-run submissions / hour 60
Concurrent executions 3

Successful submission responses (202) include rate-limit headers:

Header Description
X-RateLimit-Limit Window maximum
X-RateLimit-Remaining Submissions left in this window
X-RateLimit-Reset Unix timestamp of window reset

429 — Burst limit reached:

{
  "error": "rate_limit_exceeded",
  "message": "Submission rate limit reached for this portal. Try again later.",
  "retry_after": 1847
}

429 — Too many concurrent executions:

{
  "error": "concurrent_limit_exceeded",
  "message": "Too many concurrent executions for this portal. Wait for a running query to complete.",
  "active_count": 3,
  "limit": 3
}

Endpoints

POST /api/queries — Submit a query

POST https://app.hubstream.io/api/queries
Authorization: Bearer hs_...
Content-Type: application/json

{
  "sql": "SELECT firstname, lastname, email FROM contact WHERE jobtitle = 'CTO' LIMIT 100 RECENTLY_CREATED",
  "dry_run": false
}
Field Type Required Description
sql string RevOpsQL query. Max 10 000 characters.
dry_run boolean Estimates matching objects without writing results. Default: false.

Response 202 Accepted

{ "execution_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }

GET /api/queries/{id} — Execution status

GET https://app.hubstream.io/api/queries/a1b2c3d4-...
Authorization: Bearer hs_...

Response 200 OK

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "state": "completed",
  "processed_counter": 12450,
  "objects_processed": 12450,
  "objects_returned": 348,
  "objects_billed": 348,
  "api_calls_used": 26,
  "duration_ms": 4812,
  "error_message": null,
  "submitted_at": "2026-05-21T09:00:00Z",
  "started_at": "2026-05-21T09:00:02Z",
  "completed_at": "2026-05-21T09:00:06Z",
  "results_expires_at": "2026-05-28T09:00:06Z"
}

GET /api/queries/{id}/results — Download URL

Returns a pre-signed S3 URL (valid 1 hour) to download the result file. Only available when state = completed.

{
  "url": "https://s3.example.com/results/a1b2c3d4.json?X-Amz-Signature=...",
  "expires_in": 3600
}
Status Meaning
409 Conflict Execution not yet completed
410 Gone Results have expired

GET /api/saved-queries — List saved queries

Returns all saved queries for this portal, ordered by name.

[
  {
    "id": 42,
    "name": "CTOs in France",
    "description": "All contacts with jobtitle CTO in France",
    "query_text": "SELECT firstname, lastname FROM contact WHERE jobtitle = 'CTO' LIMIT 500 RECENTLY_CREATED",
    "created_at": "2026-05-01T10:00:00Z",
    "updated_at": "2026-05-15T14:30:00Z"
  }
]

POST /api/saved-queries/{id}/execute — Run a saved query

Submits a saved query using its stored SQL. Same rate limits as POST /api/queries.

POST https://app.hubstream.io/api/saved-queries/42/execute
Authorization: Bearer hs_...
Content-Type: application/json

{ "dry_run": false }

Response 202 Accepted

{
  "execution_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "saved_query_id": 42,
  "saved_query_name": "CTOs in France"
}

GET /api/quota — Rate-limit quota

Returns the current rate-limit state for this portal.

{
  "submissions": { "limit": 20, "remaining": 17, "reset_at": 1748812345 },
  "dry_runs":    { "limit": 60, "remaining": 58, "reset_at": 1748812345 },
  "concurrent":  { "limit": 3,  "active": 1,    "available": 2 }
}

GET /api/schema — Portal HubSpot schema

Returns the cached HubSpot CRM schema for this portal: object types, properties, and associations.

{
  "contact": {
    "label": "Contact",
    "properties": {
      "firstname": { "label": "First name", "type": "string" },
      "email":     { "label": "Email",      "type": "string" }
    }
  }
}

Returns 404 if the schema has not been generated yet. Trigger a schema sync from the HubStream dashboard.


Execution states

State Description
pending Queued — not yet started
in_progress Processing. processed_counter updates in real time.
completed Done. Results available.
error Failed. See error_message.

Recommended polling interval: 2 seconds.


Error reference

HTTP error value Cause
401 Authorization: Bearer <key> required Missing header
401 Invalid or expired API key Bad / revoked key
403 Portal is inactive Portal deactivated
403 Account is inactive Account suspended
403 plan_restriction Starter plan — upgrade to Growth or higher
404 (varies) Resource not found
409 Execution not completed yet Results requested too early
410 Results have expired Result file deleted
422 (validation errors) Invalid request body
429 rate_limit_exceeded Hourly burst limit hit
429 concurrent_limit_exceeded Too many simultaneous executions

Workflow example

Python — Submit, poll, download

import time, httpx

BASE    = "https://app.hubstream.io/api"
HEADERS = {"Authorization": "Bearer hs_your_key"}

# 1. Submit
r = httpx.post(f"{BASE}/queries", headers=HEADERS, json={
    "sql": "SELECT firstname, lastname FROM contact LIMIT 50 RECENTLY_CREATED"
})
execution_id = r.json()["execution_id"]

# 2. Poll
while True:
    status = httpx.get(f"{BASE}/queries/{execution_id}", headers=HEADERS).json()
    if status["state"] == "completed":
        break
    if status["state"] == "error":
        raise RuntimeError(status["error_message"])
    time.sleep(2)

# 3. Download
result_url = httpx.get(f"{BASE}/queries/{execution_id}/results", headers=HEADERS).json()["url"]
data = httpx.get(result_url).json()
print(f"{len(data)} records — first: {data[0]}")

Check quota before a batch

quota = httpx.get(f"{BASE}/quota", headers=HEADERS).json()

if quota["submissions"]["remaining"] < 5:
    wait = quota["submissions"]["reset_at"] - int(time.time())
    time.sleep(wait + 1)

if quota["concurrent"]["available"] == 0:
    print("All slots busy — wait for a running query to complete")