Errors
Most services return the same error shape — RFC 7807 problem details, with extra members that tell you which record or which field was at fault. A few areas add an errorCode you can branch on.
A failed request returns Content-Type: application/problem+json and a body in the shape defined by RFC 7807. Most of the API produces it from one shared handler, so the fields below are the ones you can rely on.
{
"type": "https://httpstatuses.com/404",
"title": "Not Found",
"status": 404,
"detail": "Lease not found.",
"instance": "/company-app/leases/1830472847398404096",
"traceId": "0HN7GK9C2M4PQ",
"entityType": "Lease",
"entityId": "1830472847398404096"
}statusintegerrequiredThe HTTP status, repeated in the body so a logged payload is self-contained.
titlestringrequiredThe status phrase, for example Not Found. Fixed per status — do not show it to end users on its own.
detailstringrequiredWhat actually went wrong, in a sentence. This is the useful one for a log line or a support ticket — but it is terse on not-found failures ("Lease not found."), which carry the id in entityId rather than in the sentence.
typestringrequiredA link to the status code's definition. It identifies the status, not the specific error — do not branch on it.
instancestringrequiredThe path that failed, for example /company-app/leases/1830472847398404096.
traceIdstringrequiredIdentifies this exact request in MagicDoor's logs. Log it. Quoting it in a support request is the difference between a fast answer and a slow one.
Some failures add members beyond the standard six. Which ones appear depends on why the request failed.
A map of field name to message. Present on 400 and on validation-driven 422s.
{
"type": "https://httpstatuses.com/400",
"title": "Bad Request",
"status": 400,
"detail": "Page size must be between 1 and 100",
"instance": "/company-app/properties",
"traceId": "0HN7GK9C2M4PQ",
"errors": {
"pageSize": "Page size must be between 1 and 100"
}
}Present on 404 and 409, naming the record that was missing or in conflict. entityId is a string, like every id in this API.
entityType is derived from an internal type name, so it is usually the entity (Lease, Property) but occasionally carries a Dto suffix. Treat it as a diagnostic hint rather than a stable identifier to branch on.
| Status | Means | What to do |
|---|---|---|
400 | The request was malformed — a bad id format, a parameter out of range, a missing required field | Fix the request. Retrying unchanged will fail again |
401 | The access token is missing, malformed, or expired | Exchange your API key for a fresh token and retry once. See Authentication |
403 | You are authenticated but lack the permission this endpoint requires | Have the key reissued with the permission, or ask an administrator to grant it to the key's owner |
404 | No such record — or it exists in another company | Do not retry. entityType and entityId say what was looked for |
409 | The record's current state conflicts with what you asked for | Re-read the record and decide. Each endpoint page lists the conflicts it can raise |
410 | The thing you are acting on has expired and will not come back | Do not retry. Create a new one — a fresh info-update request, for example |
422 | Well-formed, but not allowed right now — a business rule refused it | detail says which rule. Do not retry without changing something |
429 | Rate limited | Wait retry_after seconds, then retry. See rate limits |
500 | Something broke on our side | Safe to retry once, with backoff. If it persists, send us the traceId |
Every request is scoped to the company the API key belongs to. Asking for a record that belongs to another company returns 404, not 403 — the API does not confirm that an id exists somewhere else. So a 404 on an id you are sure about usually means the key is for a different company than you thought.
Some areas build their own problem body instead of going through the shared handler, and add an errorCode — a stable string that identifies the specific failure rather than just its status. Where you see one documented on an endpoint page, branch on it: it is more precise than status and, unlike detail, it is safe to compare against.
Rental applications and listing syndication do this, as do several payment-account endpoints. Their bodies also vary from the standard in small ways — a domain-specific title, sometimes no instance, and a type that is not the usual httpstatuses.com link. status and traceId behave as they do everywhere.
{
"title": "Info Update Request Expired",
"status": 410,
"detail": "This info update request has expired. Create a new one.",
"errorCode": "info_update_request_expired",
"traceId": "0HN7GK9C2M4PQ"
}Rental applications add retryAfterSeconds, provider, reason, path and parentPath on the failures where those help, and syndication adds field, reason, fixTarget and entityLevel. Each endpoint page lists the codes it can return.
If an endpoint page does not document one, the response will not carry one — branch on status and read detail for the explanation.
The status is what you branch on. detail is for humans, and its wording can change — never parse it.
async function call(path: string, token: string) {
const response = await fetch(`https://api.portal.magicdoor.com${path}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) return response.json();
const problem = await response.json();
// Log traceId on every failure — it is what support needs to find the request.
console.error(
`${problem.status} ${problem.title} on ${problem.instance}: ` +
`${problem.detail} (traceId ${problem.traceId})`,
);
switch (problem.status) {
case 401:
throw new TokenExpired(); // exchange the API key again, retry once
case 429:
throw new RateLimited(problem.retry_after);
case 400:
case 422:
throw new BadRequest(problem.errors ?? problem.detail);
default:
throw new Error(problem.detail);
}
}This API has no idempotency keys, so a retried POST creates a second record. Only retry a write when the request never completed — a connection error or a timeout with no response. A 500 that came back with a body means the server handled the request and failed partway, so re-read before you retry.