Reference notes
The conventions that apply everywhere, in one place. The machine readable
contract is at
/developers/spec/openapi.json
(OpenAPI 3.1): generate a client from it, import it into your tooling, or read it
alongside these pages.
Base URLs
| Environment | Base URL |
|---|---|
| Production | https://api.appraisalhost.com/v1 |
| Sandbox | https://sandbox.api.appraisalhost.com/v1 |
HTTPS only, TLS 1.2 or better. The version is the first path segment.
Identifiers
Identifiers are opaque prefixed strings of up to 64 characters. Store them as strings, compare them exactly, and do not parse them.
| Prefix | Resource |
|---|---|
ord_ | An order |
doc_ | A document |
msg_ | A message, or a reply on a revision request |
rr_ | A revision request |
evt_ | An event |
cli_ | An API client, which is your client id |
len_ | A lender account, from GET /account |
cmp_ | An appraisal management company, from GET /account |
order_number is different: it is the appraisal management company's own order
number, the one a person quotes on the phone. It is unique within that company,
not across the platform, so key your records on id.
Event ids are lexicographically sortable: compare them as strings, and they come out in the order the events were raised. That is what makes the event cursor reliable. Do not decode one, and do not assume any other id sorts or carries meaning.
Dates, times and money
- Timestamps are ISO-8601 with an explicit UTC offset:
2026-09-17T14:32:05Z. We always send UTC. Send UTC. - A date without a time is
YYYY-MM-DD, and means a calendar date in the property's own local time. Due dates and appointment dates are of this kind. - Money is a decimal string with two places, plus a separate three letter
currency code:
"appraisal_fee": "525.00","currency": "USD". We never use a floating point number for money, and neither should your parser. Every money field matches^-?\d+\.\d{2}$. - Phone numbers are text. Include the country code, as in
+1-217-555-0142or+15085550142, and read back what you sent. - On a contact,
typeisphoneoremailandrolesays whose it is. On an email address usehome,workorother. - Booleans are
trueandfalse, never"Y","1"or"yes". - A field that applies to a resource is sent even when it has no value, as
null. Code defensively all the same: treat a missing key asnullrather than as an error, and ignore fields you do not know. New fields may appear at any time.
Pagination
Every list endpoint except the event feed uses page numbers:
{
"data": [],
"page": 1,
"per_page": 25,
"total": 138,
"total_pages": 6
}
pagestarts at 1.per_pageis 1 to 100, and defaults to 25.- A page past the end returns an empty
dataarray, not a404. - The event feed uses a cursor instead: pass
next_cursorback asafteruntil it comes back null. See Webhooks.
The order results come back in
Every list is ordered on a stable key:
| Endpoint | Ordered by |
|---|---|
GET /orders | created_at descending, with id as the tie break |
GET /orders/{order_id}/documents | created_at descending, with id as the tie break |
GET /orders/{order_id}/revision-requests | created_at descending, with id as the tie break |
GET /orders/{order_id}/messages | created_at ascending |
GET /events | created_at ascending, which is also event id order |
GET /order-types | code ascending |
Page numbers walk a collection that can change while you are reading it, so an
order touched between your first page and your last can move. When you page with
updated_since to reconcile, walk every page quickly, then run the same window
again and compare the two runs: when they agree, nothing moved underneath you.
Never assume a row cannot move between pages.
Errors
Every error, on every endpoint, has the same body:
{
"code": "validation_failed",
"message": "The request could not be processed. See details.",
"details": [
{ "field": "property.postal_code", "code": "required", "value": null, "message": "A postal code is required." },
{ "field": "loan.purpose", "code": "invalid_value", "value": "cash_out", "message": "Must be one of: construction_only, construction_to_permanent, mortgage_modification, other, purchase, refinance." }
],
"correlation_id": "req_01J9Z40M8P1R4T7W0Y3B6D9F"
}
codeis stable. Branch on it.messageis plain text for a person, and may be reworded at any time. Never match on it.detailsnames each field at fault, with a dotted path such asborrowers.0.last_name.details[].codeis stable, anddetails[].valuecarries the machine readable value at fault when there is one. Both are safe to branch on.current_statusis a top level field on every409that refuses an action because of where the order has got to. It holds the order's current status code, so you can branch on it without walkingdetails.correlation_ididentifies the request in our logs. Log it, and quote it when you contact us.
Status codes
| Status | Meaning | What to do |
|---|---|---|
400 | The request could not be read: malformed JSON, a missing required field, a value of the wrong type. | Fix the request. Do not retry unchanged. |
401 | No token, an invalid token, an expired token, or a revoked client. | Refresh the token and retry once. Stop and alert on invalid_client or client_revoked. |
403 | The credential is valid but not allowed to do this: the action is switched off for this client, or the lender account is not one the company invoices (account_not_billable). | Do not retry. Ask the appraisal management company. |
404 | No such resource, or one this credential is not entitled to see. | Do not retry. We answer 404 rather than 403 for another account's resource so that ids cannot be probed. |
409 | A conflict with current state: an action the status does not allow, an amendment to a closed order, a reused idempotency key. | Read the order and decide. Do not retry blindly. |
413 | The upload is larger than 25 MB. | Send a smaller file. |
415 | Unsupported Content-Type, or a file type we do not accept. | Fix the request. |
422 | Well formed, but the values cannot be used: a product this lender may not order, a loan type that does not apply, a due date in the past, a missing acknowledgement. | Fix the values. details says which. |
429 | Over the rate limit. | Wait for Retry-After, then retry with backoff. |
500 | A failure on our side. | Retry a read. Retry a create with the same Idempotency-Key. For any other write, read the order first: it may have been applied. Quote the correlation_id if it continues. |
503 | The API is not serving requests for a moment, for example during planned maintenance. | Wait for Retry-After, then retry. Nothing you sent was applied. |
Common error codes
validation_failed, semantic_validation_failed, invalid_client,
client_revoked, token_expired, not_found,
action_not_allowed_in_status, action_not_permitted_for_client,
account_not_billable, recipient_not_allowed, idempotency_key_reused,
payload_too_large, unsupported_media_type, rate_limited,
webhook_not_configured, service_unavailable, internal_error.
account_not_billable is the one to read before you build: API ordering
requires a lender account the appraisal management company invoices, and a
create on an account that pays per order by card is refused with 403 and that
code.
Rate limits
600 requests per minute per client, across all endpoints. Every response carries
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and a 429
carries Retry-After in seconds. See Authentication.
Idempotency and retries
POST /ordersacceptsIdempotency-Key, remembered for 24 hours. Always send one. The key is scoped to your credential and to that endpoint, and a replay returns the original order with the original201.- Every
GETis safe to retry.PUT /webhookis idempotent by nature. POSTactions (hold,resume,cancel) are safe to retry: an action the order has already taken returns409rather than doing it twice.- A message, an upload and a revision request take no key. If one of those times out, read the thread, the document list or the revision request list before you send it again, or you will create a second one.
- Retry
429,500and503with exponential backoff and jitter. Do not retry400,403,404,409,413,415or422without changing the request.
Versioning and deprecation
- The major version is in the path. Today that is
v1. - Within
v1we make additive changes only: new endpoints, new optional request fields, new response fields, new enum values, new event types. Your integration must tolerate all five. In particular, treat an unknown status code, document kind or event type as something to store and ignore, not as an error. - We will never, within
v1, remove a field, rename one, change a field's type, change the meaning of an existing enum value, or make an optional request field required. - A breaking change means a new major version at a new path, and 12 months of
notice before
v1is retired. We publish the notice on the Changelog and write to the technical contact on every active credential. api_versionon every webhook payload tells you which version of the contract that payload was built to.- The contract file carries the same version in
info.version. Diff it between releases to see exactly what changed.
Field naming and enums
Field names are snake_case. Enum values are lower case with underscores. The
enums you will meet most:
- Loan purpose:
construction_only,construction_to_permanent,mortgage_modification,other,purchase,refinance. Sendotherwithloan.purpose_other. - Property type:
church,commercial_non_residential,condominium,condominium_over_four_stories,cooperative,farm,home_and_business_combined,manufactured_mobile_home,mixed_use_residential,multifamily_more_than_four_units,other,single_family,townhouse,two_to_four_unit_property,vacant_land. - Occupancy:
investment_property,primary_residence,second_home,vacant. - Contact type and role:
phoneoremail, andhome,cell,workorother. - Note type:
orderorappointment. - Additional contact purpose:
reportorstatus. - Report format:
uad_2_6oruad_3_6. - Appraiser certification on a product:
none,fha_required,fha_optionalorusda. - Document purpose on an upload:
engagement_letter,sales_contract,prior_reportorother. - Order status codes and document kinds: see Order lifecycle.
Order type codes, loan type codes and add-on codes are the appraisal management
company's own, and come from GET /order-types. They are stable for that
company. Do not write them into your source, and do not assume one company's
codes appear at another.
Data we do not accept
- Card numbers or any payment instrument. Orders are billed to the lender account the credential belongs to.
- Credentials in a URL or a query string.
- Real borrower data in the sandbox.
Downloading the contract
- JSON:
https://www.appraisalhost.com/developers/spec/openapi.json - The version of the contract is
info.version, and the current value is1.0.0.
Generate your client from the contract rather than hand writing models. The contract is the interface we hold ourselves to: where behavior and contract disagree, the contract is the defect report, and we fix one or the other and record it in the Changelog.