Core concepts

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.

404 Not Found
{
  "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"
}
Always present
statusintegerrequired

The HTTP status, repeated in the body so a logged payload is self-contained.

titlestringrequired

The status phrase, for example Not Found. Fixed per status — do not show it to end users on its own.

detailstringrequired

What 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.

typestringrequired

A link to the status code's definition. It identifies the status, not the specific error — do not branch on it.

instancestringrequired

The path that failed, for example /company-app/leases/1830472847398404096.

traceIdstringrequired

Identifies 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.

400 Bad Request
{
  "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.

StatusMeansWhat to do
400The request was malformed — a bad id format, a parameter out of range, a missing required fieldFix the request. Retrying unchanged will fail again
401The access token is missing, malformed, or expiredExchange your API key for a fresh token and retry once. See Authentication
403You are authenticated but lack the permission this endpoint requiresHave the key reissued with the permission, or ask an administrator to grant it to the key's owner
404No such record — or it exists in another companyDo not retry. entityType and entityId say what was looked for
409The record's current state conflicts with what you asked forRe-read the record and decide. Each endpoint page lists the conflicts it can raise
410The thing you are acting on has expired and will not come backDo not retry. Create a new one — a fresh info-update request, for example
422Well-formed, but not allowed right now — a business rule refused itdetail says which rule. Do not retry without changing something
429Rate limitedWait retry_after seconds, then retry. See rate limits
500Something broke on our sideSafe to retry once, with backoff. If it persists, send us the traceId
404 also means "not yours"

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.

410 from a rental application endpoint
{
  "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.

Everywhere else there is no errorCode

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);
  }
}
Retrying a write can duplicate it

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.