DocsPricingBlogChangelog
Examples use: Lease

Every code example, in your word.

Sign inStart building

Errors

Open

Every error SaaS.Dev returns has a code, the line it refers to and the fix. Each code links from the error itself, so you or your agent land on the fix in one click.

Errors come from two places. npx saas.dev and api.saas.dev check your definition files and your account, and their errors link here. Your product's API, CLI, SDK and MCP server return the request codes to your customers in your product's names, linked to your own errors page, such as leaseclock.com/docs/errors.

Every code

StatusCodeWhen
422invalid_yamlThe file does not parse
422unknown_keyA key its place does not take
422missing_keyA key that must be there is not
422invalid_nameA name with the wrong shape
422duplicate_nameTwo things share one name
422unknown_typeA field's type is not one SaaS.Dev has
422unknown_nounA link or a limit names a Noun the product does not have
422unknown_stateA Verb's from or to names a state status does not list
422invalid_valueA key holds a value it cannot take
409stripe_not_connectedA push to Live sells an Offer, and no Stripe account is connected
409version_changedAnother push landed while this one waited
400invalid_jsonThe body is not valid JSON
400idempotency_key_requiredA Verb POST has no Idempotency-Key
401invalid_keyThe key is missing, revoked or mistyped
402limit_reachedThe write would pass a limit of the customer's Offer
403not_allowedThe key's role cannot make this call
404not_foundNo record with that id in the key's workspace
409wrong_stateThe Verb does not run from the record's current state
409idempotency_key_reusedThe Idempotency-Key was used for a different request
413body_too_largeThe body is over 1 MB
422invalid_fieldA field is missing, unknown or the wrong type
422field_set_by_verbA PATCH sends a field that only a Verb changes
422verb_stoppedThe Verb's own function threw
429rate_limitedThe key sent more requests a second than its limit
500server_errorSomething failed inside SaaS.Dev

What an error looks like

Every error is the same JSON object, from api.saas.dev and from your product's API alike:

422 Unprocessable Content
{
  "error": {
    "code": "invalid_name",
    "message": "field \"EndsOn\" must start lower-case.",
    "file": "Noun/Lease.mdx",
    "line": 6,
    "fix": "Rename it to \"endsOn\".",
    "docs": "https://saas.dev/docs/errors#invalid_name"
  }
}
FieldWhat it holds
codeThe code, in snake_case. It never changes, so branch on it
messageWhat is wrong, in one sentence for a person or an agent
fixThe change that clears the error
docsThe code's entry: on saas.dev for errors about your definition and account, on your own docs for errors your customers get
file, lineThe file and line a definition error refers to
fieldThe field a request error refers to

Some codes add what the fix needs, such as limit and offer on limit_reached.

npx saas.dev and your product's own CLI print the same error on one line, with the status, the place, the message and the fix, and exit with status 1 so a CI step fails on it:

Terminal
npx saas.dev push
# 422  Lease.mdx line 6: field "EndsOn" must start lower-case. Rename it to "endsOn".

Your SDK throws it as a typed error with status, code, message, fix and docs. Your MCP server returns it as a tool error whose text holds the message, the fix and the link, so a customer's agent corrects the call and tries again.

Handle errors in code

Branch on code. A message can be reworded to read better, while a code stays fixed. Treat a code your client does not know by its HTTP status.

Retry rate_limited and server_error. Every other code returns the same answer until the request, the record or the plan changes. The SDK and the CLI retry both for you, waiting as long as Retry-After says and resending the same Idempotency-Key, so a retried Verb runs once.

renew.ts
import { Leaseclock, LeaseclockError } from 'leaseclock'

const $ = new Leaseclock({ token: process.env.LEASECLOCK_TOKEN })

try {
  await $.Lease.renew('5a', { endsOn: '2027-09-30' })
} catch (err) {
  if (!(err instanceof LeaseclockError)) throw err

  if (err.code === 'wrong_state') {
    // Hannah Cho's lease on 5A has ended, so she signs a new one.
    await $.Lease.create({ unit: '5a', renter: 'hannah.cho@example.com', endsOn: '2027-09-30' })
  } else {
    console.error(`${err.message} ${err.fix} ${err.docs}`)
    throw err
  }
}

Definition errors

npx saas.dev push checks every file before it saves anything. A file that breaks a rule returns 422 with the file, the line and the fix, and Test stays at its last version. The push reports every broken line in one response, so your agent fixes them all in one pass. Constraints explains why each rule exists.

