- Overview
- API Resources
Supply Chain & Retail Solutions API guide
This page covers the schema-management endpoints: saving a standard schema for an application, rolling it out for a tenant solution, upgrading to a new version, adding a new column, and inspecting what is currently rolled out. Use it together with Getting Started and the API Guide.
All schema endpoints are rooted at https://ingestion.peak.ai/api/v2/schema.
Lifecycle overview
A schema goes through three stages:
- Save the standard schema for an application (one-time per
appName+appVersion). - Roll out that standard schema for a specific tenant solution. This creates the warehouse tables (and a matching
<table_name>_failed_rowstable for each) and records a tenant-scoped schema entry. - Evolve the rolled-out schema over time — by upgrading to a new app version or by adding a new column to an existing table.
You can inspect what's been rolled out at any time using the list and get endpoints.
Save a standard schema
Saves an application-level standard schema that can later be rolled out to one or more tenant solutions. Each appName + appVersion combination must be unique.
POST https://ingestion.peak.ai/api/v2/schema
POST https://ingestion.peak.ai/api/v2/schema
Payload
| Field | Required | Description |
|---|---|---|
appName | Yes | Application identifier the schema belongs to (e.g., quote-pricing) |
appVersion | Yes | Semantic version of the schema (e.g., 1.0.0) |
schemaDefinition | Yes | Schema specification as a JSON string. See Schema definition structure below for what goes inside it. |
schemaSynergyTag | No | Optional tag used by downstream metric publishing |
dryRun | No | When true, validates the payload without persisting. Returns 200 OK instead of 201 Created. Default false. |
Schema definition structure
schemaDefinition is a JSON-encoded array of table definitions. Each table has columns, and each column declares its data type and a list of validations.
Per-table fields:
| Field | Required | Description |
|---|---|---|
tableName | Yes | Table name. Must match ^[A-Za-z_][A-Za-z0-9_]*$. |
columns | Yes | Non-empty array of column definitions (see below). |
primaryKeys | No | Array of column names that form the primary key. |
uniqueKeys | No | Array of arrays; each inner array is a set of column names that must be unique together. |
foreignKeys | No | Array of foreign-key definitions, each with referencedTable, columnMappings, and an optional constraintName. |
skipValidations | No | When true, runtime validation is skipped for this table. Default false. |
scheduleConfig | No | Per-table scheduled-ingestion configuration. See Scheduled ingestion. |
Per-column fields:
| Field | Required | Description |
|---|---|---|
columnName | Yes | Column name. Must match ^[A-Za-z_][A-Za-z0-9_]*$. |
dataType | Yes | One of boolean, date, float, integer, json, numeric, string, timestamp. See Data Types. |
validations | Yes | Non-empty array of validation rules — at least one entry per column (see below). |
validations[].type | Yes | The rule for each entry. One of required, optional, nonNull, minLength, maxLength, range, enum, timestampFormat. Every column must include exactly one of required or optional. |
precision | Conditional | Required when dataType is numeric (1–38). Rejected when dataType is float. Ignored for other types. |
scale | Conditional | Required when dataType is numeric (0–37). Rejected when dataType is float. Ignored for other types. |
defaultValue | No | Default value applied when the column is added to existing rows via schema upgrade or add-column. |
Validation rules — what to include in validations[]:
Every entry in a column's validations array is a { "type": "<rule>", ... } object. The validations array is required and must not be empty: every column has to state its required-ness explicitly.
Required vs optional columns. Every column must declare exactly one of these two markers:
{ "type": "required" }— the column's key must be present in every ingest payload. Submitting a row without it fails withDI_E_23N01.{ "type": "optional" }— callers can leave the key out, and the column is stored asNULL— or itsdefaultValue, if one is set.
A column that declares neither marker, declares both, or supplies an empty validations array is rejected at save-schema with 400 Bad Request. Two further constraints apply:
- Primary-key columns must be
requiredand cannot beoptional. Declaring a primary-key column asoptional(or withoutrequired) returns400 Bad Request. nonNullmay only be used together withrequired— anonNull+optionalcolumn is rejected with400 Bad Request. To make a column optional but never stored asnull, set adefaultValueinstead of usingnonNull.
One rule is always enforced regardless of required-ness: every date or timestamp column must include { "type": "timestampFormat", "format": "<format>" }, where format is one of:
YYYY-MM-DD— fordateYYYY-MM-DD HH:MI:SS— fortimestamp(no timezone)YYYY-MM-DD HH:MI:SS ±hh:mm— fortimestamp(with timezone)
All other validation types are optional — include them only where you want their effect. To turn one off, just leave it out of the validations array; there is no per-validator enabled: false flag.
| Type | Parameters | Effect when included |
|---|---|---|
nonNull | none | Value cannot be null or blank. (required controls key presence; nonNull controls the value.) |
minLength | value (integer) | Minimum string length. |
maxLength | value (integer) | Maximum string length. |
range | min, max (numbers) | Numeric value must fall within the inclusive range. |
enum | value (array) | Value must be one of the listed options. |
Required-ness and nullability
A column's required-ness is set by its required/optional marker, and nonNull controls whether the value may be null: required/optional control whether the key must be present in the payload, nonNull controls the value. Choose the combination that matches how the column should behave:
| You want | Marker | Include nonNull? |
|---|---|---|
Key must be present, value cannot be null | required | Yes |
Key must be present, value can be null | required | No |
Key may be omitted; stored as null (or defaultValue) when absent | optional | No |
The nonNull-with-optional (or nonNull without required) combination is rejected — to make a column optional but never stored as null, give it a defaultValue instead of nonNull. Primary-key columns must always be required.
The per-table pages in the API Guide reflect these flags as the Required and Nullable columns of each attribute table.
You can also add a column after rollout through the add-column endpoint — useful when a new optional column is needed on an already-rolled-out table; existing rows are backfilled with the supplied defaultValue.
Example column definitions:
"columns": [
{ "columnName": "order_id", "dataType": "string",
"validations": [{ "type": "required" }, { "type": "nonNull" }] },
{ "columnName": "amount", "dataType": "numeric", "precision": 10, "scale": 2,
"validations": [{ "type": "required" }, { "type": "nonNull" }, { "type": "range", "min": 0, "max": 1000000 }] },
{ "columnName": "placed_at", "dataType": "timestamp",
"validations": [{ "type": "required" }, { "type": "nonNull" }, { "type": "timestampFormat", "format": "YYYY-MM-DD HH:MI:SS" }] },
{ "columnName": "notes", "dataType": "string",
"validations": [{ "type": "required" }] },
{ "columnName": "customer_ref", "dataType": "string",
"validations": [{ "type": "optional" }] },
{ "columnName": "region", "dataType": "string", "defaultValue": "UNKNOWN",
"validations": [{ "type": "optional" }] }
]
"columns": [
{ "columnName": "order_id", "dataType": "string",
"validations": [{ "type": "required" }, { "type": "nonNull" }] },
{ "columnName": "amount", "dataType": "numeric", "precision": 10, "scale": 2,
"validations": [{ "type": "required" }, { "type": "nonNull" }, { "type": "range", "min": 0, "max": 1000000 }] },
{ "columnName": "placed_at", "dataType": "timestamp",
"validations": [{ "type": "required" }, { "type": "nonNull" }, { "type": "timestampFormat", "format": "YYYY-MM-DD HH:MI:SS" }] },
{ "columnName": "notes", "dataType": "string",
"validations": [{ "type": "required" }] },
{ "columnName": "customer_ref", "dataType": "string",
"validations": [{ "type": "optional" }] },
{ "columnName": "region", "dataType": "string", "defaultValue": "UNKNOWN",
"validations": [{ "type": "optional" }] }
]
Here order_id, amount, and placed_at are required and must carry a non-null value; notes is required (the key must be present) but its value can be null; customer_ref is optional — callers can omit it and it's stored as null; and region is optional too, but is stored as "UNKNOWN" rather than null when omitted, thanks to its defaultValue.
Example request
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"appName": "quote-pricing",
"appVersion": "1.0.0",
"schemaDefinition": "{ \"tables\": [ { \"name\": \"products\", \"columns\": [ ... ] } ] }",
"dryRun": false
}'
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"appName": "quote-pricing",
"appVersion": "1.0.0",
"schemaDefinition": "{ \"tables\": [ { \"name\": \"products\", \"columns\": [ ... ] } ] }",
"dryRun": false
}'
Response (201 Created)
{
"id": "7be5d475-4494-4c73-a012-2c27c6da5f38",
"appName": "quote-pricing",
"appVersion": "1.0.0",
"schemaSynergyTag": null,
"createdAt": "2026-04-24T07:23:19.264Z",
"updatedAt": "2026-04-24T07:23:19.264Z",
"message": "Standard schema saved successfully"
}
{
"id": "7be5d475-4494-4c73-a012-2c27c6da5f38",
"appName": "quote-pricing",
"appVersion": "1.0.0",
"schemaSynergyTag": null,
"createdAt": "2026-04-24T07:23:19.264Z",
"updatedAt": "2026-04-24T07:23:19.264Z",
"message": "Standard schema saved successfully"
}
Status codes
201 Created— schema saved200 OK—dryRun: truevalidation succeeded400 Bad Request— invalidschemaDefinition409 Conflict— a schema with thisappName+appVersionalready exists500 Internal Server Error— unexpected failure
Roll out a schema to a tenant solution
Creates the warehouse tables for a tenant solution by referencing a previously saved standard schema. Each table is created alongside its <table_name>_failed_rows companion table — used by asynchronous validation to record failed rows.
POST https://ingestion.peak.ai/api/v2/schema/rollout
POST https://ingestion.peak.ai/api/v2/schema/rollout
Payload
| Field | Required | Description |
|---|---|---|
solutionName | Yes | Tenant-specific solution identifier (e.g., QP_OOTB) |
targetSchemaName | Yes | Warehouse schema where tables will be created (e.g., STAGE) |
appName | Yes | The standard schema's appName |
appVersion | Yes | The standard schema's appVersion |
prefix | No | String prepended to every table name during creation |
suffix | No | String appended to every table name during creation |
Example request
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema/rollout' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"solutionName": "QP_OOTB",
"targetSchemaName": "STAGE",
"appName": "quote-pricing",
"appVersion": "1.0.0",
"prefix": "QP_",
"suffix": "_OOTB"
}'
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema/rollout' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"solutionName": "QP_OOTB",
"targetSchemaName": "STAGE",
"appName": "quote-pricing",
"appVersion": "1.0.0",
"prefix": "QP_",
"suffix": "_OOTB"
}'
Response (201 Created)
{
"id": "f81c9af3-65ce-4dfe-9cd5-855ebb79e617",
"solutionName": "QP_OOTB",
"schemaVersion": 1,
"targetSchemaName": "STAGE",
"prefix": "QP_",
"suffix": "_OOTB",
"createdAt": 1745484199264,
"message": "Schema rolled out successfully",
"successfulObjects": {
"QP_PRODUCTS_OOTB": "Created",
"QP_CUSTOMERS_OOTB": "Created"
},
"failedObjects": {}
}
{
"id": "f81c9af3-65ce-4dfe-9cd5-855ebb79e617",
"solutionName": "QP_OOTB",
"schemaVersion": 1,
"targetSchemaName": "STAGE",
"prefix": "QP_",
"suffix": "_OOTB",
"createdAt": 1745484199264,
"message": "Schema rolled out successfully",
"successfulObjects": {
"QP_PRODUCTS_OOTB": "Created",
"QP_CUSTOMERS_OOTB": "Created"
},
"failedObjects": {}
}
Status codes
201 Created— every table was created207 Multi-Status— some tables succeeded, some failed; inspectsuccessfulObjectsandfailedObjects400 Bad Request— request validation failed (e.g., unknownappName/appVersion)500 Internal Server Error— every table failed, or unexpected failure
Audit columns added at rollout
Each table created by a rollout includes a fixed set of audit columns alongside the business columns from your schema. These are populated automatically by the API — you do not send them in your ingest payload, and they will not collide with your own column names.
Data table — every rolled-out table includes these 6 audit columns:
| Column | Description |
|---|---|
PEAKAUDITREQUESTID | Identifier of the request that ingested the row |
PEAKAUDITCREATEDAT | Timestamp when the row was first inserted |
PEAKAUDITUPDATEDAT | Timestamp when the row was last updated (changes on every UPSERT) |
PEAKAUDITREQUESTTIME | Timestamp the caller's request was received |
PEAKAUDITCREATEDBY | Platform user identifier from the x-auth-platformuserid header |
SYSPRIMARYKEY | Synthetic primary key used internally to deduplicate and join rows |
Failed-rows table (<table_name>_failed_rows) — the companion table created next to each data table includes the audit columns above (except PEAKAUDITUPDATEDAT, since failed rows are not updated), plus two extras specific to failure tracking:
| Column | Description |
|---|---|
LOADTYPE | "upsert" or "append", derived from the operationType of the request that produced the failed row |
ERRORCODES | JSON-encoded list of error codes the row failed against (see API Guide — Error codes) |
In practice, you don't need to query the failed-rows table directly — the Data Quality Dashboard surfaces the same data with filters and drill-downs. See Data Quality Dashboard.
Column-name casing differs by warehouse: Snowflake stores them uppercase and unquoted (PEAKAUDITREQUESTID); Redshift stores them quoted and camel-cased ("peakAuditRequestId"). They refer to the same logical column.
Upgrade a rolled-out schema to a new version
Computes the diff between two standard schema versions and applies only additive changes (new tables, new columns) to your already-rolled-out solution. Any removal — of a table, column, primary key, unique key, or foreign key — is rejected with 400 Bad Request.
POST https://ingestion.peak.ai/api/v2/schema/upgrade
POST https://ingestion.peak.ai/api/v2/schema/upgrade
Payload
| Field | Required | Description |
|---|---|---|
solutionName | Yes | The tenant solution to upgrade |
appName | Yes | Application identifier (must match the one used at rollout) |
previousAppVersion | Yes | The version currently rolled out |
newAppVersion | Yes | The version to upgrade to |
Example request
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema/upgrade' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"solutionName": "QP_OOTB",
"appName": "quote-pricing",
"previousAppVersion": "1.0.0",
"newAppVersion": "1.1.0"
}'
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema/upgrade' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"solutionName": "QP_OOTB",
"appName": "quote-pricing",
"previousAppVersion": "1.0.0",
"newAppVersion": "1.1.0"
}'
Response (200 OK)
{
"solutionName": "QP_OOTB",
"appName": "quote-pricing",
"previousAppVersion": "1.0.0",
"newAppVersion": "1.1.0",
"schemaVersion": 2,
"message": "Schema upgrade completed successfully",
"successfulObjects": {
"QP_PRODUCTS_OOTB": "Added column: discount_pct",
"QP_PROMOTIONS_OOTB": "Created"
},
"failedObjects": {},
"skippedObjects": {
"QP_CUSTOMERS_OOTB": "No changes detected"
}
}
{
"solutionName": "QP_OOTB",
"appName": "quote-pricing",
"previousAppVersion": "1.0.0",
"newAppVersion": "1.1.0",
"schemaVersion": 2,
"message": "Schema upgrade completed successfully",
"successfulObjects": {
"QP_PRODUCTS_OOTB": "Added column: discount_pct",
"QP_PROMOTIONS_OOTB": "Created"
},
"failedObjects": {},
"skippedObjects": {
"QP_CUSTOMERS_OOTB": "No changes detected"
}
}
Status codes
200 OK— every applicable change succeeded (or the diff was empty)207 Multi-Status— some changes succeeded, some failed400 Bad Request— diff includes a removal; the response'svalidationErrors[]lists the removals detected404 Not Found—solutionName,appName, or one of the versions does not exist500 Internal Server Error— every change failed, or unexpected failure
Add a new column
Adds a single column to an existing table without going through a full version upgrade. The new column is added with the supplied validations and default value.
POST https://ingestion.peak.ai/api/v2/schema/{objectName}/add-attribute
POST https://ingestion.peak.ai/api/v2/schema/{objectName}/add-attribute
Path parameters
objectName— The fully qualified warehouse table name as returned by Describe a solution's schema. For a standard rolled-out table that includes any solutionprefix/suffix(for exampleQP_PRODUCTS_OOTB); for a custom table it includes the automaticC_(Snowflake) orc_(Redshift) table prefix as well (for exampleC_sales_orders_v1). The lookup is case-insensitive.
Automatic column-name prefix on standard tables
When you add a column to a table created through a standard schema rollout, the API automatically prefixes the column name in the warehouse — C_ on Snowflake (uppercase), c_ on Redshift (lowercase) — to distinguish post-rollout additions from the original schema columns. The customer sends the raw name in columnName; the prefixed name is what gets created in the warehouse, and what the response returns.
You send columnName | Tenant warehouse | Warehouse column created | Response columnName |
|---|---|---|---|
discount_pct | Snowflake | C_discount_pct | "C_discount_pct" |
discount_pct | Redshift | c_discount_pct | "c_discount_pct" |
Queries against the warehouse table must reference the prefixed name (SELECT C_discount_pct FROM ...).
Exception: columns added to a custom table are not prefixed — they keep the raw name you sent. The intent: the table's own C_/c_ prefix already signals it's a custom table, so individual columns inside don't need an additional marker.
Payload
| Field | Required | Description |
|---|---|---|
solutionName | Yes | The tenant solution the table belongs to |
columnName | Yes | Raw column name. The warehouse column is created with a C_/c_ prefix on standard tables (see above); custom-table columns are not prefixed. |
dataType | Yes | One of the supported data types — boolean, date, float, integer, json, numeric, string, timestamp. |
format | Conditional | Format string for date/timestamp types. Use YYYY-MM-DD for date; YYYY-MM-DD HH:MI:SS or YYYY-MM-DD HH:MI:SS ±hh:mm for timestamp. |
precision | Conditional | Total digits (1–38). Required for numeric; rejected for float. |
scale | Conditional | Decimal digits (0–37). Required for numeric; rejected for float. |
defaultValue | No | Default value applied to existing rows after the column is added. |
validations | Yes | Non-empty array of validation rules. Each entry is { "type": "<rule>", ... } (for example {"type": "nonNull"}, {"type": "maxLength", "value": 100}). As with save-schema, must declare exactly one of {"type": "required"} or {"type": "optional"}. |
Controlling required-ness and nullability on the new column
The required/optional marker controls the key, and nonNull controls the value — pick the combination that matches how the column should behave:
| You want | Marker | Include nonNull? |
|---|---|---|
| Key must be present, value cannot be null | required | Yes |
| Key must be present, value can be null | required | No |
Key may be missing; stored as null (or defaultValue) when absent | optional | No |
As at save-schema, a column must declare exactly one of required/optional, and nonNull requires required (a nonNull + optional column is rejected with 400 Bad Request). The doc pages on the per-table guide reflect the marker and nonNull as the Required and Nullable columns of the attributes table.
defaultValue + nonNull are mutually exclusive. If you supply a defaultValue, the API rejects a payload that also lists {"type": "nonNull"} in validations with 400 Bad Request: "Cannot use 'nonNull' validation with defaultValue. defaultValue ensures the field always has a value." — pick one of the two, not both.
Example request
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema/QP_PRODUCTS_OOTB/add-attribute' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"solutionName": "QP_OOTB",
"columnName": "discount_pct",
"dataType": "numeric",
"precision": 5,
"scale": 2,
"defaultValue": 0,
"validations": [
{ "type": "optional" },
{ "type": "range", "min": 0, "max": 100 }
]
}'
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema/QP_PRODUCTS_OOTB/add-attribute' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"solutionName": "QP_OOTB",
"columnName": "discount_pct",
"dataType": "numeric",
"precision": 5,
"scale": 2,
"defaultValue": 0,
"validations": [
{ "type": "optional" },
{ "type": "range", "min": 0, "max": 100 }
]
}'
Response (200 OK)
{
"requestId": "7be5d475-4494-4c73-a012-2c27c6da5f38",
"message": "Column added successfully",
"columnName": "C_discount_pct",
"tableName": "QP_PRODUCTS_OOTB",
"schemaVersion": 2
}
{
"requestId": "7be5d475-4494-4c73-a012-2c27c6da5f38",
"message": "Column added successfully",
"columnName": "C_discount_pct",
"tableName": "QP_PRODUCTS_OOTB",
"schemaVersion": 2
}
The columnName in the response is the prefixed warehouse column name (C_discount_pct on Snowflake, c_discount_pct on Redshift) — that's the name you'll use when querying the warehouse. For a custom table, this would be the raw name you sent.
Status codes
200 OK— column added400 Bad Request— invalid validations, unsupporteddataType, or column already exists500 Internal Server Error— unexpected failure
Create a custom table
Adds a custom table to an existing tenant solution. Use this when a tenant needs a table beyond what the application's standard schema provides — for example, a tenant-specific staging table or a derived table that isn't part of the canonical schema. The columns are defined inline in the request payload.
The solution must already exist — create it first via Roll out a schema to a tenant solution. The new table inherits the solution's existing targetSchemaName automatically; you do not specify it on the request. Calling this endpoint with a solutionName that does not exist returns 404 Not Found.
POST https://ingestion.peak.ai/api/v2/schema/custom-table
POST https://ingestion.peak.ai/api/v2/schema/custom-table
Automatic name prefixing: the created warehouse table name is prefixed to distinguish it from standard-rollout tables — C_ on Snowflake (uppercase), c_ on Redshift (lowercase). The prefix is added to the front of the final table name, including any partner-supplied prefix/suffix. So tableName: "orders" with prefix: "sales_" and suffix: "_v1" becomes C_sales_orders_v1 on Snowflake. Column names are not prefixed.
Payload:
| Field | Required | Description |
|---|---|---|
solutionName | Yes | The existing tenant solution to attach the new table to (e.g., QP_OOTB). Must already be rolled out. |
tableName | Yes | Base name for the new table. See the prefixing note above for how the final warehouse name is composed. |
columns | Yes | Non-empty list of column definitions. Each entry follows the same shape used in the standard schema (columnName, dataType, optional format, precision, scale, defaultValue, and a validations array). As at save-schema, every column's validations must declare exactly one of { "type": "required" } or { "type": "optional" }, and primary-key columns must be required. |
primaryKeys | No | Array of column names that form the primary key |
uniqueKeys | No | Array of arrays — each inner array is a set of column names that must be unique together |
prefix | No | Additional string prepended to the table name (inside the automatic C_/c_ prefix) |
suffix | No | String appended to the table name |
Example request:
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema/custom-table' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"solutionName": "QP_OOTB",
"tableName": "orders",
"prefix": "sales_",
"suffix": "_v1",
"columns": [
{ "columnName": "ORDER_ID", "dataType": "string", "validations": [{ "type": "required" }] },
{ "columnName": "AMOUNT", "dataType": "numeric", "precision": 10, "scale": 2, "validations": [{ "type": "required" }] },
{ "columnName": "PLACED_AT", "dataType": "timestamp", "validations": [{ "type": "required" }, { "type": "timestampFormat", "format": "YYYY-MM-DD HH:MI:SS" }] },
{ "columnName": "NOTES", "dataType": "string", "validations": [{ "type": "optional" }] }
],
"primaryKeys": ["ORDER_ID"]
}'
curl -X POST \
'https://ingestion.peak.ai/api/v2/schema/custom-table' \
-H 'Authorization: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"solutionName": "QP_OOTB",
"tableName": "orders",
"prefix": "sales_",
"suffix": "_v1",
"columns": [
{ "columnName": "ORDER_ID", "dataType": "string", "validations": [{ "type": "required" }] },
{ "columnName": "AMOUNT", "dataType": "numeric", "precision": 10, "scale": 2, "validations": [{ "type": "required" }] },
{ "columnName": "PLACED_AT", "dataType": "timestamp", "validations": [{ "type": "required" }, { "type": "timestampFormat", "format": "YYYY-MM-DD HH:MI:SS" }] },
{ "columnName": "NOTES", "dataType": "string", "validations": [{ "type": "optional" }] }
],
"primaryKeys": ["ORDER_ID"]
}'
ORDER_ID, AMOUNT, and PLACED_AT are required; NOTES is optional, so callers can omit it.
Response (201 Created):
{
"message": "Custom table created successfully",
"tableName": "C_sales_orders_v1",
"solutionName": "QP_OOTB",
"schemaVersion": 3,
"columnCount": 4
}
{
"message": "Custom table created successfully",
"tableName": "C_sales_orders_v1",
"solutionName": "QP_OOTB",
"schemaVersion": 3,
"columnCount": 4
}
The warehouse table name returned in the response includes the automatic C_/c_ prefix. Ingest into the custom table by sending requests to POST /api/v2/objects/<returned-tableName> — the same endpoint as any other table.
Status codes:
201 Created— custom table created400 Bad Request— validation failure (missing required fields, invalid column definitions, schema-name issues)401 Unauthorized404 Not Found— referenced solution or schema does not exist409 Conflict— a table with this name already exists in the solution500 Internal Server Error— unexpected failure
List solutions for the tenant
Returns every solution that has been rolled out for the calling tenant, with the latest version's metadata. Useful for populating dropdowns and checking what's available.
GET https://ingestion.peak.ai/api/v2/schema/solutions
GET https://ingestion.peak.ai/api/v2/schema/solutions
Response (200 OK)
{
"solutions": [
{
"solutionName": "QP_OOTB",
"targetSchemaName": "STAGE",
"prefix": "QP_",
"suffix": "_OOTB",
"schemaVersion": 2,
"tableCount": 5,
"createdAt": "2026-04-24T07:23:19.264Z"
},
{
"solutionName": "atest_automation_5",
"targetSchemaName": "stage",
"prefix": null,
"suffix": null,
"schemaVersion": 1,
"tableCount": 3,
"createdAt": "2026-01-20T09:06:15.891Z"
}
]
}
{
"solutions": [
{
"solutionName": "QP_OOTB",
"targetSchemaName": "STAGE",
"prefix": "QP_",
"suffix": "_OOTB",
"schemaVersion": 2,
"tableCount": 5,
"createdAt": "2026-04-24T07:23:19.264Z"
},
{
"solutionName": "atest_automation_5",
"targetSchemaName": "stage",
"prefix": null,
"suffix": null,
"schemaVersion": 1,
"tableCount": 3,
"createdAt": "2026-01-20T09:06:15.891Z"
}
]
}
The solutions array is empty if no rollouts have happened yet for the tenant.
Describe a solution's schema
Returns the latest version's full schema for a given solution, including every table and its columns.
GET https://ingestion.peak.ai/api/v2/schema?solutionName={solutionName}
GET https://ingestion.peak.ai/api/v2/schema?solutionName={solutionName}
Query parameters
solutionName(required) — the solution to describe
Response (200 OK)
Each table in the schema array is keyed by objectName — the full warehouse table name as it exists in your warehouse, including any solution-level prefix/suffix from rollout. Tables created through Create a custom table appear here with their automatic C_ (Snowflake) or c_ (Redshift) prefix as well.
{
"solutionName": "QP_OOTB",
"targetSchemaName": "STAGE",
"appName": "quote-pricing",
"appVersion": "1.1.0",
"prefix": "QP_",
"suffix": "_OOTB",
"createdAt": "2026-04-24T07:23:19.264Z",
"schema": [
{
"objectName": "QP_PRODUCTS_OOTB",
"primaryKeys": ["product_id"],
"uniqueKeys": [],
"foreignKeys": [],
"columns": [
{ "columnName": "product_id", "dataType": "string", "validations": [{ "type": "required" }, { "type": "nonNull" }] },
{ "columnName": "product_name", "dataType": "string", "validations": [{ "type": "required" }, { "type": "maxLength", "value": 255 }] },
{ "columnName": "discount_pct", "dataType": "numeric", "precision": 5, "scale": 2, "validations": [{ "type": "required" }, { "type": "range", "min": 0, "max": 100 }] }
],
"isRolledOut": true,
"rolloutState": "SUCCESS",
"targetSchemaName": "STAGE"
},
{
"objectName": "C_sales_orders_v1",
"primaryKeys": ["order_id"],
"uniqueKeys": [],
"foreignKeys": [],
"columns": [
{ "columnName": "order_id", "dataType": "string", "validations": [{ "type": "required" }, { "type": "nonNull" }] },
{ "columnName": "amount", "dataType": "numeric", "precision": 10, "scale": 2, "validations": [{ "type": "required" }, { "type": "nonNull" }] },
{ "columnName": "placed_at", "dataType": "timestamp", "validations": [{ "type": "required" }, { "type": "nonNull" }, { "type": "timestampFormat", "format": "YYYY-MM-DD HH:MI:SS" }] }
],
"isRolledOut": true,
"rolloutState": "SUCCESS",
"targetSchemaName": "STAGE"
}
]
}
{
"solutionName": "QP_OOTB",
"targetSchemaName": "STAGE",
"appName": "quote-pricing",
"appVersion": "1.1.0",
"prefix": "QP_",
"suffix": "_OOTB",
"createdAt": "2026-04-24T07:23:19.264Z",
"schema": [
{
"objectName": "QP_PRODUCTS_OOTB",
"primaryKeys": ["product_id"],
"uniqueKeys": [],
"foreignKeys": [],
"columns": [
{ "columnName": "product_id", "dataType": "string", "validations": [{ "type": "required" }, { "type": "nonNull" }] },
{ "columnName": "product_name", "dataType": "string", "validations": [{ "type": "required" }, { "type": "maxLength", "value": 255 }] },
{ "columnName": "discount_pct", "dataType": "numeric", "precision": 5, "scale": 2, "validations": [{ "type": "required" }, { "type": "range", "min": 0, "max": 100 }] }
],
"isRolledOut": true,
"rolloutState": "SUCCESS",
"targetSchemaName": "STAGE"
},
{
"objectName": "C_sales_orders_v1",
"primaryKeys": ["order_id"],
"uniqueKeys": [],
"foreignKeys": [],
"columns": [
{ "columnName": "order_id", "dataType": "string", "validations": [{ "type": "required" }, { "type": "nonNull" }] },
{ "columnName": "amount", "dataType": "numeric", "precision": 10, "scale": 2, "validations": [{ "type": "required" }, { "type": "nonNull" }] },
{ "columnName": "placed_at", "dataType": "timestamp", "validations": [{ "type": "required" }, { "type": "nonNull" }, { "type": "timestampFormat", "format": "YYYY-MM-DD HH:MI:SS" }] }
],
"isRolledOut": true,
"rolloutState": "SUCCESS",
"targetSchemaName": "STAGE"
}
]
}
The objectName value is what you use as the URL path on POST /api/v2/objects/{objectName} when ingesting into the table — including the C_/c_ prefix for custom tables.
Status codes
200 OK— schema returned404 Not Found— no solution with that name has been rolled out for the calling tenant
- Lifecycle overview
- Save a standard schema
- Payload
- Schema definition structure
- Example request
- Response (201 Created)
- Status codes
- Roll out a schema to a tenant solution
- Payload
- Example request
- Response (201 Created)
- Status codes
- Audit columns added at rollout
- Upgrade a rolled-out schema to a new version
- Payload
- Example request
- Response (200 OK)
- Status codes
- Add a new column
- Path parameters
- Automatic column-name prefix on standard tables
- Payload
- Controlling required-ness and nullability on the new column
- Example request
- Response (200 OK)
- Status codes
- Create a custom table
- List solutions for the tenant
- Response (200 OK)
- Describe a solution's schema
- Query parameters
- Response (200 OK)
- Status codes