Developers

The HailMate API

Read and write your workspace’s jobs, contacts, tasks, payments, photos and door knocks, look up hail history at any address, and have HailMate call you within seconds of something happening. It is a plain JSON REST API, included on every paid plan — there is no separate add-on.

Quickstart

Three steps from nothing to a job on your board.

  1. Make a key

    In HailMate open Settings → Integrations → API & Webhooks and press New key. You need to be an owner or an admin of the workspace. Copy the key somewhere safe — it starts with hm_live_ and is shown once.

  2. Check it

    GET /ping is the cheapest authenticated call. It answers with the workspace the key belongs to and whether the key can write.

    curl https://app.hailmate.ai/api/v1/ping \
      -H "Authorization: Bearer $HAILMATE_API_KEY"
    Response · 200
    {
      "ok": true,
      "object": "workspace",
      "workspace_id": "5f0c2a8e-1b3d-4c7a-9e21-7a3b8d4c6e10",
      "workspace": "Ridgeline Roofing",
      "api_version": "v1",
      "key": {
        "name": "Zapier",
        "access": "write"
      }
    }
  3. Create a job

    Only name and address are required. stage takes a stage key or the label your board shows; leave it out and the job lands in the first column of your default pipeline. The Idempotency-Key makes the request safe to retry.

    curl -X POST https://app.hailmate.ai/api/v1/jobs \
      -H "Authorization: Bearer $HAILMATE_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
      "name": "1804 Cedar Ridge Dr",
      "address": "1804 Cedar Ridge Dr",
      "city": "Plano",
      "state": "TX",
      "postal_code": "75024",
      "job_type": "insurance",
      "stage": "Inspection Scheduled",
      "homeowner_id": "b7c2d3e4-4444-4000-8000-0000000000dd",
      "lead_source": "Website",
      "insurance_company": "Example Mutual",
      "claim_number": "CLM-88213",
      "notes": "Homeowner reports hail on the back slope."
    }'

    The answer is the new job, and it is on your pipeline board straight away. Next: subscribe to a webhook so HailMate tells you when it moves.

Authentication

Every request carries the key, either as a bearer token or in X-API-Key. Both work everywhere; use whichever your tool makes easier.

Headers
Authorization: Bearer hm_live_…

# or
X-API-Key: hm_live_…
  • A key belongs to one workspace and can only ever see that workspace’s records. If you run several workspaces on one plan, each needs its own key.
  • The key is shown once. HailMate stores only a scrambled version, so nobody — us included — can read it back. Lost it? Revoke it and make another.
  • Revoking a key takes effect at once, and switches off the webhooks that key created.
  • There is no sandbox. To try things out without touching real jobs, use a separate workspace.

Full access and read-only keys

AccessCanUse it for
Full accessRead everything, create, change and delete records, and subscribe to webhooks.Your own integrations, Zapier, Make and n8n.
Read onlyRead everything and subscribe to webhooks. Changes nothing — a write answers 403 forbidden.A reporting tool, a dashboard, an accountant.

GET /ping tells you which one you are holding: key.access is write or read.

Log in with HailMate (OAuth 2.0)

An app that connects many HailMate companies — Zapier is the first — does not ask anybody for a key. It sends the person to HailMate, they pick a company and press Allow, and the app is connected. It is the standard OAuth 2.0 authorization-code flow:

  • Send the person to https://app.hailmate.ai/oauth/authorize with response_type=code, client_id, redirect_uri and state (add scope=read for read-only). Only an owner or admin can press Allow.
  • They come back with ?code=…&state=…. Within ten minutes, trade the code at POST /oauth/token for an access token (hm_oat_…), and send it as Authorization: Bearer exactly like a key.
  • An access token lasts an hour; trade the refresh token (hm_ort_…) for a new one. The refresh token does not change.
  • The connection shows in Settings → API & Webhooks as a Connected app, and disconnecting it there stops it at once. Apps are registered by us — write to contact@hailmate.ai.

Requests and responses