invalid_yaml

The file does not parse, so nothing else in it can be checked. The usual cause is a brace, bracket or quote left open.

422  Lease.mdx line 7: status is missing its closing "}". Add "}" at the end of the line.

Close it, and push again. The next push checks the rest of the file.

unknown_key

A key that its place does not take. A Noun takes name, fields and verbs, a field takes the keys of its type, and an Offer's price takes amount, currency and unit.

422  Unit.mdx line 3: a Noun takes name, fields and verbs. Rename "properties" to "fields".
422  pro.mdx line 3: price has no key "interval". Use "unit: month".

The fix names the key that does the job. Every surface reads the same keys, so a key outside them would reach one surface and miss the rest.

missing_key

A key that must be there is not. Every field needs a type, every Noun a name, and every Offer a price.

422  Unit.mdx line 4: field "label" has no type. Add one, such as "type: text".

Add the key on the line the error names.

invalid_name

A name with the wrong shape. A Noun's name starts upper-case, and field keys and Verb names are camelCase and start lower-case, because each name becomes a URL, a JSON key, an SDK property and a CLI flag.

422  Lease.mdx line 2: name "lease" must start upper-case. Rename it to "Lease".
422  Lease.mdx line 6: field "EndsOn" must start lower-case. Rename it to "endsOn".

Rename it as the fix says. The App's labels come from the same name, so endsOn still heads its column "Ends on".

duplicate_name

Two Nouns share a name, or one Noun has two fields or Verbs with the same name. Each name is an address on every surface, so it can point at one thing only.

422  Agreement.mdx line 2: name "Lease" is taken by Lease.mdx. Rename one of them.

Rename one, or merge the two files if they describe the same thing.

unknown_type

A field's type is not one SaaS.Dev has. Nouns lists every field type.

422  Unit.mdx line 6: field "bedrooms" has no type "integer". Use "number".

Use the type the fix names. It is the closest one, and it brings its own input, check and JSON schema.

unknown_noun

A link field's target, or an Offer's limits, names a Noun your product does not have. Names are singular and match the Noun's name exactly.

422  Lease.mdx line 4: field "unit" links to "Units", and no Noun has that name. Use "Unit".
422  pro.mdx line 4: limits names "Units", and no Noun has that name. Use "Unit".

Use the Noun's name as its file writes it, or add the Noun if it is missing.

unknown_state

A Verb's from or to names a state that the Noun's status field does not list.

422  Lease.mdx line 12: renew runs from "Active", which is not a status of Lease. Use "active".
422  Lease.mdx line 13: verb "end" moves to "closed", which is not a status. Use "ended", or add "closed" to status.

Use one of the status options, or add the new state to status in the same push.

invalid_value

A key holds a value it cannot take, such as a billing unit Stripe does not sell or a limit that is not a whole number.

422  pro.mdx line 3: unit "monthly" is not a billing unit. Use "month" or "year".

Use a value the fix names.

Push errors

These come from npx saas.dev push when every file is valid and the push still cannot land.

stripe_not_connected

A push to Live holds an Offer, and the product has no Stripe account connected. In Test, checkout runs on a Stripe test account SaaS.Dev opens for you, and Live sells through your own.

Terminal
npx saas.dev push --live
# 409  Leaseclock Pro sells in Live, and leaseclock.com has no Stripe account. Run "npx saas.dev stripe connect".

Connect your Stripe account once, and push again. Stripe walks through it.

version_changed

Another push landed between the change you were shown and your yes, so the change you approved is out of date. It happens when you and your agent, or two agents, push to the same product at once.

409  Test moved to version 4 while this push waited. Push again to see both changes.

Push again. The new preview shows the other push's change beside yours.

Request errors

Your product's API returns these to anyone who calls it, and the CLI, SDK and MCP server pass them on unchanged. api.saas.dev returns the same codes for calls about your own products. A preview, a GET on a Verb's URL, returns the error the POST would, without changing anything.

invalid_json

400. The body is not valid JSON. From a shell, the cause is nearly always quoting.

Wrap the body in single quotes, send Content-Type: application/json, and check the position the message names.

idempotency_key_required

400. A Verb's POST arrived with no Idempotency-Key header. The key is what lets a retry run the Verb once.

The SDK, the CLI and the MCP server send one for you. From curl, add -H "Idempotency-Key: $(uuidgen)", with a new key for each new run and the same key for a retry.

invalid_key

401. The token is missing, revoked or mistyped.

