Quick start
- Get a test account. Email rishab@homesurvey.ai with the subject "HomeSurvey.ai Open API". You get a sandbox URL and a test key. In production, each mover's API key is in its HomeSurvey.ai settings.
- Create a survey. Call Generate survey with the customer's name and your own record ID in
externalReference. Store thesurveyIdit returns. Send the link yourself, or call Send survey link. - Get the results. Register your webhook once. When the mover's estimator reviews the survey, we send the inventory to your URL.
Overview
Start a survey
| API | What it does |
|---|---|
POST /api/external/v1/survey/generate |
Creates a survey and returns its link. |
POST /api/external/v1/survey/{id}/send-link |
Sends (or re-sends) the link to the customer by email, text or both. |
Get the results
| API | What it does |
|---|---|
POST /api/external/v1/webhook |
Registers the URL where we send finished survey results. |
GET /api/external/v1/webhook |
Shows the webhook registered for this mover. |
DELETE /api/external/v1/webhook |
Removes it. |
POST /api/external/v1/webhook/test |
Sends one signed sample message to your URL. |
| Results webhook | We send the reviewed survey to your URL. |
GET /api/external/v1/survey/{id} |
Fetches the same results on demand, for example if your endpoint was down. |
Base URL: https://apiv1.go.helixiq.ai (production). A sandbox URL and test key come with your test account.
Rate limit: 60 calls a minute per API key. Above that you receive HTTP 429; wait and retry.
Authentication
Every call carries the moving company's API key in the header X-API-KEY. Each mover has its own key, found in its HomeSurvey.ai settings. Your integration uses the key of the mover you are serving, and a survey can only be reached with the key of the mover it belongs to.
All responses use one envelope:
{"status": 1, "data": { }, "message": "..."}
status 1 means success. Errors use the same envelope with status 0 and a message; for validation errors data maps each field to its problem.
| HTTP code | Meaning |
|---|---|
| 400 | Bad request. |
| 401 | Missing or wrong API key. |
| 402 | The mover has no AI survey credits left. |
| 403 | Not allowed for this key. |
| 404 | Survey not found. |
| 429 | Too many calls. Wait and retry. |
| 500 | Server error. |
1. Generate survey
POST/api/external/v1/survey/generate
Creates a new survey for a customer and returns its link. No message is sent to the customer by this call; send the link yourself, or call Send survey link.
- Credits: each new survey holds one of the mover's AI survey credits. The credit returns if the link is not used within 30 days. If the mover has no credits left, the call returns 402.
- Safe to retry: if you send the same
externalReferenceagain within 30 days, we return the survey already created for it ("duplicate": true) instead of creating a second one. - Military moves: not supported. Generate survey refuses military moves (see Military moves).
- Store
surveyId(for exampleSV000042). It is the ID used everywhere else in the API.
Body (JSON)
| Field | Type | Required | Notes |
|---|---|---|---|
customerName |
string, max 100 | yes | |
customerEmail |
string (email) | no | Needed to send the link by email. |
customerPhoneNumber |
string | no | Needed to send the link by text. |
externalReference |
string, max 100 | no | Your own record ID (for example an opportunity ID). Returned in every results message. |
externalSource |
string, max 50 | no | Your system's name. Returned in every results message. |
moveType |
string | no | One of GENERAL, LOCAL, INTRASTATE, INTERSTATE, INTERNATIONAL, STORAGE, COMMERCIAL, AUTO, SENIOR, STUDENT, SPECIALTY. Exact match. |
moveCategory |
string | no | One of RESIDENTIAL, COMMERCIAL, GENERAL. |
moveDate |
number | no | Move date as a Unix timestamp in milliseconds. |
originAddress |
string | no | |
type |
string | no | Who the link is for: CUSTOMER (default) or SALES (your estimator films the survey; requires salesRep.email). |
salesRep |
object | no | firstName, lastName, email, phoneNumber of the estimator. |
surveyNote |
string | no | A note shown with the survey. |
Example request
curl --location 'https://apiv1.go.helixiq.ai/api/external/v1/survey/generate' \
--header 'X-API-KEY: <api-key>' \
--header 'Content-Type: application/json' \
--data-raw '{
"customerName": "Jane Doe",
"customerEmail": "jane@example.com",
"customerPhoneNumber": "+15550100199",
"externalReference": "OPP-55821",
"externalSource": "your-crm",
"moveType": "LOCAL",
"moveDate": 1796515200000,
"originAddress": "123 Main St, San Diego, CA"
}'
Example response
{
"status": 1,
"data": {"id": "665f1a2b3c4d5e6f7a8b9c0d", "surveyId": "SV000042", "link": "https://homesurvey.ai/s/8f2b9c1a4e6d7f0b3c5a1e9d2f4b6a8c"},
"message": "Survey generated successfully"
}
2. Send survey link
POST/api/external/v1/survey/{id}/send-link
Sends, or re-sends, the link of an existing survey to the customer. It does not create a new survey or a new link.
Path: id is the survey ID returned by Generate survey (for example SV000042).
Body (JSON, optional)
| Field | Type | Notes |
|---|---|---|
medium |
string | EMAIL, SMS or BOTH. Leave it out to use the same channel as last time (email the first time). Email needs the customer's email; text messages need the customer's phone number and the mover's text messaging set up in HomeSurvey.ai. If a channel cannot be used, the call returns 400 and says why. |
The response lists the channels the link actually went out on in data.sent_via.
Example request
curl --location 'https://apiv1.go.helixiq.ai/api/external/v1/survey/SV000042/send-link' \
--header 'X-API-KEY: <api-key>' \
--header 'Content-Type: application/json' \
--data '{"medium": "BOTH"}'
Example response
{
"status": 1,
"data": {"id": "665f1a2b3c4d5e6f7a8b9c0d", "surveyId": "SV000042", "link": "https://homesurvey.ai/s/8f2b9c1a4e6d7f0b3c5a1e9d2f4b6a8c"},
"message": "Survey link sent successfully"
}
(data also includes "sent_via": ["EMAIL", "SMS"].)
3. Register your webhook
POST/api/external/v1/webhook
| Field | Type | Required | Notes |
|---|---|---|---|
webhookUrl |
string (https) | yes | Where we send results. |
events |
array | no | Default ["survey.reviewed"]. Add survey.completed or survey.failed if you want them. |
profile |
string | no | full (default) or core. See "Take what you can use". |
curl --location 'https://apiv1.go.helixiq.ai/api/external/v1/webhook' \
--header 'X-API-KEY: <api-key>' \
--header 'Content-Type: application/json' \
--data '{"webhookUrl": "https://your-endpoint.example.com/webhooks/homesurvey", "profile": "full"}'
GET /api/external/v1/webhook returns the URL, events and profile registered for this mover.
DELETE /api/external/v1/webhook removes it. POST /api/external/v1/webhook/test sends one signed sample message to your URL so you can check your endpoint.
The webhook secret used to sign messages is shown in the mover's HomeSurvey.ai settings, next to the API key.
4. Results webhook
Events
| Event | When it fires |
|---|---|
survey.reviewed |
The mover's estimator saved the survey as reviewed. This is the inventory a mover quotes from. Sent by default. |
survey.completed |
AI processing finished, before human review. Only if you subscribe to it. |
survey.failed |
The survey could not be processed. Only if you subscribe to it. |
If a survey is reviewed again later, we send survey.reviewed again with a higher revision. Replace the earlier data.
Take what you can use: core and full
Every results message is built so that a system which can store only part of it still gets the inventory.
- Core: survey details, totals, and every inventory line by room (name, quantity, quantity not going, volume and weight each, category, handling). Every line stands on its own, including items packed in cartons.
- Full: core plus cartons, crated items, questionnaire answers, voice notes, the AI move summary, item photos and a cube sheet PDF.
- Ignore any block you do not use. Every block outside the core is optional.
- Every message also carries
notes_text: the questionnaire answers, voice notes and AI summary as one plain-text block, for systems that only have a notes field. - If an optional part cannot be produced, we still send the core inventory, set
partialto true and list what is missing inmissing.
Delivery
- Signature: every message carries
X-HomeSurvey-Signature: sha256=<hex>, an HMAC-SHA256 of the raw request body using the mover's webhook secret. Reject any message that fails the check. - Headers:
X-HomeSurvey-Event,X-HomeSurvey-Event-Id,X-HomeSurvey-Delivery-Attempt. - Retries: if you do not answer with a 2xx within 10 seconds, we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours.
- Duplicates:
event_idstays the same across retries. Use it to ignore repeats. - Files: photos, audio and the cube sheet PDF are links that expire after 7 days. Download what you want to keep.
- Units:
unitsisimperial(cu ft, lbs) ormetric(CBM, kg). Every volume and weight in a message uses the same units.
Checking the signature (Python)
import hmac, hashlib
def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
if not header.startswith("sha256="):
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header[7:])
Message contents
| Block | What it holds |
|---|---|
schema_version, event, event_id, sent_at, revision, review_status |
Message details. review_status is reviewed or ai_only. |
survey |
Survey ID, your external_reference and external_source, customer name, email and phone, move type and category, move date, origin address, how the survey was filmed (customer_phone, estimator_phone, ai_glasses), units, when it was submitted and reviewed, who reviewed it, the reviewer's notes. |
totals |
Total volume and weight, item volume and weight, packed volume and weight, line count, piece count, carton count, crate count, lines not going. |
rooms[].items[] |
Item ID, name, quantity, quantity not going, size, volume and weight each, category, handling (two_man, heavy, fragile, needs_crate, disassemble, antique, high_value, third_party_service), high value and declared value, whether it is packed in a carton, whether the estimator added it, whether it came from the customer's voice note, the reviewer's note, photo link. |
cartons[] |
Carton type, capacity, count, packed by the mover (CP) or by the owner (PBO), the item IDs inside. |
crated_items[] |
Item, room, quantity, crate dimensions and volume, weight, handling, declared value, note. |
questionnaire[] |
Question, room (if any), answer. |
voice_notes[] |
Room, time in the video, the note in words, the item IDs it refers to, audio link. |
ai_summary |
Scope, customer summary, customer priorities, estimator tips, risk flags, crew recommendation, room summaries. |
documents |
Cube sheet PDF link. |
notes_text |
Questionnaire, voice notes and AI summary as plain text. |
partial, missing |
Set when an optional part could not be produced. |
Example
{
"schema_version": "1.0",
"event": "survey.reviewed",
"event_id": "evt_01JC8Z6Q2K4M",
"sent_at": "2026-11-20T18:04:11Z",
"revision": 1,
"review_status": "reviewed",
"survey": {
"survey_id": "SV000042",
"external_reference": "OPP-55821",
"external_source": "your-crm",
"customer": {"name": "Jane Doe", "email": "jane@example.com", "phone": "+1 555 010 0199"},
"move_type": "LOCAL", "move_category": "RESIDENTIAL",
"move_date": "2026-12-05", "origin_address": "123 Main St, San Diego, CA",
"survey_method": "ai_glasses", "units": "imperial",
"submitted_at": "2026-11-20T16:40:02Z", "reviewed_at": "2026-11-20T18:03:55Z",
"reviewed_by": {"name": "Sam Estimator", "email": "sam@mover.example"},
"reviewer_notes": "Piano goes with a third-party mover."
},
"totals": {
"total_volume": 412.5, "total_weight": 2888, "item_volume": 356.0, "item_weight": 2492,
"packed_volume": 56.5, "packed_weight": 396, "items_count": 48, "pieces_count": 61,
"carton_count": 22, "crate_count": 1, "excluded_count": 3
},
"rooms": [
{"room_id": "living", "room_name": "Living Room", "items": [
{"item_id": "i-14", "name": "Sofa 3-Seater", "quantity": 1, "quantity_excluded": 0, "size": "large",
"volume_each": 60, "weight_each": 420, "category": "Sofa",
"handling": ["two_man", "heavy"], "high_value": false, "declared_value": null,
"in_carton": false, "added_by_reviewer": false, "from_voice_note": false,
"note": null, "photo_url": "https://files.homesurvey.ai/s/..."}
]}
],
"cartons": [
{"carton_id": "c-3", "room_id": "living", "carton_type": "3.0 cu ft box",
"capacity": 3.0, "count": 4, "packing": "CP", "contents": ["i-21", "i-22"]}
],
"questionnaire": [{"question": "Do you have stairs at origin?", "room_name": null, "answer": "Yes, one flight"}],
"voice_notes": [{"room_name": "Garage", "at_seconds": 42.5, "text": "The workbench stays.",
"item_ids": ["i-77"], "audio_url": "https://files.homesurvey.ai/s/..."}],
"ai_summary": {"scope": "...", "customer_summary": "...", "customer_priorities": ["..."],
"estimator_tips": ["..."], "risk_flags": ["..."], "crew_recommendation": "...",
"room_summaries": {"Living Room": "..."}},
"documents": {"cube_sheet_pdf_url": "https://files.homesurvey.ai/s/..."},
"notes_text": "QUESTIONNAIRE\nStairs at origin: Yes, one flight\n\nVOICE NOTES\nGarage: The workbench stays.\n\nAI SUMMARY\n...",
"partial": false,
"missing": []
}
5. Fetch results on demand
GET/api/external/v1/survey/{id}?profile=full
Returns the same body as the results message, for a survey that belongs to this mover's key. review_status tells you whether it has been reviewed yet. Use it if your endpoint missed a message, or to check a survey's state.
Military moves
Military moves are not in scope for the Open API. Generate survey refuses military moves, and results for military moves are not sent to outside systems.