StayCharted AI Model TrainerPartner APIPrediction APIstaycharted.com
Contents

Models

GETList the models this key can useGETOne model, with the forms to call it

Predictions

POSTClassify one record, flat responsePOSTClassify one or many records

Reference

AuthenticationError codesRate limitingSchemas

AI Model Trainer API

Classify text with a model you trained in StayCharted AI Model Trainer.

Version 1.0.0 · Base URL https://amt.staycharted.com · OpenAPI 3.1 · OpenAPI 3.0 for connector builders

Two endpoints. You train a model in the StayCharted app — by uploading labelled examples — and then call it from your own code, or from an automation platform, to classify new text.

Getting a key. Sign in to StayCharted and issue one yourself under API keys. The full key is shown once, in the response that creates it, and never again. Keys are issued from a signed-in session, not from this API: a key that can mint further keys cannot really be revoked, because whoever held it has already made another.

What counts against your plan. Predictions are counted per record and drawn from one monthly budget shared with spreadsheet fills in the app — the same model doing the same work on the same hardware, so it is the same unit. A request refused by validation costs nothing.

Rate limits are separate, and counted in records per minute for the whole account. Every key on the account draws on one budget, and a request of 100 records costs a hundred times one of a single record — the limit exists to protect the server, so it is measured in the work that arrives, not in the number of envelopes it arrives in.

Batch vs bulk. One request carries up to 100 records. That makes this the right tool for a steady trickle — classify a ticket as it arrives, enrich a row as it is created. For tens of thousands of rows, upload the file in the app instead: it runs asynchronously on hardware sized for it, and costs you one upload rather than thousands of calls.

Errors. Every error response is JSON and carries a stable code. Branch on code, never on the human-readable error string, which is written for people and may be reworded. A refusal caused by your plan additionally carries entitlement, limit, used, resetsAt and upgrade, so a client can tell "wait until the cycle resets" from "this plan will never allow it".

Authentication

Authorization: Bearer <your key>. Issue and revoke keys in the StayCharted app; up to ten may be active at once, so an integration can be rotated without downtime.

GET /api/v1/models HTTP/1.1
Host: amt.staycharted.com
Authorization: Bearer amt_live_…

Endpoints

Models

Predictions

Models

The models on your account that are deployed and answering.

GET /api/v1/models

List the models this key can use

Only deployed models appear. A model that has been trained but not published is not callable and is not listed.

Cheap enough to be the endpoint an integration uses to verify a key — it touches no model and spends no predictions. It has its own small allowance, 60 requests a minute per workspace, separate from the records-a-minute limit on predictions.

Responses

200 — The deployed models on this account.

FieldTypeRequiredNotes
workspaceobjectyesThe workspace this key belongs to. Integrations use it to label a saved connection, so two keys can be told apart.
workspace.idstring (uuid)no
workspace.namestring | nullnoThe workspace's name, as the app's workspace switcher shows it.
workspace.ownerstring | nullnoWho shared the workspace: their name, or their email address when they have not given a name, as the app's switcher shows it. Null when it cannot be read.
modelsarray of objectyes
models[].idstring (uuid)yes
models[].namestringyes
models[].custombooleanyesTrue for a Dedicated AI Model, false for a Standard Model. Both are called the same way.
models[].modalitystringyesWhat the model reads. An image model takes a link to a picture (or imageBase64), and some also read a text column. (one of text, image)
models[].inputsarray of stringyesFor a text model, the columns it was trained on: the field names to send when input is an object. A record that sends none of them is refused with api.input_fields_missing. Empty for an image model.
models[].readsTextbooleannoWhether the model reads text. Always true for a text model. For an image model, whether it also reads a message with each picture: if true, send {"image": "<link>", "text": "..."}; if false, send the link alone.
models[].answerobjectyesWhat every answer from this model carries. single: one category, in label. list: several values from one column, in labels. record: one value per column, in fields, keyed by the names listed here. Every answer also has label, confidence and needsReview.
models[].answer.shapestringyes(one of single, list, record)
models[].answer.fieldsarray of stringyesThe answer's column names, as trained.

Example — two

{
  "workspace": {
    "id": "2fd16021-706c-4608-b488-9d0c267c7f28",
    "name": "Support",
    "owner": "Sam Patel"
  },
  "models": [
    {
      "id": "8f14e45f-ceea-467a-9b2c-1d5c2b0e7a11",
      "name": "Support tickets",
      "custom": true,
      "modality": "text",
      "inputs": [
        "Subject",
        "Body"
      ],
      "answer": {
        "shape": "record",
        "fields": [
          "Category",
          "Assignment group"
        ]
      }
    },
    {
      "id": "3c59dc04-8e88-4950-9f2b-7a1c2a1e0b02",
      "name": "Product feedback",
      "custom": false,
      "modality": "text",
      "inputs": [
        "Feedback"
      ],
      "answer": {
        "shape": "single",
        "fields": [
          "Area"
        ]
      }
    }
  ]
}
Errors: 401, 402, 403, 429

