/polaris/api/catalog. StarTree reaches it through the generic spec-compliant adapter — catalogType=iceberg-rest, serviceType=rest. Data files must be Parquet.
Running self-managed Apache Polaris or Snowflake Open Catalog instead? Those use principalclient-id/client-secretcredentials — see Apache Polaris & Snowflake Open Catalog: Onboarding via API.
How Horizon differs from Apache Polaris
Horizon speaks the Polaris REST protocol, but authentication and identifier mapping follow Snowflake’s model, not Polaris’s:Why the PAT goes in
catalog.iceberg-rest.credential. Horizon’s token endpoint expects a client_credentials exchange that sends the PAT as client_secret with no client_id. Passing the PAT through catalog.iceberg-rest.credential produces exactly that request. (Setting oauthClientSecret with an empty oauthClientId does not currently work — the empty client id causes the credential to be dropped and the catalog call goes out unauthenticated. Use catalog.iceberg-rest.credential until first-class secret-only support lands.)How it works
Onboarding is four calls. Each one feeds the next:All paths are relative to your data-plane base URL. Set
export BASE_URL=https://dp.<data-plane-id>.cp.<region>.startree.cloud/api/pinot (the StarTree Cloud data-plane proxy). A browse call then posts to $BASE_URL/connections/browse.If your controller requires authentication, add an Authorization header to every request — e.g. -H "Authorization: Bearer <token>". The examples below omit it for brevity.Prerequisites
- StarTree 0.16.0 or later with the External Table Beta feature enabled, and tiered storage configured on the cluster. Contact StarTree support if unsure.
- Network access to the controller REST endpoint, plus an
Authorizationtoken if your cluster requires auth. - A Snowflake account with Horizon Catalog and the target Iceberg table backed by an external volume (S3).
- A Snowflake service user with a programmatic access token (PAT), a role that can read the table, and a network policy governing the user (see Snowflake setup below).
- The Snowflake database (used as both
warehouseandprefix), the schema (the namespace), and the table to onboard. - Read access to the underlying object storage. This connector supports S3-compatible storage for the data files.
Snowflake setup
Run these once in Snowflake (or have an ACCOUNTADMIN do so). Replace the placeholders with your own names.The
ROLE_RESTRICTION role must match the role named in the session:role:<role> scope you configure below, and it must hold the grants from step 1. Without a network policy in effect, ADD PAT fails and the token endpoint later returns 401 unauthorized_client.Authentication
An External Table authenticates in two places, configured independently:- Catalog (REST) — the Snowflake PAT, supplied via
catalog.iceberg-rest.credentialplus the token URI and scope. - Storage (S3 data files) — keys under
catalog.iceberg-rest.auth.storage.*. Choose one of the methods below.
Catalog (REST) authentication
Pass the PAT as the catalog credential. StarTree exchanges it at the Snowflake token endpoint for a short-lived token, which it caches and refreshes automatically.The PAT is a secret — treat the table config as sensitive and rotate the token on the
DAYS_TO_EXPIRY cadence. A static PAT is not auto-rotated; when it expires, syncs fail until you update the table config with a new one. Key-pair JWT auth (a signed JWT in place of the PAT) works the same way — put the JWT in catalog.iceberg-rest.credential.Storage authentication: choose one
Static access keys
What most Snowflake external-volume setups use. Accepts eitheraccessKeyId/secretAccessKey or the shorter accessKey/secretKey aliases.
Assumed IAM role (recommended for production)
No static secrets in the table config — every S3 read assumesroleArn via STS.
Cluster node role
Omit everyauth.storage.* credential key — only region is required. Credential resolution falls back to the AWS SDK default chain (the node’s instance profile / IRSA role), which must already have S3 access.
Horizon can vend short-lived storage credentials, but the generic
rest adapter does not consume vended credentials — configure static keys, an assumed role, or the cluster’s node role for storage explicitly.Selecting the database
Set bothcatalog.iceberg-rest.warehouse and catalog.iceberg-rest.prefix to the Snowflake database name. prefix is required for Horizon: the generic adapter doesn’t send the warehouse query parameter that Snowflake’s /v1/config gates on, so without an explicit prefix, browse falls back to the un-prefixed /v1/namespaces path and fails with 404 Unable to find matching target resource method.
catalog.iceberg-rest.table.namespace to the schema name and catalog.iceberg-rest.table.tableName to the table.
Step 1: Validate and browse the connection
POST /connections/browse
There is no separate “validate” endpoint. Browsing the catalog is the validation step: a 200 with an items list (even an empty one) confirms your PAT, scope, and connectivity.
- Set
pathto""to browse the root — this both validates the connection and lists schemas (namespaces). - Set
pathto a schema name to list the tables inside it.
Request
Response
Step 2: Preview the schema
POST /tables/preview
Samples the source, infers a Pinot schema, and returns an enriched table config (S3 tier, raw field configs, time column) plus sample rows. Review it, tweak if needed, then carry the schema and config forward to steps 3 and 4.
The request and response share the same JSON shape. You send a
tableConfig describing the source; the response fills in schema, the enriched tableConfigs.offline, sampled rows, and a summary.Request
Top-level fields:config.inference — how the schema is derived:
Resolution order: explicit
schema → embedded schema → inferred schema.
config.sampling — how rows are sampled:
Setting the namespace and table. Take them from the browse response — set
catalog.iceberg-rest.table.namespace (the Snowflake schema) and .tableName to the NAMESPACE and TABLE you selected.Use executor: controller — it’s required for the controller-watcher flow and the observability endpoints. The input tableConfig is intentionally minimal; /tables/preview returns the complete tableConfigs.offline you persist in Step 4.Response
Adjust the schema (time column, column names, null handling) before moving on.
Step 3: Create the schema
POST /schemas
Send the schema object from the preview response. Rename its schemaName to match your table.
Step 4: Create the table
POST /tables
Send the enriched tableConfigs.offline object from the preview response. Its ExternalTableSyncTask block is what marks the table as external.
schedule (cron) interval. There is no separate start call.
Quickstart: onboard a table end-to-end
The whole flow as one script. Fill in the variables at the top, run the script, and the first sync starts automatically.0, verify it’s queryable.
Monitor onboarding
The status endpoint reports each sync run’s progress, and?includeLag=true adds how far the table trails the upstream catalog. Both require executor=controller (set automatically by this flow). See Observability for full request and response details.
Verify it’s queryable
Whenstatus is COMPLETED and segmentsUploaded matches filesDiscovered, run a query against the broker (or the Data Portal query console) to confirm the data is live:
summary.nSourceRows (preview only samples up to ~100 rows) — confirm it’s non-zero and plausible for your dataset. If status is COMPLETED but the count is 0, give segments a moment to load on the servers, then recheck; if it persists, see Troubleshooting.
What’s next
Now that the table is created and data is loading, these are the highest-impact follow-up steps:- Add indexes for your query patterns. Without indexes, every query scans all remote Parquet data. Add a range index on time/numeric columns, an inverted index on low-cardinality filter columns, and a bloom filter on high-cardinality ID columns. → Indexes
-
Enable caching and preload. Set
enable.prefetch.page.cache=trueandpreload.enable=trueon the S3 tier so index data is served from local disk on repeated queries instead of re-fetched from S3. → Data and Index Caching - Protect large-scan queries from OOM. For tables that receive heavy aggregations or wide scans, enable the query OOM killer so a runaway query is killed instead of crashing the server. → Best Practices & Configs — Query OOM protection
-
Monitor ongoing syncs. Use the status endpoint (with
includeLag=truefor ingestion lag) after each scheduled sync. → Observability
For common questions and failures, see the FAQ and Troubleshooting.

