This Streamlit app is a management UI for a config-driven API ingestion pipeline in Snowflake. It lets you register external REST APIs, manage credentials and network access, run ingestion (full or incremental), monitor every retry, and turn the landed JSON into flattened SQL views — all without writing SQL.
┌──────────────────────┐ ┌───────────────────────────┐ ┌──────────────────────────┐
│ INGESTION_CONFIGS │─────▶│ USP_UNIVERSAL_INGESTOR │─────▶│ LANDING_TABLE (config) │
│ (what to call) │ │ (calls the API) │ │ or API_RAW_DATA (default)│
├──────────────────────┤ ├───────────────────────────┤ ├──────────────────────────┤
│ API_ID (surrogate) │ │ Reads config │ │ API_ID (FK) │
│ API_NAME (unique) │ │ Authenticates │ │ API_NAME │
│ ENDPOINT_URL │ │ Paginates │ │ STATUS_CODE │
│ AUTH_TYPE │ │ Applies watermark │ │ PAYLOAD (VARIANT) │
│ SECRET_NAME │ │ Deduplicates (SHA-256) │ │ PAYLOAD_HASH │
│ LANDING_TABLE │ │ Routes to landing table │ │ INGEST_TS │
│ PAGINATION_TYPE │ │ Logs every retry │ │ │
│ INCREMENTAL_FLAG │ │ Persists new watermark │ └──────────────────────────┘
│ WATERMARK_* │ └───────────────────────────┘
└──────────────────────┘ │
▼
┌───────────────────────────────┐
│ INGESTION_RESPONSE_LOG │
│ One row per ATTEMPT (retry- │
│ level): status, error, time, │
│ ATTEMPT_NUMBER, IS_FINAL, │
│ PAGE_NUMBER, API_ID │
└───────────────────────────────┘
The API's hostname must be whitelisted for Snowflake to reach it.
- Go to Tab 2 → Step 1: Network Rules
- Enter:
- Rule Name:
NR_EXAMPLE_API - Allowed Hosts:
api.example.com(or*.example.comfor wildcard)
- Rule Name:
- Click Create Network Rule
- The EAI is automatically rebuilt — the new network rule is added to
EAI_UNIVERSAL_INGESTORalong with all existing rules and secrets.
Naming convention for security integrations: any integration created via Step 2: Security Integrations must start with
SEC_INT_API_(e.g.SEC_INT_API_STRIPE). The framework uses this prefix to filter out unrelated account-level integrations (Snowsight OAuth, SCIM, SSO, vendor integrations) from the dropdown shown in Step 3 OAUTH2 secret creation.
| API Auth Method | Secret Type | What to Enter |
|---|---|---|
| API key in header | GENERIC_STRING | The API key string |
| OAuth2 client credentials | PASSWORD | Username = Client ID, Password = Client Secret |
| Snowflake-managed OAuth2 | OAUTH2 | Pick the security integration when creating the secret |
| No auth needed | Skip this step | — |
Example for an API key:
- Select Secret Type:
GENERIC_STRING - Enter:
- Secret Name:
SK_EXAMPLE_API - Secret String: (paste your API key)
- Secret Name:
- Click Create Secret — the EAI auto-rebuilds.
- Go to Tab 1 → Add New API Config
- Enter
API_NAMEandENDPOINT_URL - Live Network-Rule Validation runs as you type the URL:
- ✅ Green caption:
Host covered by network rule: `api.example.com` (rule: NR_EXAMPLE_API) - ⚠ Warning + parsed host + first 8 allowed entries if not covered. The Add Config button hard-blocks unless you tick "Acknowledge gap — save anyway".
- ✅ Green caption:
- Select AUTH_TYPE — the form dynamically shows only relevant fields:
| AUTH_TYPE | Fields Shown |
|---|---|
NONE |
No auth fields |
API_KEY |
SECRET_NAME (dropdown), API_KEY_HEADER |
OAUTH2_BASIC |
SECRET_NAME (dropdown), TOKEN_URL |
OAUTH2_INTEGRATION |
SECRET_NAME (dropdown), EXTRA_HEADERS_JSON (integration is bound to the secret automatically) |
SECRET_NAME is a dropdown filtered by the correct type. If no matching secrets exist, you'll see "+ Create Secret →" with a link back to Tab 2.
- Fill in remaining fields:
| Field | Value | Notes |
|---|---|---|
| LANDING_TABLE | EXAMPLE_RAW |
Pick from dropdown, type a new name, or leave default API_RAW_DATA |
| PAGINATION_TYPE | PAGE / OFFSET / NONE |
PAGE_PARAM and START_INDEX appear only when not NONE |
| INCREMENTAL SYNC | (checkbox) | See Incremental Sync section below |
| MAX_RETRIES / RETRY_DELAY / TIMEOUT | 6 / 15 / 30 |
Sensible defaults |
- Click Add Config — the ingestor procedure auto-rebuilds with the new secret.
- Go to Tab 3, select the API, click Run Ingestion.
- Watch the progress bar; the success message includes the watermark transition (e.g.
Watermark: 2025-01-01 → 2025-05-25) when incremental is on.
- Tab 4 (Ingestion Console) — every retry attempt is logged individually; see Retry Drill-Down below.
- Tab 5 (Data Explorer) — browse landed JSON (Raw Payloads sub-tab) and discover schema / generate flattened views (Schema Discovery sub-tab) — both share the same landing-table + API filter.
Same as before — interval or CRON, with auto-suspend on N failures.
Avoid re-pulling the entire dataset every run. The framework filters by a value the API exposes (e.g., updated_at) and persists the new high-water mark after each successful run.
In the Add Config form, check "Enable Incremental Sync" and provide:
| Field | Example | What it does |
|---|---|---|
| WATERMARK_PARAM | since |
Query-string parameter to send (e.g. ?since=...). Maps to whatever the API expects: since, modified_after, updated_after. |
| WATERMARK_FIELD | updated_at |
Dotted JSON path inside each record (e.g. updated_at or meta.updated_at) — the procedure walks every record and tracks the running max. |
| Initial LAST_SYNC_VALUE | 2025-01-01T00:00:00Z (optional) |
Seed value for the first run. Leave blank to fetch all data on the first run. |
- First run — if seeded, hits
?since=<seed>; if not, fetches everything. - During run — every record's
WATERMARK_FIELDis inspected; the running max is tracked. - After success — the procedure runs
UPDATE INGESTION_CONFIGS SET LAST_SYNC_VALUE = <new_max>. - Failure — watermark is not persisted (no data gaps).
- The success message reports
Watermark: <old> -> <new>.
In the INCREMENTAL SYNC CONTROL section under each API:
- Override LAST_SYNC_VALUE — type a value and click Apply Watermark to backfill from a chosen point.
- Clear (Full Backfill) — sets
LAST_SYNC_VALUE = NULLso the next run does a complete reload.
Watermark comparison is numeric-aware (falls back to string compare). ISO-8601 timestamps sort correctly out of the box.
A single tab combining JSON browsing and schema modeling. Pick the landing table and API filter once at the top — both sub-tabs use them.
Read-only inspector. Adjust Max rows and Show full payload toggle.
- Top-line metrics: records shown, total in table, distinct APIs.
- Click any payload hash to expand the full JSON via Payload Inspector.
- Use this to confirm ingestion happened, debug "did the data arrive?", and spot bad payloads.
Turn raw JSON into queryable columns.
- Set the Records JSON Path (default
datamatches the framework's chunked payload — leave blank ifPAYLOADitself is one record) and Sample Size. - Click Discover Schema — every record is walked, leaf paths collected, types inferred (
TIMESTAMP_TZ,NUMBER,BOOLEAN,STRING,FLOAT,OBJECT,ARRAY). - Review the schema table:
| Column | Meaning |
|---|---|
PATH |
Dotted JSON path |
TYPE |
Best-fit Snowflake type (or STRING fallback) |
COVERAGE_% |
% of sampled records that contain this field |
OBSERVED_TYPES |
All types seen for this path |
- Use the Quality Check expander to spot fields with < 100% coverage (catches API drift / removed fields).
- Choose Type Strategy:
- Inferred — casts to detected types (faster downstream, riskier)
- All STRING (safe) — never errors, cast as you query
- Click Create / Replace View — generates
LATERAL FLATTEN(input => PAYLOAD:<records_path>)view, e.g.:
CREATE OR REPLACE VIEW API_DATA_PIPELINE.RAW_LANDING.V_EXAMPLE AS
SELECT base.INGEST_TS, base.API_NAME, base.STATUS_CODE,
rec.value:id::NUMBER AS ID,
rec.value:updated_at::TIMESTAMP_TZ AS UPDATED_AT,
rec.value:name::STRING AS NAME
FROM API_DATA_PIPELINE.RAW_LANDING.EXAMPLE_RAW base,
LATERAL FLATTEN(input => base.PAYLOAD:data) rec;- Or click Download DDL to save the SQL file.
Every HTTP attempt is logged as its own row in INGESTION_RESPONSE_LOG. The Console surfaces this in three layers:
PAGES WITH RETRIES · RECOVERED · EXHAUSTED (FAILED) · avg attempts per retried page.
Pages where the framework had to retry, showing API, page #, attempt count, final status, and outcome (Recovered / Exhausted).
Pick any retried page from the dropdown to see the full attempt-by-attempt history:
ATTEMPT_NUMBER | IS_FINAL_ATTEMPT | STATUS_CODE | RESPONSE_TIME_SECONDS | ERROR_MESSAGE_TEXT
1 | FALSE | 503 | 1.2 | HTTP 503
2 | FALSE | 503 | 1.4 | HTTP 503
3 | TRUE | 200 | 0.9 | NULL
Transient errors that eventually succeeded are fully visible here — perfect for spotting flaky upstream APIs before they exhaust.
Two safeguards prevent runtime "host not allowed" failures:
- Live validation in the Add Config form — refuses to save unless the URL host is covered (or you explicitly acknowledge the gap).
- Compatibility Linter at the top of Tab 1 — auto-collapsed expander listing every existing config whose
ENDPOINT_URLhost isn't covered by any current network rule. Surface drift after rules are rotated/dropped.
Matching supports exact host, host:port, and *.suffix wildcards.
| AUTH_TYPE | Required Fields | How It Works |
|---|---|---|
NONE |
— | No authentication. Direct HTTP call. |
API_KEY |
SECRET_NAME, API_KEY_HEADER | Reads key from a GENERIC_STRING secret, adds it as the named HTTP header. |
OAUTH2_BASIC |
SECRET_NAME, TOKEN_URL | Uses client ID/secret from a PASSWORD secret, calls TOKEN_URL to get a bearer token. |
OAUTH2_INTEGRATION |
SECRET_NAME | Uses Snowflake's managed OAuth flow. The integration is bound to the secret — no separate config field. |
| PAGINATION_TYPE | Behavior | Example URL |
|---|---|---|
NONE |
Single request | https://api.example.com/data |
PAGE |
Appends ?{PAGE_PARAM}=1, =2, ... |
?page=1 |
OFFSET |
Appends ?{PAGE_PARAM}=0, =100, ... |
?offset=0 |
Pagination stops when the response contains {"data": []} (empty data array).
| Tab | Purpose |
|---|---|
| Manage API Configs | Compatibility Linter; CRUD for configs; Reset Watermark control; toggle/delete. Auto-rebuilds the ingestor on any change. |
| Manage Secrets & EAI | View/create Snowflake secrets, security integrations, and network rules. EAI auto-rebuilds. |
| Run Ingestion | Run selected APIs, run-all sequential, or run-all parallel via Snowflake Tasks. |
| Ingestion Console | Filter logs by API/status, success rate, avg response time, error drill-down, and the Retry Drill-Down panel. |
| Data Explorer | Two sub-tabs: Raw Payloads (inspector) + Schema Discovery (one-click flattened view generation + quality checks). Shared landing-table + API filter. |
| Task Scheduler | Create/manage Snowflake Tasks; INTERVAL / CRON; resume/suspend/drop/run-now; execution telemetry. |
| Problem | Likely Cause | Fix |
|---|---|---|
| "Configuration not found or inactive" | API_NAME typo or ACTIVE_FLAG = FALSE |
Check Tab 1, toggle active if needed |
| "Auth failed: SECRET_NAME not configured" | SECRET_NAME blank in config | Edit the config row to add the secret name |
| HTTP 401 / 403 errors | Credentials expired or wrong | Update the secret in Tab 2 |
| "Could not connect" / timeout | Hostname not in any network rule | Add network rule in Tab 2 (EAI auto-rebuilds). Check the Compatibility Linter in Tab 1. |
| Add Config blocked | URL host not covered by any network rule | Add the rule first, or tick "Acknowledge gap" to override |
| Duplicate data not appearing | Payload hash already exists (dedup working) | Expected — identical responses are skipped |
| Watermark not advancing | WATERMARK_FIELD doesn't match the API's response shape |
Use Tab 5 (Data Explorer) → Schema Discovery to confirm the path; update the config |
| Want to re-pull all history | Watermark stuck on a recent value | Tab 1 → INCREMENTAL SYNC CONTROL → Clear (Full Backfill) |
| Flaky API but final status 200 | Transient errors hidden in old logs | Open Tab 4 → Retry Drill-Down to see every attempt |
| Generated view fails to create | Inferred type cast errors on real data | In Tab 5 (Data Explorer → Schema Discovery), switch Type Strategy to "All STRING (safe)" |
| Task auto-suspended | Exceeded SUSPEND_TASK_AFTER_NUM_FAILURES |
Fix the API issue, then Resume in Tab 6 |
| Object | Location | Purpose |
|---|---|---|
INGESTION_CONFIGS |
API_DATA_PIPELINE.METADATA |
Config table — one row per API. API_ID is the rename-safe surrogate key; API_NAME is the unique business key. |
INGESTION_RESPONSE_LOG |
API_DATA_PIPELINE.METADATA |
One row per HTTP attempt. Powers the Retry Drill-Down. |
API_RAW_DATA |
API_DATA_PIPELINE.RAW_LANDING |
Default landing table (carries API_ID for stable joins after renames). |
<LANDING_TABLE> |
API_DATA_PIPELINE.RAW_LANDING |
Custom per-API landing tables — auto-created and self-healed (API_ID column added on next run). |
USP_UNIVERSAL_INGESTOR |
API_DATA_PIPELINE.METADATA |
Main ingestion procedure: auth, paginate, watermark, retry-with-logging, dedupe, write. |
USP_REBUILD_INGESTOR |
API_DATA_PIPELINE.METADATA |
Auto-rebuilds ingestor with current secrets. |
EAI_UNIVERSAL_INGESTOR |
Account-level integration | Controls network + secret access. Auto-rebuilt on rule/secret changes. |
TASK_INGEST_<API> |
API_DATA_PIPELINE.METADATA |
Snowflake Tasks for scheduled ingestion. |
V_<API> (generated) |
API_DATA_PIPELINE.RAW_LANDING |
Flattened views created via Data Explorer → Schema Discovery. |