401 — The key is missing, malformed, unknown or revoked. Deliberately one answer for all four: distinguishing them would confirm to a stranger which keys exist.

Example — missing

{
  "error": "Send your API key as: Authorization: Bearer <key>.",
  "code": "api.key_missing"
}

Example — invalid

{
  "error": "That API key is not valid.",
  "code": "api.key_invalid"
}

402 — Your plan will not allow this. Two causes, and a client should treat them differently: plan.feature_not_included means the API is not on this plan at all and retrying will never help, while plan.prediction_limit means the month's predictions are spent and resetsAt says when they come back.

Example — plan.feature_not_included

{
  "error": "Your plan does not include the Prediction API. The model is still available in the app.",
  "code": "plan.feature_not_included",
  "entitlement": "predictionApi",
  "limit": null,
  "used": null,
  "resetsAt": null,
  "upgrade": {
    "planId": "essentials",
    "planName": "Essentials",
    "monthlyPriceCents": 6900,
    "currency": "usd",
    "newLimit": true
  }
}

Example — plan.prediction_limit

{
  "error": "This needs 40 predictions and 12 remain this billing period.",
  "code": "plan.prediction_limit",
  "entitlement": "predictionsPerMonth",
  "limit": 50000,
  "used": 49988,
  "resetsAt": "2026-10-01T00:00:00.000Z",
  "upgrade": {
    "planId": "business",
    "planName": "Business",
    "monthlyPriceCents": 29900,
    "currency": "usd",
    "newLimit": 250000
  }
}

403 — The account is suspended. Its data and its plan are intact, but it cannot spend anything until support reactivates it.

{
  "error": "This account is suspended. Contact support to reactivate it.",
  "code": "account.suspended"
}

429 — Too much sent this minute for your plan. Counted in RECORDS and for the whole account — every key draws on one budget, so adding keys does not add throughput. The allowance refills continuously rather than resetting on the minute, and the bucket always holds at least one full-size request, so a 100-record call is never permanently impossible even on a plan whose per-minute rate is lower than that. A request that does not fit is refused whole; no part of it is processed and none of it is counted against the monthly budget.

HeaderMeaning
X-RateLimit-LimitRecords a minute allowed on this plan, for the whole account. Every key on the account draws on this one budget.
X-RateLimit-RemainingRecords that can be sent right now. Refills continuously at the rate above rather than resetting on the minute, and holds at least one full request, so a request of the maximum size is always eventually accepted. A request that does not fit is refused whole — no part of it is processed — with Retry-After giving the seconds until it will.
Retry-AfterSeconds until enough allowance has refilled for THIS request. There is no window to wait out — a smaller request may well succeed sooner.
{
  "error": "Rate limit reached — 60 records a minute on your plan, and this request is 100. Try again in 40s.",
  "code": "api.rate_limited",
  "retryAfterSeconds": 24
}

GET /api/v1/models/{id}

One model, with the forms to call it

The same description as in the list, plus two JSON Schemas built from it: inputSchema, the fields to send as input to classify, and answerSchema, what classify returns. Tools that build their forms from a schema use these; everything else can use inputs and answer.

Shares the model list's allowance. A model that is not published, or that this plan cannot call, is not found.

Path parameters

NameTypeRequiredDescription
idstring (uuid)yesThe model's id, from GET /api/v1/models.

Responses

200 — The model.

A Model, plus the two schemas an integration builds its forms from.

FieldTypeRequiredNotes
idstring (uuid)yes
namestringyes
custombooleanyes
modalitystringyes(one of text, image)
inputsarray of stringyes
readsTextbooleanno
answerobjectyes
answer.shapestringno(one of single, list, record)
answer.fieldsarray of stringno
inputSchemaobjectyesJSON Schema of the input object for classify.
answerSchemaobjectyesJSON Schema of what classify returns for this model.
{
  "id": "8f14e45f-ceea-467a-9b2c-1d5c2b0e7a11",
  "name": "Support tickets",
  "custom": false,
  "modality": "text",
  "inputs": [
    "Subject",
    "Body"
  ],
  "answer": {
    "shape": "record",
    "fields": [
      "Category",
      "Assignment group"
    ]
  },
  "inputSchema": {
    "type": "object",
    "properties": {
      "Subject": {
        "type": "string",
        "title": "Subject"
      },
      "Body": {
        "type": "string",
        "title": "Body"
      }
    }
  },
  "answerSchema": {
    "type": "object",
    "properties": {
      "label": {
        "type": "string",
        "title": "Answer"
      },
      "confidence": {
        "type": "number",
        "title": "Confidence (0 to 1)"
      },
      "needsReview": {
        "type": "boolean",
        "title": "Needs review"
      },
      "fields": {
        "type": "object",
        "title": "Answer fields",
        "properties": {
          "Category": {
            "type": "string",
            "title": "Category"
          },
          "Assignment group": {
            "type": "string",
            "title": "Assignment group"
          }
        }
      }
    }
  }
}
Errors: 401, 402, 403, 404, 429

