Developers
A documented REST API and signed webhooks
Read your organization’s reservations, fleet, squawks, members and maintenance records over HTTPS, and receive an HMAC-signed callback the moment something changes. Included on every plan — no developer tier, no per-call pricing, no application to fill in.
OpenAPI 3.1.0 · API version 1.1.0 · machine-readable document
Quickstart
Your first call, in three steps
1. Create a key in the app
Sign in at hobbstack.app, open Org Settings, and scroll to Developer / API. Name the key, tick the scopes it needs, and create it. You need the organization-settings permission, so if you do not administer the organization, ask whoever does.
The full key looks like
hbs_pk_followed by 43 URL-safe characters, and it is displayed once. Only a SHA-256 hash of it is stored, so we cannot show it to you again — if it is lost, revoke it and create another.2. Call a real endpoint
Every request carries the key as a bearer token. Base URL is
https://api.hobbstack.com.bashcurl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \ https://api.hobbstack.com/api/v1/external/aircraft3. Read the response
/aircraftanswers with a bare JSON array — one object per aircraft that has not been deleted. All timestamps across the API are ISO 8601 in UTC.json[ { "id": "b1c2d3e4-f5a6-4718-8293-a1b2c3d4e5f6", "tail_number": "N172SP", "aircraft_type": "C172S", "display_name": "172SP (G1000)", "is_active": true, "hobbs_current": 4821.6, "tach_current": 4390.2, "annual_due_date": "2027-03-31", "last_100hr_hobbs": 4780.0 } ]
Filtering and paging
Reservations is the endpoint most integrations start with, and the only one with a cursor. Filter by aircraft, pilot, status, or a start-time window.
curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
"https://api.hobbstack.com/api/v1/external/reservations?limit=50&start_after=2026-08-01T00:00:00Z"Reference
Endpoints
Seven operations, all GET, all scoped to the organization the key belongs to. There is no way to reach another organization’s data with a key, and there are no write endpoints — this API cannot change anything.
The reference below is rendered from a snapshot of the live OpenAPI document, captured on 2026-08-05 at API version 1.1.0. It is a committed file rather than a request made while this page loads, so the documentation stays readable even if the API is having a bad minute. Fetch the live document any time you want the authoritative copy.
get/api/v1/external/reservationsReservations
Reservations for the API key's org with optional filters + cursor.
- Authentication
- Required —
Authorization: Bearer hbs_pk_… - Scope
read:reservations
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
limit | integer | No | default 100, 1–500 |
after | string | No | Cursor — return reservations with id > this |
aircraft_id | string | No | — |
pilot_id | string | No | — |
status | string | No | — |
start_after | string (ISO 8601) | No | ISO datetime — only reservations starting at or after |
start_before | string (ISO 8601) | No | ISO datetime — only reservations starting before |
Request
curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
https://api.hobbstack.com/api/v1/external/reservationsResponses
200— Successful Response422— Validation Error401— missing, malformed, revoked, or unknown key.403— the key is valid but does not carry the scope above.429— a rate limit was reached.
get/api/v1/external/aircraftAircraft
All active aircraft for the API key's org.
- Authentication
- Required —
Authorization: Bearer hbs_pk_… - Scope
read:aircraft
No query parameters. Returns everything the key’s organization has.
Request
curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
https://api.hobbstack.com/api/v1/external/aircraftResponses
200— Successful Response422— Validation Error401— missing, malformed, revoked, or unknown key.403— the key is valid but does not carry the scope above.429— a rate limit was reached.
get/api/v1/external/squawksSquawks
Squawks for the API key's org. Optional status / aircraft / grounding filters.
- Authentication
- Required —
Authorization: Bearer hbs_pk_… - Scope
read:squawks
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
status | string | No | — |
aircraft_id | string | No | — |
is_grounding | boolean | No | — |
Request
curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
https://api.hobbstack.com/api/v1/external/squawksResponses
200— Successful Response422— Validation Error401— missing, malformed, revoked, or unknown key.403— the key is valid but does not carry the scope above.429— a rate limit was reached.
get/api/v1/external/membersMembers
Members for the API key's org. PII (email, phone) only included if the key carries the 'read:members.full' scope; otherwise just IDs + role + status.
- Authentication
- Required —
Authorization: Bearer hbs_pk_… - Scope
read:members
No query parameters. Returns everything the key’s organization has.
Request
curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
https://api.hobbstack.com/api/v1/external/membersResponses
200— Successful Response422— Validation Error401— missing, malformed, revoked, or unknown key.403— the key is valid but does not carry the scope above.429— a rate limit was reached.
get/api/v1/external/maintenance-logsMaintenance logs
Maintenance log entries (FAR 43.9). Useful for FAA audit exports.
- Authentication
- Required —
Authorization: Bearer hbs_pk_… - Scope
read:maintenance
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
aircraft_id | string | No | — |
Request
curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
https://api.hobbstack.com/api/v1/external/maintenance-logsResponses
200— Successful Response422— Validation Error401— missing, malformed, revoked, or unknown key.403— the key is valid but does not carry the scope above.429— a rate limit was reached.
get/api/v1/external/work-ordersWork orders
Work orders for the API key's org.
- Authentication
- Required —
Authorization: Bearer hbs_pk_… - Scope
read:maintenance
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
status | string | No | — |
Request
curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
https://api.hobbstack.com/api/v1/external/work-ordersResponses
200— Successful Response422— Validation Error401— missing, malformed, revoked, or unknown key.403— the key is valid but does not carry the scope above.429— a rate limit was reached.
get/api/v1/external/reservations.icsReservation calendar feed
iCal feed of reservations. Auth via `?token=hbs_pk_...` URL param so Apple/Google/Outlook calendar apps can subscribe (none of them support Authorization headers on subscription URLs).
- Authentication
- Required — the
tokenquery parameter carries the wholehbs_pk_…key, the same string that authenticates every other endpoint on this page. Calendar clients cannot send headers on a subscription URL, so this endpoint takes the key in the URL instead. - Scope
read:reservations
Mint a read:reservations-only key for calendar feeds
read:members.full, and every endpoint in the API if it was created with no scopes ticked. Create a dedicated key with read:reservations and nothing else for calendar subscriptions, and never paste a broad key into a calendar client. If a feed URL is ever shared or screenshotted, revoke that key rather than editing the subscription: the URL is already in the calendar provider’s subscription record and in browser history, and revoking is the only thing that takes it out of service.Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
token | string | Yes | The API key (full hbs_pk_* string) |
Request
curl "https://api.hobbstack.com/api/v1/external/reservations.ics?token=hbs_pk_YOUR_KEY"Responses
200— Successful Response422— Validation Error401— missing, malformed, revoked, or unknown key.403— the key is valid but does not carry the scope above.429— a rate limit was reached.
Field-by-field response shapes, worked examples and the result-set limits for each endpoint are on the full API reference.
Authentication
Keys and scopes
Key format
hbs_pk_ plus 43 URL-safe characters. Send it as Authorization: Bearer hbs_pk_…. Keys belong to an organization, not a person, and keep working when the person who made them leaves.
Storage
Only a SHA-256 hash of the key and its first twelve characters are stored. The admin screen can show you the prefix so you can tell keys apart; it cannot show you the key.
Scopes
A key grants exactly the scopes it was created with. Calling an endpoint without its scope returns 403 with the code INSUFFICIENT_SCOPE, naming the scope you are missing.
| Scope | Grants | Endpoints |
|---|---|---|
read:reservations | Reservations — who is flying what, when, and in which state. | /reservations /reservations.ics |
read:aircraft | The active fleet, with current Hobbs and tach and annual due date. | /aircraft |
read:squawks | Squawks — reported discrepancies, their severity and whether they ground the aircraft. | /squawks |
read:members | Membership records without personal contact details: user id, role, status, join date. | /members |
read:members.full | Adds email, first and last name, and phone to each member record.This one is additive, not standalone. /members requires read:members (external_api.py:346); a key that carries only read:members.full is rejected with 403 before the extra fields are ever considered. Grant both. | /members |
read:maintenance | FAR 43.9 maintenance log entries and work orders. | /maintenance-logs /work-orders |
A key created with no scopes ticked grants everything
read:reservations key and nothing more.Rotation
Rotating a key issues a new one with the same scopes and schedules the old one to stop working after a grace window — 24 hours by default, and up to 168 hours (7 days). During the window both keys are accepted, so an integration can be redeployed without a gap in service. After it closes, the old key answers 401.
What a rejected key looks like
Revoking is immediate and irreversible. Every authentication failure is a 401 with a JSON body carrying a human message and a stable code; branch on the code, not the message.
$ curl https://api.hobbstack.com/api/v1/external/aircraft
{"detail":"Bearer token required","code":"UNAUTHORIZED"}
$ curl -H "Authorization: Bearer hbs_pk_revoked_or_unknown_key_value_here" \
https://api.hobbstack.com/api/v1/external/aircraft
{"detail":"Invalid or revoked API key","code":"UNAUTHORIZED"}Rate limits
60 per minute, 1000 per hour
Two limits are counted per key, and whichever binds first wins. The per-minute limit exists so an hour’s allowance cannot land in a single second; the per-hour limit is the actual budget. Both are per key, so splitting a busy integration across two keys gives it two budgets.
| Limit | Window | Response when exceeded |
|---|---|---|
60 requests | per calendar minute | 429 RATE_LIMITED |
1000 requests | per calendar hour | 429 RATE_LIMITED |
Both windows are fixed calendar buckets, not sliding ones: the minute counter resets at the top of each minute and the hour counter at the top of each hour. A burst that straddles a boundary can therefore spend two windows’ worth of calls back to back.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{"detail":"Rate limit (60/min) reached for this API key","code":"RATE_LIMITED"}There are no quota headers yet
X-RateLimit-Remaining, X-RateLimit-Reset or Retry-After. The only signal that you are at the limit is the 429 itself, so pace your own calls and back off when you see one rather than reading a header that is not there. We would rather tell you that than let you write code against a header we do not send.Webhooks
Signed callbacks for 13 events
Register an https endpoint in Org Settings and Hobbstack POSTs a JSON envelope for each subscribed event. Every one of the thirteen events below fires from a real code path in the running product.
The delivery
POST /your-endpoint HTTP/1.1
Content-Type: application/json
X-Hobbstack-Event: squawk.created
X-Hobbstack-Signature: sha256=6f1b...c92a
User-Agent: Hobbstack-Webhooks/1.0
{
"event": "squawk.created",
"delivered_at": "2026-08-04T18:22:11.204871+00:00",
"data": {
"id": "9c8b7a65-4321-4fed-8cba-0987654321fe",
"aircraft_id": "b1c2d3e4-f5a6-4718-8293-a1b2c3d4e5f6",
"description": "Left main tire worn to cord on outboard shoulder",
"severity": "grounding",
"is_grounding": true,
"reported_by": "11111111-2222-4333-8444-555555555555",
"created_at": "2026-08-04T18:22:10+00:00"
}
}The envelope is always event, delivered_at and data. The signature is an HMAC-SHA256 of the raw request body, hex-encoded and prefixed with sha256=, keyed with the secret shown once when the subscription was created.
Verify the signature
Hash the bytes you received, not a re-serialised copy of the parsed JSON, and compare in constant time. Reject anything that does not match.
import hashlib
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["HOBBSTACK_WEBHOOK_SECRET"] # shown once, at creation
@app.post("/hobbstack-webhook")
def receive():
# Sign the RAW bytes. Hashing json.dumps(request.json) will not match:
# key order and separators differ from what we sent.
expected = "sha256=" + hmac.new(
SECRET.encode(), request.get_data(), hashlib.sha256
).hexdigest()
sent = request.headers.get("X-Hobbstack-Signature", "")
# Compare BYTES. compare_digest() raises TypeError on str operands that
# are not both ASCII-only, and Flask decodes header values as latin-1 —
# so a hostile header carrying any byte >= 0x80 would turn a 401 into an
# unhandled 500. errors="replace" keeps the encode itself total.
if not hmac.compare_digest(sent.encode("utf-8", "replace"), expected.encode()):
abort(401)
envelope = request.get_json()
event = envelope["event"] # also in the X-Hobbstack-Event header
data = envelope["data"]
handle(event, data)
# Answer 2xx quickly. Anything else counts as a failed attempt, and
# five consecutive failures disable the subscription.
return "", 200import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.HOBBSTACK_WEBHOOK_SECRET; // shown once, at creation
// express.raw keeps req.body as a Buffer. express.json would give you a
// parsed object, and re-serialising it produces different bytes than the
// ones we signed, so every delivery would fail verification.
app.post(
"/hobbstack-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const expected =
"sha256=" +
crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const sent = req.get("X-Hobbstack-Signature") ?? "";
const a = Buffer.from(sent);
const b = Buffer.from(expected);
// timingSafeEqual throws on a length mismatch, so check length first.
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const envelope = JSON.parse(req.body.toString("utf8"));
handle(envelope.event, envelope.data);
// Answer 2xx quickly. Anything else counts as a failed attempt, and
// five consecutive failures disable the subscription.
res.sendStatus(200);
},
);Events
| Event | Fires when |
|---|---|
squawk.created | A member or instructor reports a discrepancy against an aircraft. |
squawk.resolved | A squawk moves into the resolved status from anything else. |
squawk.escalated | An existing squawk is escalated to grounding severity. |
reservation.created | Any reservation is created, whatever state it lands in. |
reservation.confirmed | A newly created reservation lands in the confirmed state — the common case. It fires alongside reservation.created, not instead of it, with an identical payload. |
reservation.cancelled | A reservation is cancelled by a member or by staff. |
reservation.checked_in | A pilot completes the pre-flight check-in for a reservation. |
reservation.completed | A pilot completes checkout at the end of the flight. |
flight_session.completed | The same checkout. It fires immediately after reservation.completed with a byte-identical payload, for integrations that model flights rather than bookings. |
aircraft.grounded | An aircraft is taken out of service. Two different code paths fire it, and they send different fields — branch on reason, and treat every field except id and reason as optional. |
aircraft.reactivated | An aircraft is returned to service. |
mel.created | A maintenance item is deferred onto the Minimum Equipment List. |
mel.expired | A scheduled job finds a deferred MEL item past its due date. If that grounds the aircraft, a separate aircraft.grounded fires straight after with reason "mel_expired". |
A real example payload for every event, the retry and auto-disable rules, and the exact reasons a URL can be refused are on the webhooks reference.
Versioning
How changes reach you
The API is at version 1.1.0, served under the /api/v1/ prefix. No breaking change has been shipped to it. That is a small claim today, and stating the rules now is how it stays true.
- New fields can appear without notice. Ignore keys you do not recognise rather than validating responses against a closed schema. This is the one thing a client has to get right to be future-proof.
- New endpoints, query parameters and event types are additive and will not disturb an existing integration.
- Removing or renaming a field, changing a type, or changing the meaning of an existing value is a breaking change and will not happen inside
/api/v1/. It would ship under a new prefix, with the old one kept running while integrations move. - Changes are announced on the changelog. Read it or subscribe to the RSS feed. The OpenAPI document at its live URL always reflects what is deployed right now, and its
info.versionis the number to diff against.
Building something with this?
Tell us what you are integrating and what is missing — the API grows from what people actually ask for. Email support@hobbstack.com or use the contact form.
Checking whether the API is up? There is no separate status page. The health endpoint at https://api.hobbstack.com/api/health answers {"status":"ok"} when the API and its datastores are reachable, and is the honest answer until there is a real one.