Nexus
Documentation / Connectors

Generic HTTP/REST Connector

Integrate any REST or GraphQL API as a first-class Nexus connector by authoring sandbox-compiled build_request and parse_response scripts, with OAuth2/auth resolved through the bridge and SSRF-checked egress.

The generic HTTP/REST connector is Nexus's answer to "we need data from an API you don't have a native adapter for." Instead of waiting for Stingray to ship a bespoke integration, you author two small scripts — one that builds the outbound request for a query, one that parses the response into rows — and register them as a bridge. From that point the API behaves like any other connector: it is reachable through UQL, ctx.adapters, discovery, RBAC, and the capability model, with credentials held in Key Vault and every request SSRF-checked. This page is for engineers integrating a REST or GraphQL API that has no native connector.

When to use this connector (and when not to)

You want to… Use
Expose a third-party REST/GraphQL API as a governed, reusable data source for reports, pipelines, and AI agents Generic HTTP/REST connector
Integrate a SQL database with a JDBC driver Generic JDBC connector
Make a one-off, ad-hoc call from inside a single pipeline (a webhook, a status poke) pipeline ctx.http
Integrate a well-known system (NetSuite, QuickBooks, Monday, a SQL database) the native connector — less to author, more capability built in

The distinction between this connector and ctx.http matters and is covered in full below. In short: the connector is a durable, shared, credentialed data source; ctx.http is a raw client for one pipeline's private logic.

How it works

A generic HTTP/REST bridge is configured with a base URL, an authentication method, and two author-supplied scripts:

  • build_request — receives the incoming UQL query (collection, where, limit, offset, ordering, and — for writes — the record payload) and returns the HTTP request to make: method, path, query string, headers, and body. This is where you translate UQL into the API's own query conventions and where pagination is expressed.
  • parse_response — receives the raw HTTP response and returns the list of row dicts (for reads) or the write result. This is where you dig the records out of whatever envelope the API wraps them in (data, results, items, a GraphQL data.board.items path) and normalize field names.

Both scripts are compiled under the same sandbox as pipelines: an import allowlist, no filesystem access, no raw sockets, no exec/eval. They receive data and a small helper surface and return data. They cannot open their own network connections — all egress goes through the governed HTTP client, which enforces the SSRF checks and the tenant egress allowlist described below. A script that tries to reach outside that boundary fails at compile or call time, not silently.

Authentication resolved through the bridge

You do not put tokens in your scripts. The bridge resolves credentials from your tenant's Key Vault and injects them into the request context, so build_request reads a resolved value rather than a raw secret. Supported shapes include:

  • OAuth2 client-credentials — the bridge fetches and caches an access token, refreshing it before expiry, and exposes it to build_request for the Authorization header.
  • Bearer / static token — a stored secret presented as Authorization: Bearer <token>.
  • API-key header — an arbitrary header name/value (some APIs use X-API-Key, or a raw Authorization value with no Bearer prefix).

Because auth is bridge-resolved, rotating a credential is a Key Vault operation — no script edit, no redeploy — and the token never appears in report definitions, pipeline code, or logs.

SSRF-checked, allowlisted egress

Every request the connector makes passes the same egress guard as the rest of the platform: the destination is validated against the tenant's app-layer egress allowlist, and the resolved IP is checked to block private, link-local, and metadata-endpoint ranges (the DNS-rebind-resistant IP-pinning check). This holds at the network layer too — the tenant VNet's NSG constrains outbound traffic. The connector cannot be turned into a server-side request forgery primitive by a crafted build_request.

Capabilities

A generic HTTP/REST bridge declares its capabilities like any other connector, and you set them to match what you actually implement in your scripts:

  • Read — always, once build_request/parse_response handle reads.
  • Write — insert/update/delete if you implement the write branch of build_request and the API supports it.
  • Discover — you can implement schema discovery to enumerate the API's resources, or leave collections author-specified.
  • Pushdown — you declare which where operators and which ordering/paging your build_request genuinely translates into the API's query parameters. Anything you don't push down, the engine applies in memory over the returned rows (bounded by the row budget), exactly as with any low-pushdown native connector. Declaring pushdown you haven't implemented would produce wrong results — declare only what you translate, and let the engine handle the rest correctly. See the pushdown semantics in the overview.