401 — The key is missing, malformed, unknown or revoked. Deliberately one answer for all four: distinguishing them would confirm to a stranger which keys exist.

Example — missing

{
  "error": "Send your API key as: Authorization: Bearer <key>.",
  "code": "api.key_missing"
}

Example — invalid

{
  "error": "That API key is not valid.",
  "code": "api.key_invalid"
}

402 — Your plan will not allow this. Two causes, and a client should treat them differently: plan.feature_not_included means the API is not on this plan at all and retrying will never help, while plan.prediction_limit means the month's predictions are spent and resetsAt says when they come back.

Example — plan.feature_not_included

{
  "error": "Your plan does not include the Prediction API. The model is still available in the app.",
  "code": "plan.feature_not_included",
  "entitlement": "predictionApi",
  "limit": null,
  "used": null,
  "resetsAt": null,
  "upgrade": {
    "planId": "essentials",
    "planName": "Essentials",
    "monthlyPriceCents": 6900,
    "currency": "usd",
    "newLimit": true
  }
}

Example — plan.prediction_limit

{
  "error": "This needs 40 predictions and 12 remain this billing period.",
  "code": "plan.prediction_limit",
  "entitlement": "predictionsPerMonth",
  "limit": 50000,
  "used": 49988,
  "resetsAt": "2026-10-01T00:00:00.000Z",
  "upgrade": {
    "planId": "business",
    "planName": "Business",
    "monthlyPriceCents": 29900,
    "currency": "usd",
    "newLimit": 250000
  }
}

403 — The account is suspended. Its data and its plan are intact, but it cannot spend anything until support reactivates it.

{
  "error": "This account is suspended. Contact support to reactivate it.",
  "code": "account.suspended"
}

404 — No published model with that id on this account.

{
  "error": "No such model.",
  "code": "api.model_not_found"
}

429 — Too much sent this minute for your plan. Counted in RECORDS and for the whole account — every key draws on one budget, so adding keys does not add throughput. The allowance refills continuously rather than resetting on the minute, and the bucket always holds at least one full-size request, so a 100-record call is never permanently impossible even on a plan whose per-minute rate is lower than that. A request that does not fit is refused whole; no part of it is processed and none of it is counted against the monthly budget.

HeaderMeaning
X-RateLimit-LimitRecords a minute allowed on this plan, for the whole account. Every key on the account draws on this one budget.
X-RateLimit-RemainingRecords that can be sent right now. Refills continuously at the rate above rather than resetting on the minute, and holds at least one full request, so a request of the maximum size is always eventually accepted. A request that does not fit is refused whole — no part of it is processed — with Retry-After giving the seconds until it will.
Retry-AfterSeconds until enough allowance has refilled for THIS request. There is no window to wait out — a smaller request may well succeed sooner.
{
  "error": "Rate limit reached — 60 records a minute on your plan, and this request is 100. Try again in 40s.",
  "code": "api.rate_limited",
  "retryAfterSeconds": 24
}

Predictions

POST /api/v1/models/{id}/classify

Classify one record, flat response

One record in, one answer out, with nothing nested.

predict is the endpoint for code. This one is for everything that is not code — Zapier, Make, Power Automate, Salesforce Flow, a spreadsheet formula. Those tools bind a field to a response value by picking it from a list, and predictions[0].label is awkward or impossible to express in most of them.

So every value here is a top-level key: label, confidence, modelId, modelName. That is the whole difference. Same auth, same plan limits, same monthly budget, same one record spent.

There is no usage field because it would always be 1.

Path parameters

NameTypeRequiredDescription
idstring (uuid)yesThe model's id, from GET /api/v1/models.

Request body (application/json)

FieldTypeRequiredNotes
inputone ofyesThe text to classify, or an object of the fields the model reads (FieldsInput). For an IMAGE model: a link to the picture (http or https), or {"imageBase64": "..."} for a picture sent in the request (under 10 MB — send a link for anything larger). A picture model that also reads text (readsText) takes {"image": "<link>", "text": "..."} or {"imageBase64": "...", "text": "..."}.
{
  "input": "My invoice charged me twice this month"
}

Responses

200 — The answer, flat.

HeaderMeaning
X-RateLimit-LimitRecords a minute allowed on this plan, for the whole account. Every key on the account draws on this one budget.
X-RateLimit-RemainingRecords that can be sent right now. Refills continuously at the rate above rather than resetting on the minute, and holds at least one full request, so a request of the maximum size is always eventually accepted. A request that does not fit is refused whole — no part of it is processed — with Retry-After giving the seconds until it will.

Deliberately flat — every value a top-level key, so a no-code tool can bind to it without walking an array.

