Developers

Helix Field API

Read a company's customers, jobs, invoices and booking requests, add customers and jobs, and have Helix Field post to your URL when something happens. This page describes version 1 of the API exactly as it behaves.

Base URLhttps://app.helixfielddispatch.com/api/public/v1
FormatJSON over HTTPS
AuthenticationAn API key in a request header

Getting started

Every request goes to the base URL above, followed by the endpoint's path. The version is part of the path, so a request to /api/public/v1/me always gets version 1.

Get an API key

  1. An admin of the company signs in to the Helix Field office app and opens Settings > Integrations.
  2. Under Make a key for, pick Zapier or Your own software and press Make key.
  3. Copy the key straight away. It is shown once and never again; Helix Field keeps only a fingerprint of it, so a lost key cannot be looked up. Revoke it and make another.

A key belongs to the company, not to a person, and everything it creates is recorded against the key. Keys start with hf_live_. A company can have up to ten keys that are not revoked. The Integrations screen lists each key by its first characters, the day it was made and the day it was last used, and an admin can revoke any of them there. A revoked key stops working at once, and the webhook subscriptions it made are turned off with it.

Make a first call

GET /me answers with the company the key belongs to. It is the call to make to check a key.

Request
curl https://app.helixfielddispatch.com/api/public/v1/me \
  -H "Authorization: Bearer $HELIX_API_KEY"
Response 200
{
  "company": "Helix Field Demo",
  "company_id": "55cc855f-ba9d-482c-bd91-9840b36e753c",
  "key_name": "Your own software",
  "api_version": "v1",
  "events": ["booking.received", "customer.created", "invoice.paid", "job.completed", "job.created"]
}

Authentication

Send the key in one of these two headers on every request:

  • Authorization: Bearer hf_live_...
  • X-API-Key: hf_live_..., for clients that cannot set the first one

A key in the URL is not read. A request that carries its key only in the query string is answered 401, which keeps keys out of server logs and browser history.

A missing key, a key that does not start with hf_live_, an unknown key and a revoked key all get the same answer, so the response never says which of these it was:

HTTP/1.1 401 Unauthorized
{"detail": "A valid API key is required"}

When the company's Helix Field account is locked (its subscription was canceled or stayed unpaid past the grace period, or the account was closed), every request with its key is answered 402, as the office app is:

HTTP/1.1 402 Payment Required
{"detail": "This company's Helix account is not active"}

Requests and responses

  • Bodies are JSON. Send Content-Type: application/json with a body. Fields this page does not list are ignored.
  • Ids are UUID strings, such as "6505eb45-3350-4c08-b6ee-45f2d2cc4319".
  • Times are ISO 8601, written without an offset and to the second, such as "2026-10-16T07:52:08". When something happened (created_at, completed_at, occurred_at) is in UTC; created_at on a webhook subscription also carries fractions of a second. A job's scheduled_date is the company's own clock time as it was booked, and an invoice's issued_date and due_date are days on the company's calendar, written at midnight.
  • Money is a string with two decimals, such as "185.00", so no amount passes through a floating point number.
  • Fields with no value are present and null; lists with nothing in them are [].

Lists

Every list endpoint returns a JSON array, newest first by the time the record was created. limit sets how many come back: 25 when it is left out, at most 100. A number below 1 is read as 1 and a number above 100 as 100; a value that is not a whole number is a 422.

There are no pages and no cursor: a list returns the newest records up to limit. A list never includes records the company has removed.

Errors

An error is a status code and a JSON body with a detail; a 429 is the one exception (see Rate limits). Most details are one sentence written to be shown to a person:

{"detail": "Customer not found"}

When the request itself is malformed (a required field is missing, a field is too long or of the wrong type, or the body is not valid JSON), detail is a list instead, one entry per problem. loc says where the problem is; the value that was sent is never echoed back.

{"detail": [
  {"type": "missing", "loc": ["body", "first_name"], "msg": "Field required"}
]}
StatusWhen
401The key is missing, malformed, unknown or revoked.
402The company's Helix Field account is locked.
404The customer named in the path or in customer_id is not one of this company's customers, or the path does not exist.
405The path exists but not with this method.
409A customer with that email already exists, or a job was asked for a customer marked "Do not service" at a company that blocks work for them.
422The request is malformed, or a value is not allowed (for example a job_type that is not on the list).
429Too many requests to an endpoint that has a rate limit. See Rate limits.

