Destinations
REST endpoints for outbound destinations. Configure warehouse tables and cloud file drops, push golden records on demand or stream a one-shot browser export.
Destinations
Endpoints for managing outbound destinations — where the funnel pushes golden records after matching + clustering completes. Symmetric to the /sources API on the ingest side.
Two families ship in v1:
- Warehouses:
postgres_dest,mysql_dest,snowflake_dest,bigquery_dest. Require atarget_tablethat already exists; writersTRUNCATEthenINSERT(overwrite) or justINSERT(append). - Cloud file drops:
s3_dest,gcs_dest,azure_blob_dest. Format inferred from URL suffix (.csv→ CSV; anything else → Parquet).
The browser-side /export endpoint streams CSV / Parquet / Excel directly without persisting a destination row.
Note: All connection strings are encrypted at rest via the same envelope-encryption envelope
credentials.encrypt_payloaduses for source credentials.GETresponses mask the password (user:***@host); only the/runpath decrypts the plaintext to hand to the writer.
List all destinations visible to the authenticated user. Org members see destinations owned by anyone in their org plus their own user-scoped ones.
Response
[
{
"id": "abc-123-...",
"clerk_user_id": "user_2abc...",
"org_id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Warehouse · golden_customers",
"destination_type": "postgres_dest",
"connection_string": "postgres://user:***@warehouse.example.com:5432/analytics",
"target_table": "public.golden_customers",
"entity_type": "person",
"config_json": {},
"write_mode": "overwrite",
"status": "idle",
"error_message": null,
"last_run_at": "2026-05-22T18:30:00Z",
"last_row_count": 14238,
"credential_type": null,
"has_credential": false,
"version": 7,
"created_at": "2026-05-20T09:00:00Z"
}
]
| Field | Type | Description |
|---|---|---|
id | string | Destination identifier (UUID) |
org_id | string | null | UUID of the owning org, or null for solo users |
destination_type | string | One of the 7 registered writer types |
connection_string | string | Masked on read — passwords replaced with *** |
target_table | string | null | Required for warehouses; null for cloud file drops |
entity_type | string | Which entity_type's golden records to push |
config_json | object | Per-writer config (e.g. format: "csv" for file writers) |
write_mode | string | overwrite (truncate first) or append |
status | string | idle, running, or error |
version | integer | Monotonically increments on every status change; ETag handle |
has_credential | boolean | True iff an inline encrypted credential is stored |
List the destination_type strings the backend will accept on POST. Useful for the frontend connector registry to detect drift.
Response
{
"types": [
"azure_blob_dest",
"bigquery_dest",
"gcs_dest",
"mysql_dest",
"postgres_dest",
"s3_dest",
"snowflake_dest"
]
}
Create a new destination. The connection_string is encrypted server-side before persistence.
Request
{
"name": "Warehouse · golden_customers",
"destination_type": "postgres_dest",
"connection_string": "postgres://user:pw@warehouse.example.com:5432/analytics",
"target_table": "public.golden_customers",
"entity_type": "person",
"config_json": {},
"write_mode": "overwrite"
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–100 chars, display name |
destination_type | string | Yes | Must match a registered writer |
connection_string | string | Yes | URL or bucket URI. Encrypted before persist. |
target_table | string | No | Required for warehouses; ignored by file writers |
entity_type | string | Yes | Which entity_type to push on each /run |
config_json | object | No | Per-writer extras (e.g. format) |
write_mode | string | No | overwrite (default) or append |
Response 201 Created — same shape as the list endpoint (with masked connection string).
Errors
| Status | Body | Meaning |
|---|---|---|
400 Bad Request | {detail: "unknown destination_type: ..."} | Writer not registered |
402 Payment Required | {detail: {error: "quota_exceeded", gate, limit, current}} | Hit plan's max_destinations |
Delete a destination. Org members can delete each other's org-scoped destinations.
Response 204 No Content
Errors
| Status | Meaning |
|---|---|
404 Not Found | Destination doesn't exist or caller is in a different org |
POST /api/golden/destinations/{id}/run [AUTH] [RATE-LIMITED 30/hr]
Push the golden records for the configured entity_type to this destination.
Request
{
"write_mode": "overwrite"
}
write_mode is optional; if omitted, the destination's stored default is used.
Response 200 OK
{
"ok": true,
"rows_written": 14238,
"target": "public.golden_customers"
}
Errors
| Status | Body | Meaning |
|---|---|---|
402 Payment Required | {detail: {error: "quota_exceeded", gate: "max_concurrent_destination_runs", ...}} | Hit plan's concurrent-run cap |
404 Not Found | — | Destination doesn't exist or visible to caller |
409 Conflict | {detail: "destination is already running ..."} | Another /run already holds the row (optimistic lock) |
422 Unprocessable Entity | {detail: {error: "destination_table_missing", message, suggested_ddl}} | Warehouse target table doesn't exist; body carries a copyable CREATE TABLE |
500 Internal Server Error | — | Writer raised a non-timeout exception |
503 Service Unavailable | {detail: "<...>_dest connect timed out after Ns ..."} | Writer's connect or write timeout (default 10s / 300s) |
Tip: Each successful run emits a
destination.runrow in theaudit_logtable (org users only). Failures emitdestination.run.failedwith the truncated error.
Attach or rotate the encrypted credential for a destination. Used by all writers except plain Postgres / MySQL where the credential lives inline in the connection string.
Request
{
"credential_type": "aws_iam",
"payload": {
"access_key_id": "AKIA...",
"secret_access_key": "...",
"region": "us-east-1"
}
}
Recognised credential_type values:
| Type | Used by | Payload shape |
|---|---|---|
aws_iam | s3_dest | {access_key_id, secret_access_key, region} |
service_account | gcs_dest, bigquery_dest | {key_json} (full JSON blob as string) |
azure_account_key | azure_blob_dest | {account_name, account_key} |
Response 200 OK {ok: true}
Clear the inline credential. The destination row remains; just unauth'd until a new credential is POSTed.
Response 204 No Content
GET /api/golden/destinations/export [AUTH] [RATE-LIMITED 60/hr]
Stream a one-shot download of every golden record for an entity_type. No destination row, no credentials.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
entity_type | string | Yes | Which entity_type's golden records to export |
format | string | No | csv (default), parquet, or excel |
limit | integer | No | Cap on rows returned (default 100,000) |
Response 200 OK — file stream with Content-Disposition: attachment and the appropriate Content-Type:
csv→text/csvparquet→application/x-parquetexcel→application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Errors
| Status | Meaning |
|---|---|
503 Service Unavailable | excel format requested but xlsxwriter not installed on the server |
Note: Each export emits a
destination.exportaudit_log row for org users with{row_count, format, entity_type}in theafterfield.
Per-destination_type config reference
postgres_dest
connection_string:postgres://user:pw@host:5432/dbnametarget_table: required, e.g.public.golden_customersconfig_json: none- credentials: none (embedded in connection string)
Uses COPY FROM STDIN for bulk insert. overwrite runs TRUNCATE first.
mysql_dest
connection_string:mysql://user:pw@host:3306/dbnametarget_table: requiredconfig_json: none- credentials: none
Batched INSERTs (1000 rows per round-trip). LOAD DATA LOCAL INFILE is faster but most managed MySQL offerings disable it.
snowflake_dest
connection_string:snowflake://user:pw@account/DB/SCHEMA?warehouse=WH&role=ROLEtarget_table: required, e.g.GOLDEN_CUSTOMERS(case preserved)config_json: none- credentials: none
v1 uses batched INSERTs. PUT + COPY INTO would be faster — that's roadmap once volume justifies the pandas dependency.
bigquery_dest
connection_string:bigquery://project/datasettarget_table: required, table name onlyconfig_json: none- credentials:
service_accountwith{key_json}(full JSON blob)
Uses load_table_from_file with a Parquet payload. Faster than streaming inserts; respects free-tier load quotas.
s3_dest
connection_string:s3://bucket/path/to/file.parquet(or.csv)target_table: ignoredconfig_json:{format: "csv" | "parquet"}— auto-set from URL suffix- credentials:
aws_iamwith{access_key_id, secret_access_key, region}
IAM user needs s3:PutObject on the bucket.
gcs_dest
connection_string:gs://bucket/path/to/file.parquettarget_table: ignoredconfig_json:{format: ...}auto-set from URL suffix- credentials:
service_accountwith{key_json}
Service account needs storage.objectCreator on the bucket.
azure_blob_dest
connection_string:azure://account.blob.core.windows.net/container/path/file.parquettarget_table: ignoredconfig_json:{format: ...}auto-set from URL suffix- credentials:
azure_account_keywith{account_name, account_key}
For least privilege, consider a stored access policy on the container instead of the master account key.
TypeScript client
The repo ships a thin typed client at frontend/lib/destinations.ts:
import {
listDestinations,
createDestination,
runDestination,
exportEntities,
storeDestinationCredential,
} from '@/lib/destinations'
// Push every golden person to the warehouse
const destinations = await listDestinations(token)
const warehouseDest = destinations.find(d => d.name === 'Warehouse · golden_customers')
const result = await runDestination(token, warehouseDest!.id)
console.log(`wrote ${result.rows_written} rows`)
// Or a one-shot browser download
const blob = await exportEntities(token, 'person', 'parquet')
The client handles 402 → quota-exceeded window event dispatch automatically so the existing <QuotaExceededModal /> lights up without per-callsite wiring.