FieldTypeRequiredNotes
labelstring | nullyesThe category. Null only if the model returned nothing usable.
confidencenumber | nullyes0 to 1. Null when the engine does not report one.
modelIdstring (uuid)yes
modelNamestringyes
errorstringnoImage models only, and only when this picture could not be used: why, as a code — not_found, forbidden, unreachable, timeout, too_large, web_page, not_an_image, unsupported_format, too_small, corrupt, blocked_address, not_a_link. The label and confidence are null, and no image is counted for it.
messagestringnoThe same reason in plain words, for showing to a person.
noPhotobooleannoText + pictures models only. The answer came from the text alone, with no picture.
pictureProblemstringnoText + pictures models only. A picture was sent but could not be used (same codes as error); the answer came from the text.
needsReviewbooleannoTrue when the confidence is below the model's review cutoff: the answer a person should check before anything acts on it. Present on every answer with a label. On plans with the Review Queue, these answers also wait there for your team.
labelsarray of stringnoModels whose answer is a list (several values in one column, such as the groups that can open a file): the values, sorted. label holds the same values joined with "; ".
fieldsobjectnoModels whose answer has several columns (such as GL account and cost center): each column's value, by the column's name. label holds the values joined with " | ".
{
  "label": "Billing",
  "confidence": 0.94,
  "modelId": "8f14e45f-ceea-467a-9b2c-1d5c2b0e7a11",
  "modelName": "Support tickets"
}
Errors: 400, 401, 402, 403, 404, 409, 429, 503

400 — No text was sent.

{
  "error": "Send \"input\" with the text to classify.",
  "code": "api.input_missing"
}

401 — The key is missing, malformed, unknown or revoked. Deliberately one answer for all four: distinguishing them would confirm to a stranger which keys exist.

Example — missing

{
  "error": "Send your API key as: Authorization: Bearer <key>.",
  "code": "api.key_missing"
}

Example — invalid

{
  "error": "That API key is not valid.",
  "code": "api.key_invalid"
}

402 — Your plan will not allow this. Two causes, and a client should treat them differently: plan.feature_not_included means the API is not on this plan at all and retrying will never help, while plan.prediction_limit means the month's predictions are spent and resetsAt says when they come back.

Example — plan.feature_not_included

{
  "error": "Your plan does not include the Prediction API. The model is still available in the app.",
  "code": "plan.feature_not_included",
  "entitlement": "predictionApi",
  "limit": null,
  "used": null,
  "resetsAt": null,
  "upgrade": {
    "planId": "essentials",
    "planName": "Essentials",
    "monthlyPriceCents": 6900,
    "currency": "usd",
    "newLimit": true
  }
}

Example — plan.prediction_limit

{
  "error": "This needs 40 predictions and 12 remain this billing period.",
  "code": "plan.prediction_limit",
  "entitlement": "predictionsPerMonth",
  "limit": 50000,
  "used": 49988,
  "resetsAt": "2026-10-01T00:00:00.000Z",
  "upgrade": {
    "planId": "business",
    "planName": "Business",
    "monthlyPriceCents": 29900,
    "currency": "usd",
    "newLimit": 250000
  }
}

403 — The account is suspended. Its data and its plan are intact, but it cannot spend anything until support reactivates it.

{
  "error": "This account is suspended. Contact support to reactivate it.",
  "code": "account.suspended"
}

404 — No model with that id on this account.

{
  "error": "No such model.",
  "code": "api.model_not_found"
}

409 — The model exists but cannot answer: it is not deployed yet (api.model_not_deployed — publish it in the app first), or it has to be retrained before it can answer again (api.model_needs_retraining — its owner retrains it in the app). Nothing was counted against your budget.

{
  "error": "That model is not deployed yet.",
  "code": "api.model_not_deployed"
}

429 — Too much sent this minute for your plan. Counted in RECORDS and for the whole account — every key draws on one budget, so adding keys does not add throughput. The allowance refills continuously rather than resetting on the minute, and the bucket always holds at least one full-size request, so a 100-record call is never permanently impossible even on a plan whose per-minute rate is lower than that. A request that does not fit is refused whole; no part of it is processed and none of it is counted against the monthly budget.

HeaderMeaning
X-RateLimit-LimitRecords a minute allowed on this plan, for the whole account. Every key on the account draws on this one budget.
X-RateLimit-RemainingRecords that can be sent right now. Refills continuously at the rate above rather than resetting on the minute, and holds at least one full request, so a request of the maximum size is always eventually accepted. A request that does not fit is refused whole — no part of it is processed — with Retry-After giving the seconds until it will.
Retry-AfterSeconds until enough allowance has refilled for THIS request. There is no window to wait out — a smaller request may well succeed sooner.
{
  "error": "Rate limit reached — 60 records a minute on your plan, and this request is 100. Try again in 40s.",
  "code": "api.rate_limited",
  "retryAfterSeconds": 24
}

503 — The model cannot answer right now because a service it depends on is unavailable — usually for a minute or two during a restart. Nothing was counted against your budget. Retry after a short wait.

{
  "error": "This model is temporarily unable to answer. Try again in a few minutes.",
  "code": "api.model_unavailable"
}

POST /api/v1/models/{id}/predict

Classify one or many records

Send input with a single string, or inputs with an array of up to 100. predictions comes back as an array either way — a response whose type depends on the request's type is the kind of thing an integration gets wrong once and blames you for.

