Skip to content

Errors

Every non-2xx response from /api/v1 returns a uniform JSON envelope so client error handling can be written once.

Envelope

{
"error": {
"code": "validation_error",
"message": "Invalid watch payload.",
"details": {
"formErrors": [],
"fieldErrors": {
"scanCadence": [
"Invalid enum value. Expected 'hourly' | 'daily' | 'weekly'."
]
}
}
}
}
FieldTypeNotes
error.codestringMachine-readable identifier. The full list is below. New codes may be added; clients must treat unknown codes as the generic internal failure.
error.messagestringHuman-readable description. May change between releases — do not match on this.
error.detailsunknown | undefinedOptional, code-specific structured detail. For validation_error: zod’s flatten() shape ({ formErrors, fieldErrors }). Otherwise treat as opaque.

There is no top-level data key on error responses, and there is no error key on success responses. Branch on the HTTP status code (or, equivalently, on "error" in body).

Standard codes

CodeHTTPWhen
unauthenticated401Missing Authorization header, malformed Bearer token, unknown key, or revoked key.
forbidden402Tier-quota breach: maxWatches reached, scanCadence faster than tier minimum. Body includes error.upgradeUrl: "/pricing". (Status is 402 Payment Required, not 403, because the fix is “upgrade”, not “fix the request”.)
forbidden403Tier below Team (no apiAccess), or required scope missing on the key.
not_found404The path does not match a route, or the resource does not exist / is not visible to the calling key. We deliberately do not distinguish “you cannot see it” from “it does not exist” to avoid leaking ID space.
validation_error400Malformed JSON. Distinct from 422 to mirror Express’s body parser.
validation_error422Body or query parameters parsed but failed schema validation. details carries the zod flatten() shape ({ formErrors, fieldErrors }).
method_not_allowed405The endpoint does not accept the HTTP method used. Includes Allow response header.
rate_limited429Per-key quota exceeded. See rate limits for the response headers.
internal500Unhandled server error. Retryable (idempotently). The dashboard’s status page is the canonical place to check for incidents.

validation_errordetails shape

{
"error": {
"code": "validation_error",
"message": "Invalid watch payload.",
"details": {
"formErrors": [],
"fieldErrors": {
"scanCadence": [
"Invalid enum value. Expected 'hourly' | 'daily' | 'weekly'."
]
}
}
}
}

details is the result of zod’s flatten()formErrors carries top-level (cross-field) issues, fieldErrors is a map from dotted path to a list of human-readable error messages. Consume fieldErrors for per-field UI; consume formErrors for the “everything else” bucket.

forbidden — distinguishing causes

forbidden covers two distinct causes — tier-quota (402) and tier/scope (403) — but the error.code is the same string in both. Branch on the HTTP status code to tell them apart, and surface error.message to the operator. Tier-quota responses carry an additional error.upgradeUrl field (always /pricing today).

  • 402solo plan is capped at 25 active watches; upgrade to add more.
  • 402solo plan does not allow hourly scans (min cadence: daily).
  • 403free plan does not include REST API access. Upgrade to Team to use /api/v1.
  • 403This API key does not have the required scope: watches:write.

Examples

Missing Bearer token — 401

Terminal window
curl -sS https://api.trademarksentinel.app/api/v1/watches
{
"error": {
"code": "unauthenticated",
"message": "Missing or invalid API key."
}
}

Insufficient scope — 403

Terminal window
curl -sS -X POST https://api.trademarksentinel.app/api/v1/watches \
-H "Authorization: Bearer ts_KEY_WITH_ONLY_WATCHES_READ" \
-H "Content-Type: application/json" \
-d '{ "watchType": "trademark", "name": "Acme", "attributes": {} }'
{
"error": {
"code": "forbidden",
"message": "This API key does not have the required scope: watches:write."
}
}

Validation failed — 422

Terminal window
curl -sS -X POST https://api.trademarksentinel.app/api/v1/watches \
-H "Authorization: Bearer ts_REPLACE_ME" \
-H "Content-Type: application/json" \
-d '{ "watchType": "trademark", "name": "Acme", "attributes": {}, "scanCadence": "minutely" }'
{
"error": {
"code": "validation_error",
"message": "Invalid watch payload.",
"details": {
"formErrors": [],
"fieldErrors": {
"attributes.niceClasses": ["Required"],
"attributes.designations": ["Required"],
"scanCadence": [
"Invalid enum value. Expected 'hourly' | 'daily' | 'weekly'."
]
}
}
}
}