Rate limits

The endpoints that write have a limit per minute, counted per calling IP address:

EndpointLimit
POST /customers60 a minute
PUT /customers/{customer_id}/tags60 a minute
POST /jobs60 a minute
POST /hooks30 a minute

The other endpoints have no limit of their own. A request over the limit is answered 429 with this body, and nothing is saved:

{"error": "Rate limit exceeded: 60 per 1 minute"}

No Retry-After header is sent. Wait for the minute to pass and send the request again.

Endpoints

Paths are relative to https://app.helixfielddispatch.com/api/public/v1.

EndpointWhat it does
GET /meThe company this key belongs to
GET /eventsThe webhook events, each with its description
GET /customersList customers, or find one by email
POST /customersAdd a customer
PUT /customers/{customer_id}/tagsReplace a customer's tags
GET /jobsList jobs
POST /jobsCreate a job for a customer
GET /invoicesList invoices
GET /bookingsList booking requests from the online booking form
GET /hooksList webhook subscriptions
POST /hooksSubscribe a URL to an event
DELETE /hooks/{hook_id}Unsubscribe
GET

/me

The company and key making the request. Use it to test a key.

FieldTypeDescription
companystringThe company's name in Helix Field
company_idstringThe company's id
key_namestringWhat the key was made for: "Zapier" or "Your own software"
api_versionstringAlways "v1" on this path
eventsarray of stringsEvery event a subscription can name, sorted

The example is under Getting started.

GET

/events

Every webhook event with the sentence the office app shows for it.

Response 200
[
  {"event": "booking.received", "description": "A booking request arrived from the online form."},
  {"event": "customer.created", "description": "A customer was added."},
  {"event": "invoice.paid", "description": "An invoice was paid in full."},
  {"event": "job.completed", "description": "A job was completed by the crew."},
  {"event": "job.created", "description": "A job was created."}
]
GET

/customers

The company's customers, newest first, as customer objects.

QueryDescription
limitHow many to return: 25 by default, 1 to 100
emailFind a customer by email. Spaces around the address are trimmed and it is matched against the address on file in any case. The answer is a list with the customer in it, or an empty list when there is none.
Request
curl "https://app.helixfielddispatch.com/api/public/v1/customers?email=dana.reyes@example.com" \
  -H "Authorization: Bearer $HELIX_API_KEY"
Response 200
[
  {
    "id": "6505eb45-3350-4c08-b6ee-45f2d2cc4319",
    "name": "Dana Reyes",
    "first_name": "Dana",
    "last_name": "Reyes",
    "company_name": null,
    "email": "dana.reyes@example.com",
    "phone": "405-555-0134",
    "customer_type": "commercial",
    "tags": ["Spring campaign"],
    "created_at": "2026-10-16T07:52:08"
  }
]
POST

/customers

Adds a customer and answers 201 with the new customer object. A customer.created webhook is sent for it.

FieldTypeDescription
first_namestring, required1 to 100 characters
last_namestringUp to 100 characters. Empty when left out.
company_namestringUp to 200 characters
emailstringUp to 255 characters. Saved trimmed and lowercased. It must not belong to another customer of the company in any case, removed customers included; if it does, the answer is 409 with "A customer with that email already exists. Find them by email instead." A customer can be added with no email.
phonestringUp to 50 characters
notesstringUp to 5,000 characters. Saved on the customer for the office; not returned by the API.
customer_typestringresidential (when left out), commercial or contractor
tagsarray of stringsUp to 50 tag names, each 1 to 40 characters. A name the company does not have yet is created.
Request
curl https://app.helixfielddispatch.com/api/public/v1/customers \
  -H "Authorization: Bearer $HELIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"first_name": "Dana", "last_name": "Reyes", "email": "Dana.Reyes@example.com",
       "phone": "405-555-0134", "customer_type": "commercial", "tags": ["Spring campaign"]}'