Answers are returned in the order they were sent. Every record in the request is counted, including ones the model answers with low confidence.

The whole request is checked against your remaining monthly budget before any of it runs, so a request that would cross the limit is refused entirely rather than half-served.

Path parameters

NameTypeRequiredDescription
idstring (uuid)yesThe model's id, from GET /api/v1/models.

Request body (application/json)

Exactly one of input or inputs.

Shape 1

FieldTypeRequiredNotes
inputone ofyes

Shape 2

FieldTypeRequiredNotes
inputsarray of one ofyesText for a text model. For an IMAGE model, each item is a link to a picture or an ImageInput, and at most 20 of them (api.too_many_images): each is fetched and read inside the request. (minItems: 1, maxItems: 100)

Example — One record

{
  "input": "My invoice charged me twice this month"
}

Example — Several at once

{
  "inputs": [
    "My invoice charged me twice this month",
    "The parcel arrived with a cracked screen"
  ]
}

Responses

200 — One prediction per input, in the order they were sent.

HeaderMeaning
X-RateLimit-LimitRecords a minute allowed on this plan, for the whole account. Every key on the account draws on this one budget.
X-RateLimit-RemainingRecords that can be sent right now. Refills continuously at the rate above rather than resetting on the minute, and holds at least one full request, so a request of the maximum size is always eventually accepted. A request that does not fit is refused whole — no part of it is processed — with Retry-After giving the seconds until it will.
FieldTypeRequiredNotes
modelobjectyes
model.idstring (uuid)yes
model.namestringyes
predictionsarray of objectyes
predictions[].inputstringyesEchoed back, so a batch can be matched up without relying on order alone.
predictions[].labelstring | nullyesNull when the model could not produce one.
predictions[].confidencenumber | nullyes0 to 1. Null when the model does not report one.
predictions[].errorstringnoImage models only, and only when this picture could not be used: why, as a code — not_found, forbidden, unreachable, timeout, too_large, web_page, not_an_image, unsupported_format, too_small, corrupt, blocked_address, not_a_link. The label and confidence are null, and no image is counted for it.
predictions[].messagestringnoThe same reason in plain words, for showing to a person.
predictions[].noPhotobooleannoText + pictures models only. The answer came from the text alone, with no picture.
predictions[].pictureProblemstringnoText + pictures models only. A picture was sent but could not be used (same codes as error); the answer came from the text.
predictions[].needsReviewbooleannoTrue when the confidence is below the model's review cutoff: the answer a person should check before anything acts on it. Present on every answer with a label. On plans with the Review Queue, these answers also wait there for your team.
predictions[].labelsarray of stringnoModels whose answer is a list (several values in one column, such as the groups that can open a file): the values, sorted. label holds the same values joined with "; ".
predictions[].fieldsobjectnoModels whose answer has several columns (such as GL account and cost center): each column's value, by the column's name. label holds the values joined with " | ".
usageobjectyes
usage.predictionsintegeryesRecords counted against this month's budget by this request.

Example — two

{
  "model": {
    "id": "8f14e45f-ceea-467a-9b2c-1d5c2b0e7a11",
    "name": "Support tickets"
  },
  "predictions": [
    {
      "input": "My invoice charged me twice this month",
      "label": "Billing",
      "confidence": 0.94
    },
    {
      "input": "The parcel arrived with a cracked screen",
      "label": "Shipping",
      "confidence": 0.71
    }
  ],
  "usage": {
    "predictions": 2
  }
}
Errors: 400, 401, 402, 403, 404, 409, 429, 503

400 — The request body is wrong. Checked before anything else, so these cost no predictions and no rate-limit budget.

Example — api.input_required

{
  "error": "Send \"input\" with one string, or \"inputs\" with an array of them.",
  "code": "api.input_required"
}

Example — api.too_many_inputs

{
  "error": "One request carries at most 100 records — this one has 250. Send more requests, or fill a spreadsheet in the app for a large job.",
  "code": "api.too_many_inputs"
}

Example — api.too_many_images

{
  "error": "One request carries at most 20 pictures — this one has 50. Send more requests, or fill a file in the app for a large job.",
  "code": "api.too_many_images"
}

Example — api.input_invalid

{
  "error": "Every input must be a non-empty string, or an object of the fields the model reads.",
  "code": "api.input_invalid"
}

Example — api.input_fields_missing

{
  "error": "None of the fields this model reads were sent. It reads: \"Name\", \"Path\", \"Owner\".",
  "code": "api.input_fields_missing"
}

Example — api.text_input_invalid

{
  "error": "This model reads text: send each input as a string.",
  "code": "api.text_input_invalid"
}

Example — api.image_input_invalid

{
  "error": "This model reads pictures: send each input as a link to the picture (http:// or https://), or as {\"imageBase64\": \"...\"}.",
  "code": "api.image_input_invalid"
}

Example — api.image_too_large

{
  "error": "A picture sent in the request must be under 10 MB on its own, or 1.5 MB each when several are sent together — send a link for a larger one.",
  "code": "api.image_too_large"
}