On your product's API, create a key in the app under Settings → API keys, and read it from the environment as $LEASECLOCK_TOKEN. On api.saas.dev, run npx saas.dev login, or set SAAS_DEV_TOKEN in CI to a token from id.org.ai.

limit_reached

402. The write would take a workspace past a limit in its Offer. Leaseclock Pro holds 200 Units, and Northgate Rentals already has 200:

402 Payment Required
{
  "error": {
    "code": "limit_reached",
    "message": "Leaseclock Pro includes 200 Units, and this workspace holds 200.",
    "fix": "Delete a Unit this workspace no longer manages, or move to a plan with more Units.",
    "noun": "Unit",
    "limit": 200,
    "offer": "leaseclock.com/offers/pro",
    "docs": "https://leaseclock.com/docs/errors#limit_reached"
  }
}

Nothing is saved, and an import that would pass the limit is rejected whole. To sell more room, raise limits in the Offer, or add a larger Offer such as Offer/team.mdx, and the upgrade prompt in the app offers it. Offers covers limits.

not_allowed

403. The key's role cannot make the call. A Viewer at Northgate Rentals can read leases, and renewing one needs a Member.

403  A Viewer cannot run renew. Ask a workspace Admin for the Member role.

An Admin changes the role in the app under Settings → Members. Every surface checks the same rule, so a call the app would block is blocked in the API, the CLI and the agent's tools too.

not_found

404. No record has that id in the key's workspace. A record in another customer's workspace answers the same way, so an id reveals nothing about anyone else's records.

List the records to find the id, as in npx leaseclock show leases. A record created in Test lives only in Test, so reach it with a Test key.

wrong_state

409. The Verb does not run from the record's current state. Hannah Cho's lease on 5A ended on September 30, 2026, and renew runs only from: [active]:

409 Conflict
{
  "error": {
    "code": "wrong_state",
    "message": "Lease 5a is ended, and renew runs only on an active lease.",
    "fix": "Renew an active lease, or create a new lease for Unit 5A.",
    "docs": "https://leaseclock.com/docs/errors#wrong_state"
  }
}

The App shows the same reason beside a disabled Renew button. Run a Verb the state allows, or create a new record.

idempotency_key_reused

409. The Idempotency-Key was already used for a different request. A key stands for one request, so a retry with it returns the first response, and a new request under it is stopped before it runs.

Send a new key for each new request.

body_too_large

413. The body is over 1 MB.

Send the records in smaller batches. Each batch is checked and saved on its own.

invalid_field

422. A field is missing, unknown to the Noun, or the wrong type. The error names the field, and for an unknown one, the closest field the Noun has:

422 Unprocessable Content
{
  "error": {
    "code": "invalid_field",
    "field": "endsOn",
    "message": "endsOn must be a date, and \"next year\" is not one.",
    "fix": "Send a date such as 2027-11-30.",
    "docs": "https://leaseclock.com/docs/errors#invalid_field"
  }
}

Send the field as its type reads in JSON: a date as 2027-11-30, a link as the linked record's id, a choice as its option's key. The API lists each type's form.

field_set_by_verb

422. A PATCH sends a field that only a Verb changes. Lease's status moves only through end, so a PATCH with "status": "ended" stops here, and the fix names POST /api/leases/2a/end.

Run the Verb. It keeps the change in the record's history, with an undo.

verb_stopped

422. The Verb's own function threw. The message is the text your function threw, word for word:

422  endsOn must be later

The person or agent changes the input and runs it again. To change the words they read, change the throw in the Verb's fn. Verbs shows a function that throws.

rate_limited

429. The key sent more requests a second than its limit: 100 in Live and 25 in Test. Retry-After gives the seconds to wait, and every response carries RateLimit-Remaining, so a script slows down before it is stopped.

Wait and retry. The SDK and the CLI do both for you.

server_error

500. Something failed inside SaaS.Dev. Every write either completes or leaves the record as it was, so nothing half-saves. Your customers read Leaseclock's name in the message, and nothing in it names SaaS.Dev.

Retry with the same Idempotency-Key, and the request runs once.

Your customers' errors page

Every product gets its own errors page with every request code, in its own names and on its own domain. Leaseclock's is leaseclock.com/docs/errors. When Priya Shah's import script at Northgate Rentals passes 200 Units, the error links her to Leaseclock's entry for limit_reached, and the fix there names Leaseclock Pro and its pricing page. Nothing on it names SaaS.Dev.