Response 201
{
  "id": "6505eb45-3350-4c08-b6ee-45f2d2cc4319",
  "name": "Dana Reyes",
  "first_name": "Dana",
  "last_name": "Reyes",
  "company_name": null,
  "email": "dana.reyes@example.com",
  "phone": "405-555-0134",
  "customer_type": "commercial",
  "tags": ["Spring campaign"],
  "created_at": "2026-10-16T07:52:08"
}
PUT

/customers/{customer_id}/tags

Replaces the customer's tags with the list sent and answers 200 with the customer object. A tag left out of the list comes off the customer, an empty list removes every tag, and a name the company does not have yet is created. No webhook is sent.

FieldTypeDescription
tagsarray of stringsUp to 50 tag names, each 1 to 40 characters

A customer_id that is not one of the company's customers, or belongs to a removed customer, is answered 404 with "Customer not found".

Request
curl -X PUT https://app.helixfielddispatch.com/api/public/v1/customers/6505eb45-3350-4c08-b6ee-45f2d2cc4319/tags \
  -H "Authorization: Bearer $HELIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tags": ["Net 30", "VIP"]}'
GET

/jobs

The company's jobs, newest first, as job objects.

QueryDescription
limitHow many to return: 25 by default, 1 to 100
status_filterOnly jobs with this status: scheduled, in_progress, completed or cancelled. Any other value returns an empty list.
Request
curl "https://app.helixfielddispatch.com/api/public/v1/jobs?status_filter=completed&limit=10" \
  -H "Authorization: Bearer $HELIX_API_KEY"
POST

/jobs

Creates a job for an existing customer and answers 201 with the new job object. The job's status is scheduled; without a scheduled_date it has no date, and the office books it from Helix Field. A job.created webhook is sent for it. At a company with a single active user, the job is assigned to that person, the same as a job made in the office app, unless the company has turned that off in its Job Settings.

FieldTypeDescription
customer_idstring, requiredThe customer the job is for
property_idstringThe customer's property where the work is. When it is left out, the job goes to the customer's only property, or to their primary property when they have several.
titlestring, required1 to 255 characters
descriptionstringUp to 5,000 characters
job_typestringmaintenance (when left out), repair, installation or inspection
scheduled_datestringAn ISO 8601 date, or date and time. A time without an offset is the company's own clock time and is saved as written. A time with an offset, or with Z for UTC, is moved to the company's time zone (Central time unless the company has chosen another): for a company on Central time, "2026-10-20T14:00:00Z" and "2026-10-20T10:00:00-04:00" are both saved as "2026-10-20T09:00:00". A time of exactly midnight UTC, which many tools send for a date with no time, keeps its date and starts at midnight on the company's clock.
AnswerWhen
404"Customer not found": the customer is not one of the company's, or was removed.
409The customer is marked "Do not service" and the company has that mark block new work. There is no override through the API; the office can book the job in Helix Field with a reason.
422"customer_id is not a valid id", "property_id is not a valid id", "Property not found for this customer", "This customer has no property on file yet; add one in Helix first", or "job_type must be maintenance, repair, installation or inspection".
Request
curl https://app.helixfielddispatch.com/api/public/v1/jobs \
  -H "Authorization: Bearer $HELIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "6505eb45-3350-4c08-b6ee-45f2d2cc4319", "title": "Spring opening",
       "job_type": "maintenance", "scheduled_date": "2026-10-20T09:00:00", "description": "Open the pond"}'
Response 201
{
  "id": "6e704563-572e-4fc0-8826-fc5140892025",
  "job_number": "JOB-AA107B31",
  "title": "Spring opening",
  "status": "scheduled",
  "job_type": "maintenance",
  "service_id": null,
  "service_name": null,
  "customer_id": "6505eb45-3350-4c08-b6ee-45f2d2cc4319",
  "customer_name": "Dana Reyes",
  "property_id": "4bf224ef-3402-4fec-97d8-628fc57245fc",
  "scheduled_date": "2026-10-20T09:00:00",
  "scheduled_start_time": null,
  "completed_at": null
}
GET

/invoices

The company's invoices, newest first, as invoice objects.

