Appearance
Errors
Every error the External API v2 returns, from a malformed UUID to an internal failure, uses a single format: an RFC 9457 application/problem+json body with a stable, machine-readable code. There are no bare HTML error pages, no ad-hoc JSON shapes, and no format differences between authentication, validation, and business errors.
This page covers the envelope, the complete code catalog, field-level validation errors, and recommended handling per code. For the errors specific to posting instructions, see Instructions & instructability; for authentication setup, see Authentication.
Scope
This format applies to every route under /api/v2/external/.... Legacy v1 routes keep their historical error formats, which is one more reason to migrate.
The envelope
Every 4xx and 5xx response has Content-Type: application/problem+json and this shape:
| Member | Type | Presence | Description |
|---|---|---|---|
type | string (URI reference) | always | Stable URI reference identifying the problem type, e.g. /errors/not-found. Never a class name. |
title | string | always | Short human-readable summary of the problem type. Stable per type, but display-only: do not switch on it. |
status | integer | always | The HTTP status code of this occurrence. Matches the response status line. |
detail | string | optional | Human-readable, occurrence-specific explanation. Free-form and localizable; never parse it. |
instance | string (URI reference) | optional | Identifies this specific occurrence, typically the request path. |
code | string | always | Stable machine-readable error code: the member to switch on in your integration. Full catalog below. |
requestId | string, nullable | always | Correlation id, mirrors the X-Request-Id response header. Quote it when contacting support. |
errors | array | optional | Field-level validation failures. Present on validation_failed; see Validation errors. |
Some problem types add extension members at the top level of the body (RFC 9457 extensions): for example requiredScope on insufficient_scope, or clientCaseReference and missingPreviousRenewalDates on instruction_skipped. The catalog below lists every extension. Per RFC 9457, ignore any member you do not recognize: new extensions may be added without notice.
A complete example: a POST /patent-instructions rejected because an earlier renewal event of the same patent has no instruction yet.
json
{
"type": "/errors/instruction-skipped",
"title": "Instruction skipped",
"status": 422,
"detail": "An earlier renewal event of this patent is still awaiting an instruction. Instruct the earliest un-instructed event first.",
"instance": "/api/v2/external/patent-instructions",
"code": "instruction_skipped",
"requestId": "7f3c2a9e-4b1d-4c6a-9e2f-8a5d0c3b1e47",
"clientCaseReference": "ACME-EP-0042",
"missingPreviousRenewalDates": ["2026-03-31"]
}Here the extensions tell you which case is affected (clientCaseReference, your own docketing reference) and the due dates of the earlier events still awaiting a decision (missingPreviousRenewalDates). Why this rule exists, and how to avoid tripping it, is explained in Instructions & instructability.
Switch on code rather than on detail or title
code and type are contractual and stable. detail and title are human-readable text that may be reworded or localized at any time.
Error catalog
code | HTTP | type | When it happens | Extensions |
|---|---|---|---|---|
validation_failed | 400 | /errors/validation-failed | A request body, query parameter, or path parameter failed validation: bad UUID or YYYY-MM path param, unknown sort field on GET /patents, missing due-date window on GET /patent-events, malformed instruction body… | errors[]; see Validation errors |
invalid_api_key | 401 | /errors/invalid-api-key | The x-api-key header is missing or the key is unknown. See Authentication. | None |
api_key_expired | 401 | /errors/api-key-expired | The key is recognized but past its expiresAt. Distinct from invalid_api_key so you can alert on rotation specifically. | None |
insufficient_scope | 403 | /errors/insufficient-scope | The key is valid but does not carry the scope this endpoint requires (e.g. calling POST /patent-instructions with a read-only key). | requiredScope: the missing scope, e.g. "patents:instructions:write" |
forbidden | 403 | /errors/forbidden | Business-level denial unrelated to scopes, e.g. calling the exchange-rate-freeze endpoints when the FX-freeze capability is not enabled for your account. See Fees & FX. | None |
not_found | 404 | /errors/not-found | The resource does not exist in your portfolio: an unknown id and someone else's id are indistinguishable by design. Also returned by document routes while the document does not exist yet. | None |
instruction_already_set | 409 | /errors/instruction-already-set | The event already has an executed decision. Instructions are executed immediately and are not modifiable via the API, so re-posting conflicts with the existing state. | None |
instruction_too_early | 422 | /errors/instruction-too-early | The instruction window has not opened yet (today is before the event's opensAt, the first day of the month two months before the due month). | None |
instruction_skipped | 422 | /errors/instruction-skipped | An earlier un-instructed event of the same patent must be instructed first: only the chronologically earliest un-instructed event of a patent can be instructed. | clientCaseReference, missingPreviousRenewalDates |
instruction_lapsed | 422 | /errors/instruction-lapsed | The requested decision is not offered for this event: typically PROCEED on a lapsed event, whose grace period is over and whose office no longer accepts payment. Check instructability.allowedDecisions. | None |
rate_limit_exceeded | 429 | /errors/rate-limit-exceeded | Your key's daily call quota is exhausted. The Retry-After header gives the seconds until the quota resets at midnight (server local time). | None |
internal_error | 500 | /errors/internal-error | Unexpected server-side failure. The detail is deliberately generic; the incident is logged server-side under your requestId. | None |
Instruction batches are atomic
A POST /patent-instructions call is all-or-nothing: if any entry fails with a 409 or 422, no instruction in the batch is executed. Fix or remove the offending entry and re-post. The reliable way to avoid these errors altogether is to read each event's instructability object first, because it is computed by the same rule the POST enforces. See Instructions & instructability.
401/403 semantics
invalid_api_key and api_key_expired are about the key itself; insufficient_scope is about what the key may do; forbidden is about what your account may do. Only the first two are fixable in your request configuration. The last two require a change on the Renewr side. See Authentication.
Validation errors
When code is validation_failed, the errors array pinpoints each failing field:
| Member | Type | Description |
|---|---|---|
pointer | string | RFC 6901 JSON Pointer to the offending field in the request body, e.g. /instructions/0/eventId. ~ and / in key names are escaped as ~0 and ~1. |
code | string | Stable machine-readable code for this specific field failure. |
message | string | Human-readable, localizable description. Display it, do not parse it. |
Example of a bad instruction body:
bash
curl -X POST "https://api.renewr.example/api/v2/external/patent-instructions" \
-H "x-api-key: $RENEWR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"instructions": [
{ "eventId": "not-a-uuid", "decision": "PROCEED" },
{ "eventId": "3f8a1c2e-9b4d-4e7a-8c1f-2d5e6a7b8c9d", "decision": "RENEW" }
]
}'json
{
"type": "/errors/validation-failed",
"title": "Validation failed",
"status": 400,
"detail": "The request failed validation.",
"instance": "/api/v2/external/patent-instructions",
"code": "validation_failed",
"requestId": "b2d94c6a-1e3f-4a8b-9c7d-5e2f1a0b3c4d",
"errors": [
{
"pointer": "/instructions/0/eventId",
"code": "invalid_string",
"message": "Invalid uuid"
},
{
"pointer": "/instructions/1/decision",
"code": "invalid_enum_value",
"message": "Invalid enum value. Expected 'PROCEED' | 'SKIP_THIS_TIME' | 'DROP', received 'RENEW'"
}
]
}The same envelope covers invalid query and path parameters: a non-UUID patentId on GET /patents/{patentId}, a malformed yearMonth on PUT /exchange-rate-freezes/{yearMonth}, or an incomplete due-date window on GET /patent-events all return a standard validation_failed 400.
Not everything invalid is an error
Some inputs are deliberately forgiving instead of failing: an unknown include value is silently ignored, and itemsPerPage above 100 is silently clamped to 100. See Conventions for the full list of lenient behaviors.
Handling guidance
Three failure classes: retry it, fix it, or read it as a state signal.
code | Class | What your integration should do |
|---|---|---|
rate_limit_exceeded (429) | Retry | Wait the Retry-After seconds, then resume. Do not hammer: the quota is daily. Spread batch work to stay under it; see Syncing data. |
internal_error (500) | Retry | Retry with exponential backoff (e.g. 1 s, 2 s, 4 s, a few attempts). If it persists, stop and contact support with the requestId. |
validation_failed (400) | Fix | A bug in your request construction. Log the errors[] pointers, fix the code, do not retry unchanged. |
invalid_api_key (401) | Fix | Check the x-api-key header and key value. See Authentication. |
api_key_expired (401) | Fix | The key needs rotation. Alert your operations channel; no request change will help. |
insufficient_scope (403) | Fix | The key lacks requiredScope. Request a key with the right scopes. |
forbidden (403) | Fix | Capability not enabled for your account (e.g. FX freeze). Contact Renewr, do not retry. |
instruction_too_early, instruction_skipped, instruction_lapsed (422) | Fix | A business-rule violation. Consult the event's instructability (opensAt, blockedReason, allowedDecisions) and adjust; see Instructions & instructability. |
instruction_already_set (409) | State signal | The decision is already recorded, which is usually a success from your workflow's point of view. Common on replays after a timeout: treat it as "already done", verify via GET .../events/{eventId} (appliedDecision), and continue. |
not_found (404) | State signal | The resource is not in your portfolio. If you expected it, check the id and whether the patent was imported; see Syncing data. Never retry in a loop. |
429 responses omit the rate-limit headers
Successful authenticated responses carry RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Use them to throttle proactively. The 429 itself does not carry them; it carries Retry-After instead. Details in Conventions.
Correlating with support: requestId
Every response, success or error, carries an X-Request-Id header, and every problem body mirrors it in requestId. Server-side, 5xx incidents are logged under that same id.
- Log it with every failed call in your integration.
- Quote it in any support request: it lets Renewr find the exact server-side trace of your call.
- Optionally set it yourself: if you send a non-empty
X-Request-Idrequest header, it is echoed back instead of a server-generated one, letting you propagate your own correlation ids end-to-end.
bash
curl -i "https://api.renewr.example/api/v2/external/patents/3f8a1c2e-9b4d-4e7a-8c1f-2d5e6a7b8c9d" \
-H "x-api-key: $RENEWR_API_KEY" \
-H "X-Request-Id: my-sync-run-2026-07-22-0042"Next: the routine errors you will actually meet in production show up while polling; see Syncing data and the monthly renewal cycle for where each one fits in a real integration.