401 — The key is missing, malformed, unknown or revoked. Deliberately one answer for all four: distinguishing them would confirm to a stranger which keys exist.

Example — missing

{
  "error": "Send your API key as: Authorization: Bearer <key>.",
  "code": "api.key_missing"
}

Example — invalid

{
  "error": "That API key is not valid.",
  "code": "api.key_invalid"
}

402 — Your plan will not allow this. Two causes, and a client should treat them differently: plan.feature_not_included means the API is not on this plan at all and retrying will never help, while plan.prediction_limit means the month's predictions are spent and resetsAt says when they come back.

Example — plan.feature_not_included

{
  "error": "Your plan does not include the Prediction API. The model is still available in the app.",
  "code": "plan.feature_not_included",
  "entitlement": "predictionApi",
  "limit": null,
  "used": null,
  "resetsAt": null,
  "upgrade": {
    "planId": "essentials",
    "planName": "Essentials",
    "monthlyPriceCents": 6900,
    "currency": "usd",
    "newLimit": true
  }
}

Example — plan.prediction_limit

{
  "error": "This needs 40 predictions and 12 remain this billing period.",
  "code": "plan.prediction_limit",
  "entitlement": "predictionsPerMonth",
  "limit": 50000,
  "used": 49988,
  "resetsAt": "2026-10-01T00:00:00.000Z",
  "upgrade": {
    "planId": "business",
    "planName": "Business",
    "monthlyPriceCents": 29900,
    "currency": "usd",
    "newLimit": 250000
  }
}

403 — The account is suspended. Its data and its plan are intact, but it cannot spend anything until support reactivates it.

{
  "error": "This account is suspended. Contact support to reactivate it.",
  "code": "account.suspended"
}

404 — No model with that id on this account.

{
  "error": "No such model.",
  "code": "api.model_not_found"
}

409 — The model exists but cannot answer: it is not deployed yet (api.model_not_deployed — publish it in the app first), or it has to be retrained before it can answer again (api.model_needs_retraining — its owner retrains it in the app). Nothing was counted against your budget.

{
  "error": "That model is not deployed yet.",
  "code": "api.model_not_deployed"
}

429 — Too much sent this minute for your plan. Counted in RECORDS and for the whole account — every key draws on one budget, so adding keys does not add throughput. The allowance refills continuously rather than resetting on the minute, and the bucket always holds at least one full-size request, so a 100-record call is never permanently impossible even on a plan whose per-minute rate is lower than that. A request that does not fit is refused whole; no part of it is processed and none of it is counted against the monthly budget.

HeaderMeaning
X-RateLimit-LimitRecords a minute allowed on this plan, for the whole account. Every key on the account draws on this one budget.
X-RateLimit-RemainingRecords that can be sent right now. Refills continuously at the rate above rather than resetting on the minute, and holds at least one full request, so a request of the maximum size is always eventually accepted. A request that does not fit is refused whole — no part of it is processed — with Retry-After giving the seconds until it will.
Retry-AfterSeconds until enough allowance has refilled for THIS request. There is no window to wait out — a smaller request may well succeed sooner.
{
  "error": "Rate limit reached — 60 records a minute on your plan, and this request is 100. Try again in 40s.",
  "code": "api.rate_limited",
  "retryAfterSeconds": 24
}

503 — The model cannot answer right now because a service it depends on is unavailable — usually for a minute or two during a restart. Nothing was counted against your budget. Retry after a short wait.

{
  "error": "This model is temporarily unable to answer. Try again in a few minutes.",
  "code": "api.model_unavailable"
}

Error codes

Every error is JSON with a stable code. Branch on the code; the error sentence is for people and may be reworded.