QueryDescription
limitHow many to return: 25 by default, 1 to 100
status_filterOnly invoices with this status: draft, sent, paid or overdue. Any other value returns an empty list.
Response 200
[
  {
    "id": "6c01eb96-503e-4e40-b0ec-c588505b5f97",
    "invoice_number": "INV-1001",
    "status": "draft",
    "customer_id": "6505eb45-3350-4c08-b6ee-45f2d2cc4319",
    "customer_name": "Dana Reyes",
    "job_id": null,
    "total": "185.00",
    "paid_amount": "0.00",
    "paid_date": null,
    "issued_date": "2026-10-16T00:00:00",
    "due_date": "2026-11-15T00:00:00"
  }
]
GET

/bookings

Booking requests sent through the company's online booking form, newest first, as booking request objects. Takes limit: 25 by default, 1 to 100. Every request is listed whether the office has accepted it, declined it, marked it as spam or not answered it yet; the object does not say which.

GET

/hooks

The company's webhook subscriptions that are switched on, oldest first, made through any of its keys, as subscription objects. secret is always null here.

POST

/hooks

Subscribes a URL to one event and answers 201 with the subscription, including its secret. The secret signs every delivery; it is in this answer and nowhere else, so keep it. Each call makes a new subscription, so the same URL subscribed twice receives every delivery twice.

FieldTypeDescription
eventstring, requiredOne of the events. Any other value is 422 with "Unknown event. One of: booking.received, customer.created, invoice.paid, job.completed, job.created".
target_urlstring, required12 to 2,000 characters. It must be an https URL (otherwise "target_url must be an https URL") at a public address: localhost, loopback, .local and .internal names and private network addresses are refused with "target_url must be a public address".
Request
curl https://app.helixfielddispatch.com/api/public/v1/hooks \
  -H "Authorization: Bearer $HELIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event": "job.created", "target_url": "https://hooks.example.com/helix/jobs"}'
Response 201
{
  "id": "0cc1e630-f74f-43de-b15c-47b4db15e5a2",
  "event": "job.created",
  "target_url": "https://hooks.example.com/helix/jobs",
  "secret": "your-signing-secret-shown-only-here",
  "created_at": "2026-10-16T07:52:40.330158"
}
DELETE

/hooks/{hook_id}

Removes the subscription for good and answers 204 with no body. Nothing more is sent to it, including retries that were still owed. The answer is 204 as well when the subscription was already removed or the id is unknown, so an unsubscribe can be repeated safely.

curl -X DELETE https://app.helixfielddispatch.com/api/public/v1/hooks/0cc1e630-f74f-43de-b15c-47b4db15e5a2 \
  -H "Authorization: Bearer $HELIX_API_KEY"

Objects

The list endpoints, the create endpoints and the data of a webhook all use these shapes.

Customer

FieldTypeDescription
idstringThe customer's id
namestring or nullFirst and last name; the company name when both are empty
first_namestring
last_namestringCan be empty
company_namestring or null
emailstring or nullnull when the customer has no email
phonestring or null
customer_typestringresidential, commercial or contractor
tagsarray of stringsThe customer's tag names
created_atstringWhen the customer was added

Job

FieldTypeDescription
idstringThe job's id
job_numberstringThe number the office sees, such as JOB-AA107B31
titlestring
statusstringscheduled, in_progress, completed or cancelled
job_typestringmaintenance, repair, installation or inspection
service_idstring or nullThe service from the company's service list, when the job has one
service_namestring or nullThat service's name
customer_idstring or null
customer_namestring or nullThe customer's name
property_idstring or nullWhere the work is
scheduled_datestring or nullThe date the job is booked for
scheduled_start_timestring or nullThe booked start time as HH:MM
completed_atstring or nullWhen the crew finished the job

Invoice

FieldTypeDescription
idstringThe invoice's id
invoice_numberstringSuch as INV-1001
statusstringdraft, sent, paid or overdue
customer_idstring or null
customer_namestring or nullThe customer's name
job_idstring or nullThe job the invoice was raised from, if any
totalstringThe amount of the invoice
paid_amountstringHow much has been paid so far
paid_datestring or nullWhen the invoice was paid in full
issued_datestring
due_datestring

Booking request