Every path is under https://app.hailmate.ai/api/v1. Send and receive JSON (Content-Type: application/json); the one exception is a file upload, which may also be multipart/form-data.

  • Every record carries id (a UUID) and object (job, contact, task…). Most carry url, which opens that record in HailMate.
  • Timestamps such as created_at are ISO 8601 in UTC. Dates such as date_of_loss are YYYY-MM-DD. A task’s due_date is the exception — see wall-clock times.
  • A value HailMate doesn’t have is null rather than an empty string, and an empty list such as tags is []. Money is a plain number of dollars.
  • PATCH sends only what changes. A field sent as null is cleared; a field left out is untouched.
  • Deleting a job or a contact puts it in Settings → Recently Deleted for 30 days, restorable with everything on it. A deleted task is gone.

Pagination and syncing

Every list is newest first and returns the same envelope. Pass next_cursor back as ?cursor= until it is null. limit is 1 to 200 (default 50).

GET /jobs?limit=1
{
  "object": "list",
  "data": [
    {
      "id": "f2b1a0c4-1111-4000-8000-0000000000aa",
      "object": "job",
      "job_number": "JOB-00142",
      "stage": "claim_approved"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjIjoiMjAyNi0wOS0xNlQxNDowMjowMC4wMDBaIiwiaSI6ImYyYjFhMGM0In0"
}
  • Don’t build a cursor yourself. It is opaque and ours to change; one we did not issue is a 400.
  • Every list takes updated_since, created_after and created_before (an ISO date or date-time). Store when you last synced and send it as updated_since to pull only what moved.
  • The /search endpoints are not paged: they return the best matches, up to limit.
async function* allJobs(filters = {}) {
  let cursor = null;
  do {
    const query = new URLSearchParams({ limit: '200', ...filters });
    if (cursor) query.set('cursor', cursor);
    const response = await fetch(`https://app.hailmate.ai/api/v1/jobs?${query}`, {
      headers: { Authorization: `Bearer ${process.env.HAILMATE_API_KEY}` },
    });
    const page = await response.json();
    if (!response.ok) throw new Error(`${page.error.code}: ${page.error.message}`);
    yield* page.data;
    cursor = page.next_cursor; // null on the last page
  } while (cursor);
}

// Everything that changed since your last sync.
for await (const job of allJobs({ updated_since: '2026-09-01T00:00:00Z' })) {
  console.log(job.job_number, job.stage_label, job.balance_due);
}

Rather be told than ask?

Polling with updated_since is fine, but a webhook reaches you within a second or two of the change and costs none of your rate limit.

Idempotency

Networks drop answers. Send Idempotency-Key: <any unique string> on a POST, PATCH or DELETE and a retry with the same key replays the first answer instead of doing the work twice — no second job, no second payment.

  • Keys are remembered for 24 hours. A UUID is ideal; so is the id of the thing that caused the request, such as a form submission.
  • A replayed answer carries Idempotent-Replayed: true.
  • Reusing a key for a different request is a 409 conflict. So is a retry that arrives while the first request is still running — wait a moment and try again.

Rate limits

120 requests a minute per key. Hail history lookups are also limited to 20 a minute per key, inside that. Every response says where you stand:

Response headers
HTTP/1.1 200 OK
X-Request-Id: req_4c1n8y2m0q7p3x5z9a1b
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 42

X-RateLimit-Reset is the number of seconds until the window resets. Past the limit the answer is 429 rate_limited with a Retry-After header in seconds — wait that long, then retry.

Retrying a 429 (Node.js)
async function hailmate(path, options = {}, attempt = 1) {
  const response = await fetch(`https://app.hailmate.ai/api/v1${path}`, {
    ...options,
    headers: { Authorization: `Bearer ${process.env.HAILMATE_API_KEY}`, ...options.headers },
  });
  if (response.status === 429 && attempt < 5) {
    const wait = Number(response.headers.get('Retry-After') ?? 1);
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
    return hailmate(path, options, attempt + 1);
  }
  return response;
}

Errors

Every error has the same shape, whatever went wrong. Branch on code — message is written for a person and may be reworded. field names the request field that was wrong, when there was one.

400 Bad Request
{
  "error": {
    "code": "invalid_request",
    "message": "No stage called \"Aproved\" on this workspace's boards. Use a key or a label from GET /v1/stages.",
    "field": "stage",
    "request_id": "req_4c1n8y2m0q7p3x5z9a1b",
    "doc_url": "https://hailmate.ai/docs/api#errors"
  }
}
CodeHTTPWhat it means
invalid_request400Something in the request was wrong. field names what.
unauthorized401The key is missing, wrong or revoked.
plan_required402The workspace has no active subscription.
forbidden403A read-only key tried to change something.
not_found404No such record in this workspace.
method_not_allowed405Wrong verb for that path.
conflict409An Idempotency-Key was reused for a different request, or the first request with it is still running.
payload_too_large413Files are capped at 25 MB.
rate_limited429Too many requests. Wait Retry-After seconds, then retry.
server_error500Our fault. Retry shortly, and quote the request_id if it keeps happening.

A 5xx, a 429 or a dropped connection is worth retrying — with an Idempotency-Key on writes. A 4xx other than 429 will fail the same way again until the request changes.

Request IDs and the request log

Every response carries X-Request-Id, and every error body repeats it as request_id. Log it — it is the fastest way for us to find one request among millions.

Owners and admins can see every call a key made — its path, status and the error if there was one — under Settings → Integrations → API & Webhooks → Requests, for 14 days.

How HailMate data works

A few things that save an integration from its most common mistakes.

Stages have a key and a label

Every workspace names its own board. GET /stages gives each stage’s key (what you filter on) and label (what a person reads). Every job carries both, plus stage_is_completed and stage_is_lost. Anywhere you send a stage you may use either, so {"stage": "Claim Approved"} works. Moving a job with PATCH /jobs/{id} runs your stage automations and fires job.stage_changed, exactly as dragging the card does.

Money matches the Money tab

Every job carries total_job_value, amount_received and balance_due — the same figures its Money tab shows. An estimate’s total is the package the homeowner chose (else the base package), computed the way the PDF computes it; package_totals lists every package. A refund is its own payment with a negative amount, so summing amount gives the net. A payment applies to an invoice only when you name one with invoice_id — HailMate never guesses which invoice a cheque pays.

Phone numbers match however they were typed

GET /contacts/search?phone= treats +16155550148, 615-555-0148 and (615) 555-0148 as the same number. Send whatever format you have. US numbers you send are stored as (615) 555-0148, the way your team types them.

Appointment times are wall-clock

An appointment is a task with appointment_type set to inspection, adjuster_meeting or build_day. Its due_date is the local time your crew will read, with no offset — 2026-10-02T15:00:00. If you send one with an offset (2026-10-02T15:00:00-05:00, which is what Zapier sends), the offset is dropped and 3:00 PM is kept. HailMate never shifts an appointment by a time zone.

/jobs/search and /contacts/search with no criteria return an empty list — never “the newest records”, which is how a blank search step ends up writing to the wrong job.

What the API deliberately cannot do

  • Set sms_consent. No integration can establish that a homeowner agreed to be texted.
  • Create an estimate or an invoice. Both are documents with pricing and a signing or payment flow, built in HailMate. You can read both, and be told when they are sent, viewed, signed or paid.
  • Build a storm list. That spends data credits and happens in HailMate; the API reads the result, and storm_list.ready tells you when it is done.

Versioning

The version is in the path: /v1. Within it we only ever add — new endpoints, new fields, new events, new values. We don’t rename or remove a field, or change what one means, inside a version.

  1. Ignore fields you don’t recognise rather than failing on them.
  2. Treat enums as open: handle a value you haven’t seen, such as a new knock result, gracefully.
  3. Watch the changelog for what’s new.

OpenAPI document

The whole API — every path, parameter, object and webhook — is described in one OpenAPI 3.1 document. The reference on this site is rendered from it. Import it into Postman, Insomnia or a code generator.

Getting help

Email contact@hailmate.ai or use the Help button in the app, and quote the request_id of the call that went wrong.