Sandbox
Build and test against the same contract you will use in production, with test data and without touching a real appraisal.
| Environment | Base URL |
|---|---|
| Sandbox | https://sandbox.api.appraisalhost.com/v1 |
| Production | https://api.appraisalhost.com/v1 |
Getting access
Sandbox access is provisioned on request by our integrations team. There is no self serve signup, because every sandbox credential is tied to a test lender account we create for you.
Write to integrations@appraisalhost.com and tell us:
- Your company name and the product you are integrating.
- A technical contact: name, email, and time zone.
- Whether you will receive webhooks during the build, and the HTTPS URL to
deliver them to. You can set or change this yourself later with
PUT /webhook. - Which order types you need in the sandbox catalog: at least one product on the current report standard, at least one on the prior standard, and a product with an FHA certification requirement if you will place those orders.
We reply with a client id, a client secret, and the sandbox order type codes. Keep the secret in a secret manager: it is shown once.
What the sandbox is
- The same API, the same contract, the same error catalog, the same webhook signatures.
- A test appraisal management company with a test lender account, a test order type catalog, and a test appraiser.
- Your own data only. Nothing in the sandbox is shared with another partner.
Driving the sandbox
You drive the sandbox yourself. Two calls do it: one moves an order through the lifecycle, the other sends you a test delivery. Both are ordinary API calls, so your test suite runs a full lifecycle on every commit without anyone else being involved.
Move an order
POST /sandbox/orders/{order_id}/advance moves one of your sandbox orders to the
status you name, and raises everything a real move raises: the same events, the
same payloads, the same signed deliveries to your endpoint.
curl -s -X POST https://sandbox.api.appraisalhost.com/v1/sandbox/orders/ord_9TBK4C2QFA7M/advance \
-H 'Authorization: Bearer aht_at_7Qk2rX9wTm4ZbN1sV6yH0pL8' \
-H 'Content-Type: application/json' \
-d '{ "to_status": "appraiser_assigned" }'
Three rules:
- It exists in the sandbox only. The same path in production returns
404. - The transition table below is the whole rule for
to_status. You may skip statuses, because a real order often does. You may not move an order backwards, you may not name the status the order is already at, and you may not reachpending_review,revision_requested,reconsideration_requested,on_hold,off_hold,cancelledorreactivatedthis way: those are produced byPOST /orders, byPOST /orders/{order_id}/revision-requests, and byhold,resumeandcancel, which work in the sandbox exactly as they do in production. A move the table does not list is refused with409and codeaction_not_allowed_in_status, with the order's current status incurrent_statusand the status you asked for indetails[0].value. - It fires the same notifications production fires, to the same endpoint, with the same signature.
What you may advance to, and from where
| Current status | to_status you may name |
|---|---|
pending_review | new |
new | appraiser_assigned, appraiser_accepted, borrower_contacted, left_message_for_borrower, appointment_scheduled, awaiting_review, report_rejected_for_correction, report_complete |
appraiser_assigned | appraiser_accepted, borrower_contacted, left_message_for_borrower, appointment_scheduled, awaiting_review, report_rejected_for_correction, report_complete |
appraiser_accepted | borrower_contacted, left_message_for_borrower, appointment_scheduled, awaiting_review, report_rejected_for_correction, report_complete |
borrower_contacted | left_message_for_borrower, appointment_scheduled, awaiting_review, report_rejected_for_correction, report_complete |
left_message_for_borrower | borrower_contacted, appointment_scheduled, awaiting_review, report_rejected_for_correction, report_complete |
appointment_scheduled | awaiting_review, report_rejected_for_correction, report_complete |
awaiting_review | report_rejected_for_correction, report_complete |
report_rejected_for_correction | awaiting_review, report_complete |
report_complete | Nothing. Open a correction or a reconsideration first, which moves the order to revision_requested or reconsideration_requested. |
revision_requested | revised_report_complete |
reconsideration_requested | revised_report_complete |
revised_report_complete | Nothing. Open another correction first. |
on_hold, cancelled | Nothing. Resume a held order first; a cancelled order is not advanced. |
An order at off_hold or reactivated may be advanced to whatever the table
allows for the status it held before the hold or the cancellation, which the
hold and cancel responses give you in previous_status.
A sequence that exercises the whole contract, step by step against the table above:
- Place an order with
POST /orders. It is created atnew, andorder.createdfires. An order the company would review first is created atpending_review: advance it tonewbefore the rest of the sequence. - Advance to
appraiser_assigned: you receiveorder.assignedandorder.status_changed. - Advance to
appointment_scheduled, skippingappraiser_acceptedand the borrower contact statuses, which the table allows and a real order often does. Then advance toawaiting_review. - Advance to
report_complete: you receiveorder.completedwith every delivered document listed, plusorder.document_addedfor each file. - Open a revision request, which moves the order to
revision_requested, then advance torevised_report_complete: you receiveorder.revision_requested,order.revision_responded, and a secondorder.completedwithrevisionset to true. - On separate orders, call
hold,resumeandcancelto exercise the409rules and the remaining events. The advance call cannot set those statuses.
Send yourself a test delivery
POST /webhook/test delivers one signed specimen event to your configured
endpoint and returns what your endpoint answered. It is how you prove your
signature check, your timestamp tolerance and your ten second acknowledgement
before you place an order at all.
curl -s -X POST https://sandbox.api.appraisalhost.com/v1/webhook/test \
-H 'Authorization: Bearer aht_at_7Qk2rX9wTm4ZbN1sV6yH0pL8' \
-H 'Content-Type: application/json' \
-d '{ "type": "order.status_changed" }'
{
"event_id": "evt_01M2TM1N60M4HGM9QGTHVCDMA2",
"type": "order.status_changed",
"url": "https://los.example.com/hooks/appraisal-host",
"delivered": true,
"response_status": 200,
"response_time_ms": 142,
"attempted_at": "2026-09-18T16:02:04Z"
}
The delivery is signed with your current secret and carries the same headers as
a real one. Its envelope carries "test": true, and it is not written to your
feed, so GET /events is unaffected. Its payload carries a synthetic
order_id, always prefixed ord_test_, which no real order in any environment
ever carries, so a specimen can never be mistaken for one of your orders.
POST /webhook/test works in production too, on the same terms: see
Webhooks for what a handler does with it.
What differs from production
| Difference | Detail |
|---|---|
| No real people | No email or message ever reaches a real borrower, agent or appraiser. Notifications stay inside the sandbox. |
| Test data only | Use fictional borrowers and fictional contact details. The sandbox is for test data only: real borrower data belongs in production. |
| Test documents | Delivered reports, invoices and certificates are sample files with the right shape, the right media type and the right document kinds. They are not real appraisals and carry no opinion of value you should read. |
| Test catalog | Order type codes, loan types and add-ons are ours, not a real company's. Production codes come from the appraisal management company you will serve, and are read from GET /order-types, never hard coded. |
| Advancing | Sandbox orders move when you call POST /sandbox/orders/{order_id}/advance, in the sequence you are testing, rather than on a clock. |
| Rate limit | The same 600 requests per minute, so that your retry and backoff behavior is tested honestly. |
| Data retention | Sandbox orders and events may be cleared when an integration goes quiet for a long period. Nothing there is a record of anything. |
A checklist before you go live
Work through this in the sandbox. It is the same list our integrations team walks with you before we hand over production credentials.
- The lender account you will use in production is one the appraisal management company invoices, rather than one that pays per order by card.
- You read
GET /order-typesand send codes from it, rather than codes written into your source. - You send an
Idempotency-Keyon every create, and you handle the409for a reused key. - You store our
idfor every order and document against your own loan file. - You handle every status code in the catalog, including statuses your
first test happened not to produce, and you branch on
coderather than onlabel. - You verify the webhook signature, reject a stale timestamp, and return a 2xx within 10 seconds before doing your own work.
- Your webhook handler is idempotent on the event id, and tolerates events arriving out of order.
- You can catch up from
GET /eventsafter your listener has been down. - You handle
401with one token refresh and retry, and stop on a revoked client. - You handle
429withRetry-After. - You download documents through a fresh signed link rather than storing links, and you handle all the document kinds your products deliver.
- You parse a
report_datafile in every report format your products are delivered in. - You call
GET /accountat start-up for each credential and assert the lender it names against your own configuration.
Moving to production
Production credentials are issued by the appraisal management company you are integrating for, not by us. When your sandbox run is complete, tell us and we will introduce you. Change the base URL and the credential: nothing else in your integration changes.