Worked example: a paginated REST API → rows

Suppose you're integrating a project-tracking API at https://api.example.com/v2, where issues live at GET /issues, results are wrapped in { "data": [...], "next_cursor": "..." }, and the API takes status, limit, and cursor query params. You want a bridge that reads issues and pushes down an equality filter on status.

build_request — translate the UQL query into the API's request, following the cursor for pagination:

def build_request(query, auth, page):
    params = {"limit": min(query.get("limit", 100), 200)}

    # Push down an equality filter on status; everything else is left to the engine.
    where = query.get("where", {})
    status = where.get("status", {}).get("$eq")
    if status:
        params["status"] = status

    # page.cursor is None on the first call, then whatever parse_response returned.
    if page.cursor:
        params["cursor"] = page.cursor

    return {
        "method": "GET",
        "path": "/issues",
        "params": params,
        "headers": {"Authorization": f"Bearer {auth.access_token}"},
    }

parse_response — pull rows out of the envelope and hand back the next cursor so the engine keeps paging until it has enough:

def parse_response(response):
    body = response.json()
    rows = [
        {
            "id": item["id"],
            "title": item["title"],
            "status": item["status"],
            "assignee": item.get("assignee", {}).get("email"),
            "updated_at": item["updated_at"],
        }
        for item in body.get("data", [])
    ]
    return {"rows": rows, "next_cursor": body.get("next_cursor")}

With pushdown-filter declared for $eq on status, a UQL query like where status $eq "open" limit 500 sends status=open to the API and pages until it has 500 rows. A filter the scripts don't translate — say $contains on title — is still honored: the engine applies it in memory over the fetched rows, so the answer is correct even though the API never saw that predicate.

Reached from a pipeline, the bridge is just another adapter:

async def run(ctx):
    issues = await ctx.adapters.example_issues.read({
        "collection": "issues",
        "where": {"status": {"$eq": "open"}},
        "limit": 500,
    })
    await ctx.workspace.upsert("open_issues", issues.rows, keys=["id"])
    ctx.logger.info("staged %d open issues", len(issues.rows))

Relationship to ctx.http

Both go through the same SSRF-safe, allowlisted egress path — but they serve different jobs.

Generic HTTP/REST connector ctx.http
What it is A registered bridge / data source A raw HTTP client injected into one pipeline
Reusability Shared across reports, pipelines, AI agents Private to the pipeline that calls it
Query surface Full UQL: where, pushdown, limit, discovery Whatever you code by hand
Credentials Bridge-resolved from Key Vault, cached/refreshed You fetch secrets via ctx.secrets and set headers yourself
Governance RBAC-scoped as a connector resource; appears in audit as a bridge Governed as pipeline egress
Best for A durable integration many things depend on A one-off call inside bespoke pipeline logic

Use ctx.http when the call is incidental to one pipeline's logic — posting a notification, poking a webhook, a single lookup. Promote it to a generic HTTP/REST connector the moment more than one thing needs the data, you want it queryable via UQL, or you want it governed and auditable as a first-class source.

What this does and doesn't do

  • It does give you a governed, credentialed, SSRF-checked, RBAC-scoped, UQL-queryable data source for any REST or GraphQL API, without a native adapter.
  • It does let you declare exactly the pushdown you implement, with the engine correctly handling everything else in memory.
  • It does not magically infer an API's shape — you author the request/response mapping. The scripts are small, but they are yours to write and maintain.
  • It does not grant scripts unrestricted network or filesystem access — they are sandbox-compiled and can only egress through the governed client.
  • For heavy analytical workloads over a low-pushdown API, stage into the internal workspace and aggregate there rather than pulling large row sets repeatedly.

See also