HTTPCodeWhat it means
400api.image_input_invalidThis model reads pictures: send each input as a link to the picture (http:// or https://), or as {"imageBase64": "..."}.
400api.image_too_largeA picture sent in the request must be under 10 MB on its own, or 1.5 MB each when several are sent together — send a link for a larger one.
400api.input_fields_missingNone of the fields this model reads were sent. It reads: "Name", "Path", "Owner".
400api.input_invalidEvery input must be a non-empty string, or an object of the fields the model reads.
400api.input_missingSend "input" with the text to classify.
400api.input_requiredSend "input" with one string, or "inputs" with an array of them.
400api.text_input_invalidThis model reads text: send each input as a string.
400api.too_many_imagesOne request carries at most 20 pictures — this one has 50. Send more requests, or fill a file in the app for a large job.
400api.too_many_inputsOne request carries at most 100 records — this one has 250. Send more requests, or fill a spreadsheet in the app for a large job.
401api.key_invalidThat API key is not valid.
401api.key_missingSend your API key as: Authorization: Bearer <key>.
402plan.feature_not_includedYour plan does not include the Prediction API. The model is still available in the app.
402plan.prediction_limitThis needs 40 predictions and 12 remain this billing period.
403account.suspendedThis account is suspended. Contact support to reactivate it.
404api.model_not_foundNo such model.
409api.model_not_deployedThat model is not deployed yet.
429api.rate_limitedRate limit reached — 60 records a minute on your plan, and this request is 100. Try again in 40s.
503api.model_unavailableThis model is temporarily unable to answer. Try again in a few minutes.

Rate limiting

Too much sent this minute for your plan. Counted in RECORDS and for the whole account — every key draws on one budget, so adding keys does not add throughput.

The allowance refills continuously rather than resetting on the minute, and the bucket always holds at least one full-size request, so a 100-record call is never permanently impossible even on a plan whose per-minute rate is lower than that. A request that does not fit is refused whole; no part of it is processed and none of it is counted against the monthly budget.

HeaderMeaning
X-RateLimit-LimitRecords a minute allowed on this plan, for the whole account. Every key on the account draws on this one budget.
X-RateLimit-RemainingRecords that can be sent right now. Refills continuously at the rate above rather than resetting on the minute, and holds at least one full request, so a request of the maximum size is always eventually accepted. A request that does not fit is refused whole — no part of it is processed — with Retry-After giving the seconds until it will.
Retry-AfterOn a 429: seconds until enough allowance has refilled for this request.

Schemas

Model

FieldTypeRequiredNotes
idstring (uuid)yes
namestringyes
custombooleanyesTrue for a Dedicated AI Model, false for a Standard Model. Both are called the same way.
modalitystringyesWhat the model reads. An image model takes a link to a picture (or imageBase64), and some also read a text column. (one of text, image)
inputsarray of stringyesFor a text model, the columns it was trained on: the field names to send when input is an object. A record that sends none of them is refused with api.input_fields_missing. Empty for an image model.
readsTextbooleannoWhether the model reads text. Always true for a text model. For an image model, whether it also reads a message with each picture: if true, send {"image": "<link>", "text": "..."}; if false, send the link alone.
answerobjectyesWhat every answer from this model carries. single: one category, in label. list: several values from one column, in labels. record: one value per column, in fields, keyed by the names listed here. Every answer also has label, confidence and needsReview.
answer.shapestringyes(one of single, list, record)
answer.fieldsarray of stringyesThe answer's column names, as trained.

ModelDetail

A Model, plus the two schemas an integration builds its forms from.

FieldTypeRequiredNotes
idstring (uuid)yes
namestringyes
custombooleanyes
modalitystringyes(one of text, image)
inputsarray of stringyes
readsTextbooleanno
answerobjectyes
answer.shapestringno(one of single, list, record)
answer.fieldsarray of stringno
inputSchemaobjectyesJSON Schema of the input object for classify.
answerSchemaobjectyesJSON Schema of what classify returns for this model.

PredictRequest

Exactly one of input or inputs.

Shape 1

FieldTypeRequiredNotes
inputone ofyes

Shape 2

FieldTypeRequiredNotes
inputsarray of one ofyesText for a text model. For an IMAGE model, each item is a link to a picture or an ImageInput, and at most 20 of them (api.too_many_images): each is fetched and read inside the request. (minItems: 1, maxItems: 100)

FieldsInput

A record for a TEXT model, as the fields it was trained on, by column name: {"Name": "Q4 budget.xlsx", "Path": "/sites/Finance/Budget"}. The model reads them exactly as it read its training rows, so you don't write the Column: value text yourself. Nested objects are read with dotted names ({"owner": {"department": "HR"}} is the column owner.department), so a record exported from another system can be sent as it is. Fields the model doesn't read are ignored; a record with none of them is refused (api.input_fields_missing, which names the fields it reads).

A model trained on a SharePoint permission export reads Path. The item's location can be sent as Path, URL, webUrl or Resource Path, as a full URL or a server-relative path: {"webUrl": "https://contoso.sharepoint.com/sites/HR/Shared%20Documents/x.docx"} is read as /sites/HR/Shared Documents/x.docx, the way the export was read in training.

ImageInput

A picture sent in the request, for an image model. Each picture the model reads counts once against Images / month as well as once against the prediction budget; a link that cannot be fetched counts against neither.

For a model that reads text with its pictures ("text + pictures"), also send text: the text columns it was trained on, as Column: value lines. Send the picture as image (a link) or imageBase64, or send text alone for a message with no picture; the answer then says "noPhoto": true.

FieldTypeRequiredNotes
imageBase64stringnoThe picture file (JPEG, PNG, WebP, GIF, BMP or TIFF), base64-encoded. Under 10 MB when it is the only picture in the request, 1.5 MB each when several are sent together.
imagestringnoA link to the picture (http or https). Text + pictures models.
textstringnoThe text read with the picture. Text + pictures models.

ClassifyResponse

Deliberately flat — every value a top-level key, so a no-code tool can bind to it without walking an array.

FieldTypeRequiredNotes
labelstring | nullyesThe category. Null only if the model returned nothing usable.
confidencenumber | nullyes0 to 1. Null when the engine does not report one.
modelIdstring (uuid)yes
modelNamestringyes
errorstringnoImage models only, and only when this picture could not be used: why, as a code — not_found, forbidden, unreachable, timeout, too_large, web_page, not_an_image, unsupported_format, too_small, corrupt, blocked_address, not_a_link. The label and confidence are null, and no image is counted for it.
messagestringnoThe same reason in plain words, for showing to a person.
noPhotobooleannoText + pictures models only. The answer came from the text alone, with no picture.
pictureProblemstringnoText + pictures models only. A picture was sent but could not be used (same codes as error); the answer came from the text.
needsReviewbooleannoTrue when the confidence is below the model's review cutoff: the answer a person should check before anything acts on it. Present on every answer with a label. On plans with the Review Queue, these answers also wait there for your team.
labelsarray of stringnoModels whose answer is a list (several values in one column, such as the groups that can open a file): the values, sorted. label holds the same values joined with "; ".
fieldsobjectnoModels whose answer has several columns (such as GL account and cost center): each column's value, by the column's name. label holds the values joined with " | ".

NeedsReview

True when the confidence is below the model's review cutoff: the answer a person should check before anything acts on it. Present on every answer with a label. On plans with the Review Queue, these answers also wait there for your team.

AnswerLabels

Models whose answer is a list (several values in one column, such as the groups that can open a file): the values, sorted. label holds the same values joined with "; ".

AnswerFields

Models whose answer has several columns (such as GL account and cost center): each column's value, by the column's name. label holds the values joined with " | ".

Prediction

FieldTypeRequiredNotes
inputstringyesEchoed back, so a batch can be matched up without relying on order alone.
labelstring | nullyesNull when the model could not produce one.
confidencenumber | nullyes0 to 1. Null when the model does not report one.
errorstringnoImage models only, and only when this picture could not be used: why, as a code — not_found, forbidden, unreachable, timeout, too_large, web_page, not_an_image, unsupported_format, too_small, corrupt, blocked_address, not_a_link. The label and confidence are null, and no image is counted for it.
messagestringnoThe same reason in plain words, for showing to a person.
noPhotobooleannoText + pictures models only. The answer came from the text alone, with no picture.
pictureProblemstringnoText + pictures models only. A picture was sent but could not be used (same codes as error); the answer came from the text.
needsReviewbooleannoTrue when the confidence is below the model's review cutoff: the answer a person should check before anything acts on it. Present on every answer with a label. On plans with the Review Queue, these answers also wait there for your team.
labelsarray of stringnoModels whose answer is a list (several values in one column, such as the groups that can open a file): the values, sorted. label holds the same values joined with "; ".
fieldsobjectnoModels whose answer has several columns (such as GL account and cost center): each column's value, by the column's name. label holds the values joined with " | ".

PredictResponse

FieldTypeRequiredNotes
modelobjectyes
model.idstring (uuid)yes
model.namestringyes
predictionsarray of objectyes
predictions[].inputstringyesEchoed back, so a batch can be matched up without relying on order alone.
predictions[].labelstring | nullyesNull when the model could not produce one.
predictions[].confidencenumber | nullyes0 to 1. Null when the model does not report one.
predictions[].errorstringnoImage models only, and only when this picture could not be used: why, as a code — not_found, forbidden, unreachable, timeout, too_large, web_page, not_an_image, unsupported_format, too_small, corrupt, blocked_address, not_a_link. The label and confidence are null, and no image is counted for it.
predictions[].messagestringnoThe same reason in plain words, for showing to a person.
predictions[].noPhotobooleannoText + pictures models only. The answer came from the text alone, with no picture.
predictions[].pictureProblemstringnoText + pictures models only. A picture was sent but could not be used (same codes as error); the answer came from the text.
predictions[].needsReviewbooleannoTrue when the confidence is below the model's review cutoff: the answer a person should check before anything acts on it. Present on every answer with a label. On plans with the Review Queue, these answers also wait there for your team.
predictions[].labelsarray of stringnoModels whose answer is a list (several values in one column, such as the groups that can open a file): the values, sorted. label holds the same values joined with "; ".
predictions[].fieldsobjectnoModels whose answer has several columns (such as GL account and cost center): each column's value, by the column's name. label holds the values joined with " | ".
usageobjectyes
usage.predictionsintegeryesRecords counted against this month's budget by this request.

Error

FieldTypeRequiredNotes
errorstringyesFor a person to read. May be reworded — do not match on it.
codestringyesStable. Branch on this.

PlanError

FieldTypeRequiredNotes
errorstringyesFor a person to read. May be reworded — do not match on it.
codestringyesStable. Branch on this.
entitlementstring | nullnoWhich plan limit refused this.
limitinteger | nullno
usedinteger | nullno
resetsAtstring | null (date-time)noWhen the billing cycle rolls over and a monthly budget refills. Null for refusals that waiting will not fix.
upgradeobject | nullnoThe cheapest plan that would actually raise this limit, or null when no plan would — worth checking before telling somebody to upgrade into the same wall.

Generated from backend/openapi.yaml by scripts/build-api-reference.py. Do not edit by hand.