Authentication
The API uses OAuth 2.0 client credentials. You exchange a client id and a client secret for a short lived bearer token, then send that token on every call.
Environments
| Environment | Token endpoint |
|---|---|
| Production | POST https://api.appraisalhost.com/v1/oauth/token |
| Sandbox | POST https://sandbox.api.appraisalhost.com/v1/oauth/token |
Credentials are per environment. A sandbox credential never works in production, and the reverse.
How credentials are issued
The appraisal management company issues credentials to one of its lender accounts from its own screens, and can revoke them at any time. A credential therefore identifies both the lender and the company, which is why one address serves everyone: you do not tell us where the order belongs, the credential does.
- The client id starts with
cli_and is safe to log. - The client secret is shown once, at creation, and is never displayed again. Store it in a secret manager. If it is lost, the company issues a new one and revokes the old.
- A credential carries the
lenderscope in version 1. It can do everything on this reference for that lender account, and nothing outside it. - Ask the appraisal management company for production credentials. Ask our integrations team for sandbox credentials.
If you serve several lenders, you hold one credential per lender account and keep them separate. Never share one credential across lenders: it would place their orders on the wrong account.
Getting a token
POST /oauth/token is an OAuth 2.0 client credentials token endpoint. It accepts
the standard form encoding and, as an alternative, JSON. A client generated from
our contract and a client you write yourself both work, unchanged.
Form encoded, with the credential in the body:
curl -s -X POST https://api.appraisalhost.com/v1/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials' \
-d 'client_id=cli_example_2f8c41' \
-d 'client_secret=example_secret_do_not_use_9f4b1c77ae'
Form encoded, with the credential in an HTTP Basic header, where the client id is the user name and the client secret is the password:
curl -s -X POST https://api.appraisalhost.com/v1/oauth/token \
-u 'cli_example_2f8c41:example_secret_do_not_use_9f4b1c77ae' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials'
Or as JSON:
curl -s -X POST https://api.appraisalhost.com/v1/oauth/token \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "client_credentials",
"client_id": "cli_example_2f8c41",
"client_secret": "example_secret_do_not_use_9f4b1c77ae"
}'
The answer is the same either way:
{
"access_token": "aht_at_7Qk2rX9wTm4ZbN1sV6yH0pL8",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "lender"
}
Errors at this endpoint, and only at this endpoint, follow RFC 6749 rather than the error body used everywhere else, so that a standard OAuth 2.0 client can classify them:
{
"error": "invalid_client",
"error_description": "The client id or client secret is not valid.",
"correlation_id": "req_01J9Z2A7K3M5P8R1T4W6Y9B2D"
}
error is one of invalid_request, invalid_client, invalid_grant,
unauthorized_client, unsupported_grant_type or invalid_scope. Every other
endpoint answers with the one error body described on the
Reference notes page.
The examples on these pages use obviously fake credentials. They will not authenticate anywhere.
Send the token on every other call:
curl -s 'https://api.appraisalhost.com/v1/orders?per_page=5' \
-H 'Authorization: Bearer aht_at_7Qk2rX9wTm4ZbN1sV6yH0pL8'
What a credential is for
GET /account answers the question a configuration file cannot: which lender
account, and which appraisal management company, is this credential actually
for.
curl -s https://api.appraisalhost.com/v1/account \
-H 'Authorization: Bearer aht_at_7Qk2rX9wTm4ZbN1sV6yH0pL8'
{
"client_id": "cli_example_2f8c41",
"environment": "production",
"lender": { "id": "len_4WQ2H8ZKPB31", "name": "Example Mortgage Group" },
"company": { "id": "cmp_7YT5M2XRND94", "name": "Example Appraisal Management" },
"scopes": ["lender"],
"rate_limit": { "requests_per_minute": 600 }
}
Check it at start-up
Do this once per credential, every time your service starts, before it places anything:
- Fetch a token with the credential.
- Call
GET /account. - Check
environmentis the environment this deployment is for. - Check
lender.idandlender.nameagainst your own configuration for that lender. If they do not match, refuse to place orders with that credential and raise an alert. - Log
client_idwithlender.id, so that a credential mapped to the wrong lender is visible in your logs rather than in somebody's order.
A credential placed against the wrong lender puts that lender's borrower on another lender's account. This check is what stops it.
Token lifetime and refresh
- A token is valid for 60 minutes.
expires_inis the authority, in seconds from the response. - Tokens are opaque. Do not parse one, and do not depend on its length or alphabet.
- Cache the token and reuse it until shortly before it expires. Asking for a token before every call wastes your rate limit.
- Requesting a new token does not invalidate the old one, so a rolling refresh is safe: fetch a new token at about 55 minutes and swap it in.
- There is no refresh token. Client credentials are the refresh.
Handle expiry defensively. If a call returns 401 with code token_expired,
fetch a new token and retry the call once. If the retry also returns 401 with
code invalid_client or client_revoked, stop and alert an operator: the
credential has been revoked or replaced.
{
"code": "token_expired",
"message": "The access token has expired.",
"correlation_id": "req_01J9Z41P0R3T6W9Y2B5D8F1H"
}
Rotation and revocation
- Rotation. The appraisal management company creates a second credential, you deploy it, then the company revokes the first. Nothing is interrupted, because both work during the overlap.
- Revocation. A revoked client id fails immediately with
401and codeclient_revoked. Tokens already issued to it stop working at the same moment. - Compromise. If a secret may have leaked, ask the company to revoke it
right away and issue a new one. Tell us at
integrations@appraisalhost.comso we can check the access log for the credential.
Rate limits
The limit is 600 requests per minute per client, counted across every endpoint. Every response carries your current position:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Unix time in seconds when the window resets. |
Over the limit, we answer 429 with a Retry-After header in seconds:
{
"code": "rate_limited",
"message": "Too many requests. Retry after 30 seconds.",
"correlation_id": "req_01J9Z48F4H7K0M3P6R9T2W5Y"
}
Wait for Retry-After, then retry. Back off exponentially if you are still
limited. Webhooks exist so that you do not have to poll: a webhook subscription
plus an occasional reconciliation read is well inside the limit, while polling
every order every minute is not.
Idempotency
POST /orders accepts an Idempotency-Key header, and you should always send
one. Use a value that is unique to the order in your own system, for example
los-88421-create-1.
- A key is scoped to your client credential and to the endpoint that accepts it. The same key sent by a different credential is a different key.
- We remember the key and its result for 24 hours.
- A repeat with the same key and the same body returns the original order, with
the same
idand the same201. Nothing is created twice. - A repeat with the same key and a different body is refused with
409and codeidempotency_key_reused. - After 24 hours the key is forgotten, so do not rely on it as a permanent
duplicate guard. Store the returned
idagainst your loan file instead. POST /ordersis the call that takes a key. The other writes carry none, so a request of theirs that timed out is settled by reading rather than by sending it again: readGET /orders/{order_id}/messages,GET /orders/{order_id}/documentsorGET /orders/{order_id}/revision-requestsand look for what you sent.
curl -s -X POST https://api.appraisalhost.com/v1/orders \
-H 'Authorization: Bearer aht_at_7Qk2rX9wTm4ZbN1sV6yH0pL8' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: los-88421-create-1' \
-d @order.json
Transport and hygiene
- HTTPS only, TLS 1.2 or better. Plain HTTP is refused.
- Never put a client id or secret in a URL, a query string, or a browser. A browser cannot keep a secret, so the API is called from your server.
- We do not set cookies and do not keep a session. Every call carries its own token.
- Log the
correlation_idfrom every error. It is the fastest way for our integrations team to find your request.