Skip to main content
Hobbstack

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. 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. 2. Call a real endpoint

    Every request carries the key as a bearer token. Base URL is https://api.hobbstack.com.

    bash
    curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
      https://api.hobbstack.com/api/v1/external/aircraft
  3. 3. Read the response

    /aircraft answers 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.

bash
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

NameTypeRequiredNotes
limitintegerNodefault 100, 1–500
afterstringNoCursor — return reservations with id > this
aircraft_idstringNo
pilot_idstringNo
statusstringNo
start_afterstring (ISO 8601)NoISO datetime — only reservations starting at or after
start_beforestring (ISO 8601)NoISO datetime — only reservations starting before

Request

curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
  https://api.hobbstack.com/api/v1/external/reservations

Responses

  • 200 Successful Response
  • 422 Validation Error
  • 401 — 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/aircraft

Responses

  • 200 Successful Response
  • 422 Validation Error
  • 401 — 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

NameTypeRequiredNotes
statusstringNo
aircraft_idstringNo
is_groundingbooleanNo

Request

curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
  https://api.hobbstack.com/api/v1/external/squawks

Responses

  • 200 Successful Response
  • 422 Validation Error
  • 401 — 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/members

Responses

  • 200 Successful Response
  • 422 Validation Error
  • 401 — 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

NameTypeRequiredNotes
aircraft_idstringNo

Request

curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
  https://api.hobbstack.com/api/v1/external/maintenance-logs

Responses

  • 200 Successful Response
  • 422 Validation Error
  • 401 — 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

NameTypeRequiredNotes
statusstringNo

Request

curl -H "Authorization: Bearer hbs_pk_YOUR_KEY" \
  https://api.hobbstack.com/api/v1/external/work-orders

Responses

  • 200 Successful Response
  • 422 Validation Error
  • 401 — 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 token query parameter carries the whole hbs_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

The token in this URL is a full API key, not a feed-only token. Anyone who has the URL has everything that key can read — member email addresses and phone numbers if it carries 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

NameTypeRequiredNotes
tokenstringYesThe 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 Response
  • 422 Validation Error
  • 401 — 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.

ScopeGrantsEndpoints
read:reservationsReservations — who is flying what, when, and in which state./reservations /reservations.ics
read:aircraftThe active fleet, with current Hobbs and tach and annual due date./aircraft
read:squawksSquawks — reported discrepancies, their severity and whether they ground the aircraft./squawks
read:membersMembership records without personal contact details: user id, role, status, join date./members
read:members.fullAdds 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:maintenanceFAR 43.9 maintenance log entries and work orders./maintenance-logs /work-orders

A key created with no scopes ticked grants everything

The scope check treats an empty scope list as read-all, for compatibility with keys issued before scopes existed. If you untick every box in the key creator you get a key that can read every endpoint, which is almost certainly not what you meant. Always tick the scopes you actually need. This matters most for the calendar feed, where the key travels in a URL you hand to Google or Apple: give that one its own 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.

bash
$ 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.

LimitWindowResponse when exceeded
60 requestsper calendar minute429 RATE_LIMITED
1000 requestsper calendar hour429 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.

429 response
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

The API does not currently return 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

What arrives at your endpoint
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.

Python (Flask)
python
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 "", 200
Node (Express)
javascript
import 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

EventFires when
squawk.createdA member or instructor reports a discrepancy against an aircraft.
squawk.resolvedA squawk moves into the resolved status from anything else.
squawk.escalatedAn existing squawk is escalated to grounding severity.
reservation.createdAny reservation is created, whatever state it lands in.
reservation.confirmedA 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.cancelledA reservation is cancelled by a member or by staff.
reservation.checked_inA pilot completes the pre-flight check-in for a reservation.
reservation.completedA pilot completes checkout at the end of the flight.
flight_session.completedThe same checkout. It fires immediately after reservation.completed with a byte-identical payload, for integrations that model flights rather than bookings.
aircraft.groundedAn 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.reactivatedAn aircraft is returned to service.
mel.createdA maintenance item is deferred onto the Minimum Equipment List.
mel.expiredA 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.version is 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.