BYOM
Bring your own model — upload a pre-trained artifact, validate it, and register it as a model you can deploy.
Already trained a model elsewhere? BYOM (bring your own model) puts it in
your catalog in three calls: upload the file, validate it (the
platform detects the framework, infers the input/output signature, and
runs a live forward pass), then register it as a kind: "byom"
model.
Accepted formats: PyTorch (.pt), TensorFlow/Keras (.h5, SavedModel),
ONNX (.onnx), and scikit-learn / XGBoost pickles (.pkl).
Upload size is capped by your plan (the byom_max_mb limit — for
example Starter allows files up to 500 MB, Team up to
2 GB, and Enterprise has no cap; the free tier doesn't include
BYOM). The cap is resolved from your plan automatically on every call.
Uploads also charge credits per MB — see Billing
for tiers and rates.
Step 1 — Upload
Send the artifact as a multipart file field. The upload is charged per
MB before storage (and refunded automatically if storage fails):
curl -X POST "$API_BASE/v1/models/byom/upload" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-F "file=@./sales_forecaster.onnx"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.byom.post_models_byom_upload(files={"file": open("./sales_forecaster.onnx", "rb")})
import { PredictAI } from "@predictai/sdk";
import { openAsBlob } from "node:fs";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const form = new FormData();
form.append("file", await openAsBlob("./sales_forecaster.onnx"), "./sales_forecaster.onnx");
const data = await client.byom.postModelsByomUpload({ form });
{
"data": {
"model_id": "1f4c3a9e6b8d4e219c7a5b3f8d2e6a41",
"model_uri": "s3://celmind-byom/ORG_ID/1f4c3a9e…/v1/model.onnx",
"local_staging_path": "/tmp/byom_1f4c3a9e….onnx",
"size_bytes": 48211284,
"ext": ".onnx",
…
}
}Keep the model_id — the next two calls take it.
Request
| Field | Meaning |
|---|---|
file required | The model artifact, as a multipart form field. One file per upload. |
Errors
| Status | Why | Example message |
|---|---|---|
400 | No file in the request | "multipart 'file' field required" |
400 | Empty file | "file is empty" |
402 | Not enough credits for the per-MB upload charge | error.code insufficient_credits, error.details.needed 46.0 |
403 | Your plan doesn't include BYOM | error.code plan_limit |
413 | File exceeds your plan's size cap | error.code plan_limit, error.details.cap_mb 500 |
Step 2 — Validate
Validation auto-detects the framework, infers the model's signature, and
runs one forward pass with random input of the inferred shape. It's a
paid operation (byom.validate):
curl -X POST "$API_BASE/v1/models/byom/validate" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{ "model_id": "1f4c3a9e6b8d4e219c7a5b3f8d2e6a41" }'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.byom.post_models_byom_validate(json={"model_id": "1f4c3a9e6b8d4e219c7a5b3f8d2e6a41"})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.byom.postModelsByomValidate({ json: { model_id: "1f4c3a9e6b8d4e219c7a5b3f8d2e6a41" } });
{
"data": {
"model_id": "1f4c3a9e6b8d4e219c7a5b3f8d2e6a41",
"detection": { "framework": "onnx", "confidence": 0.98, "notes": "ONNX graph header" },
"signature": {
"input_signature": [ { "name": "input", "shape": [-1, 20, 3], "dtype": "float32" } ],
"output_signature": [ { "name": "output", "shape": [-1, 14, 1], "dtype": "float32" } ]
},
"validation": {
"ok": true,
"framework": "onnx",
"file_size_bytes": 48211284,
"static_warnings": [],
"static_errors": [],
"dynamic_passed": true,
"forward_latency_ms": 11.4,
"output_shape": [1, 14, 1],
"notes": ""
}
}
}Request body
| Field | Meaning |
|---|---|
model_id required | The ID from upload. |
framework optional | Skip auto-detection: onnx, pytorch, tensorflow, sklearn, xgboost. |
input_shape_hint optional | An input shape like [1, 20, 3] — helps when signature inference is ambiguous. |
Response fields
| Field | Meaning |
|---|---|
detection | The detected framework with a confidence score and what gave it away. |
signature | The inferred input_signature / output_signature — name, shape, dtype per tensor. You pass these back at register. |
validation | The verdict: ok, static_warnings / static_errors, whether the live forward pass ran (dynamic_passed), and its latency. |
A model that fails validation still returns 200 — check
validation.ok and read static_errors:
"static_errors": ["File size 812.4 MB exceeds your plan's cap of 500 MB"]Pickle files get a standing warning: they execute code at load time, so consider re-exporting to ONNX.
Errors
| Status | Why | Example message |
|---|---|---|
400 | No model_id in the body | "model_id required" |
404 | Upload expired or wrong id | "No staged file found for model_id 1f4c…" |
402 | Not enough credits | error.code insufficient_credits, error.details.operation "byom.validate" |
Step 3 — Register
Pass the signature and validation objects from step 2 back in, plus a
name. This creates the catalog entry:
curl -X POST "$API_BASE/v1/models/byom/register" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"model_id": "1f4c3a9e6b8d4e219c7a5b3f8d2e6a41",
"model_uri": "s3://celmind-byom/ORG_ID/1f4c3a9e…/v1/model.onnx",
"framework": "onnx",
"name": "Sales forecaster (imported)",
"description": "Trained offline on 3 years of daily sales",
"visibility": "private",
"signature": { "input_signature": […], "output_signature": […] },
"validation": { "ok": true, … },
"segment_id": "SEGMENT_ID"
}'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.byom.post_models_byom_register(json={
"model_id": "1f4c3a9e6b8d4e219c7a5b3f8d2e6a41",
"model_uri": "s3://celmind-byom/ORG_ID/1f4c3a9e…/v1/model.onnx",
"framework": "onnx",
"name": "Sales forecaster (imported)",
"description": "Trained offline on 3 years of daily sales",
"visibility": "private",
"signature": {"input_signature": […], "output_signature": […]},
"validation": {"ok": True, …},
"segment_id": "SEGMENT_ID",
})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.byom.postModelsByomRegister({
json: {
model_id: "1f4c3a9e6b8d4e219c7a5b3f8d2e6a41",
model_uri: "s3://celmind-byom/ORG_ID/1f4c3a9e…/v1/model.onnx",
framework: "onnx",
name: "Sales forecaster (imported)",
description: "Trained offline on 3 years of daily sales",
visibility: "private",
signature: { input_signature: […], output_signature: […] },
validation: { ok: true, … },
segment_id: "SEGMENT_ID",
},
});
{
"data": {
"blueprint_id": "b7d2e911-…",
"model_id": "5a80c4f3-…",
"training_id": "e2c619ab-…",
…
}
}Request body
| Field | Meaning |
|---|---|
model_id required | The ID from upload. |
model_uri required | The storage URI from the upload response, echoed back. |
framework required | The framework from the validation response. |
name required | Display name for the catalog entry. |
signature required | The signature object from validate, passed back unchanged. |
validation required | The validation object from validate, passed back unchanged. ok must be true. |
segment_id optional | Bind the model to a segment: also creates a pipeline and a ready-to-promote training record (see below). |
description optional | Free-text description. |
visibility optional | private (default) or public. |
license optional | Defaults to proprietary. |
preprocessing optional | Preprocessing applied before inference. |
Response fields
| Field | Meaning |
|---|---|
blueprint_id | The new model's catalog ID (response fields use the internal name, like the route paths). |
model_id | Only when segment_id was passed: the ID of the pipeline created to bind the model to that segment. |
training_id | Only when segment_id was passed: a ready-to-promote training record — go straight to deploy with one POST /v1/deployments call. |
Omit segment_id and you get just the catalog entry.
Compatibility with your segment
When you pass a segment_id, the platform checks the model's signature
against the segment's layout before saving anything: the number of
input features must match the segment's features (base + engineered),
and the output must map onto the segment's labels. Mismatches return a
422 with the concrete issue — never a broken deployment later:
{
"error": {
"code": "byom_incompatible_segment",
"message": "The model's signature doesn't match the segment's feature/label layout.",
"details": {
"issues": ["Model expects 5 input features per step but the segment provides 3 (revenue, units, price)."],
"expected": { "n_features": 3, "features": ["revenue", "units", "price"], "n_labels": 1, "labels": ["revenue"] }
}
}
}The same check runs when you later create a pipeline from a registered
BYOM model (POST /v1/pipelines) and again as a backstop at promotion
time — so a segment whose features changed since registration is caught
before it can break serving.
Errors
| Status | Why | Example message |
|---|---|---|
400 | Required field missing | "Missing required fields: signature, validation" |
403 | Plan quota for uploaded models reached | "BYOM model limit reached. Your plan allows 3 uploaded models per workspace." |
404 | Segment not found in this workspace | "Segment SEGMENT_ID not found in this workspace" |
422 | Signature doesn't fit the segment | error.code byom_incompatible_segment |
422 | Artifact can't be converted for serving | error.code byom_staging_failed |
BYOM models don't count against the regular per-workspace model quota —
they have their own byom_models quota, checked here at register time.
See Billing.
Download your model
Your model is never locked in. Fetch a time-limited download link for the exact file you uploaded:
curl "$API_BASE/v1/models/byom/BLUEPRINT_ID/download" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.byom.get_models_byom_by_blueprint_id_download("BLUEPRINT_ID")
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.byom.getModelsByomByBlueprintIdDownload("BLUEPRINT_ID");
{
"data": {
"download_url": "https://celmind-byom.s3.amazonaws.com/…&X-Amz-Expires=3600…",
"filename": "9f2c41d8-7b3a-4e1f-8c2d-5a6b7c8d9e0f.onnx",
"framework": "onnx",
"expires_in_seconds": 3600
}
}The URL is valid for one hour; request a fresh one any time. The file
saves under the model's ID (the download link carries a
Content-Disposition header, so the name is applied automatically).
Errors
| Status | Why | Example message |
|---|---|---|
403 | Not your model (and not public) | "You don't have access to this model" |
404 | Unknown id, or not a BYOM model | "BYOM model BLUEPRINT_ID not found" |