FieldTypeDescription
idstringThe request's id
service_idstring or nullThe service that was asked for
service_namestringThat service's name
contact_namestringThe name the person gave
contact_emailstring or null
contact_phonestring or null
addressstring or null
citystring or null
zip_codestring or null
timing_preferencestring or nullWhen the person would like the work done, in their words
notesstring or null
created_atstringWhen the request arrived

Webhook subscription

FieldTypeDescription
idstringThe id to unsubscribe with
eventstringThe event it is for
target_urlstringWhere deliveries are posted
secretstring or nullThe signing secret, in the answer to POST /hooks only
created_atstringWhen it was made

Webhooks

Subscribe a URL with POST /hooks and Helix Field posts to it each time the event happens, in the ways the table below lists. The office can see every subscription under Settings > Integrations, with when it last sent, and can remove one there.

Events

EventSent whendata
customer.createdA customer is added by hand in Helix Field, through POST /customers, or by accepting a booking request from someone who is not already a customer. A customer made by an import does not send it, so one import does not start a Zap for every row. tags lists the tags sent with POST /customers, and is empty for a customer added in Helix Field.Customer
job.createdA job is created by hand in Helix Field, through POST /jobs, by accepting a booking request, by converting an accepted estimate, or by a season booking, which sends one for each visit it books. The visits a recurring plan makes ahead of time and a job made by an import do not send it, so a plan or an import does not start a Zap for every visit or row.Job
job.completedA job is marked completed, from the crew's phone or the office app.Job
invoice.paidThe payments on an invoice reach its total, whether by card, cash or check. An invoice that is reopened by a refund and then paid in full again is sent again.Invoice
booking.receivedSomeone sends a request through the company's online booking form.Booking request

Delivery

Each delivery is an HTTPS POST to the subscription's target_url with a JSON body:

{"data":{"company_name":null,"created_at":"2026-10-16T07:52:08","customer_type":"commercial","email":"dana.reyes@example.com","first_name":"Dana","id":"6505eb45-3350-4c08-b6ee-45f2d2cc4319","last_name":"Reyes","name":"Dana Reyes","phone":"405-555-0134","tags":["Spring campaign"]},"event":"customer.created","occurred_at":"2026-10-16T07:52:08"}
FieldDescription
eventThe event's name
occurred_atWhen it happened, in UTC
dataThe object the event is about, in the shape listed under Objects

The body is compact JSON with its keys in alphabetical order. It comes with these headers:

HeaderValue
Content-Typeapplication/json
User-AgentHelixField-Webhooks/1
X-Helix-EventThe event's name
X-Helix-DeliveryThe delivery's id. It is the same on every retry of one delivery, so it can be used to ignore a delivery already handled.
X-Helix-Signaturesha256= followed by the signature in hex

A delivery counts as received when the URL answers with any 2xx status within 10 seconds. Redirects are not followed, so a 3xx answer counts as a failure.

Verifying a signature

The signature is an HMAC SHA256 of the exact bytes of the body, keyed with the subscription's secret. Compute it over the raw body as received, before parsing it, and compare it with the header in constant time. A delivery whose signature does not match did not come from Helix Field.

Python
import hashlib
import hmac


def is_from_helix(secret: str, body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")
Node.js
const crypto = require("crypto");

function isFromHelix(secret, rawBody, signatureHeader) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Retries and turning off

  • The first attempt is made as soon as the change that caused the event is saved.
  • A failed delivery is tried again, up to five attempts in all. The waits after each failure are at least 5 minutes, 30 minutes, 2 hours and 8 hours. Retries are sent by a pass that runs once an hour, so a retry goes out on the first pass after its wait, up to about an hour later. After the fifth failed attempt that delivery is given up.
  • A URL that answers 410 Gone is asking not to be called again: its subscription is turned off at once.
  • A subscription whose deliveries fail 25 times in a row is turned off. Any delivery that succeeds sets the count back to zero.
  • A subscription is also turned off when the key that made it is revoked.
  • While a company's Helix Field account is locked, its owed retries are given up rather than kept.

A subscription that is turned off receives nothing more and no longer appears in GET /hooks. It is not turned back on; subscribe again with POST /hooks.