StructureHub API — Developer Guide (v1.0)

Base URL: https://api.structurehub.net API Version: 2026-07-23 (this document describes this version exactly) Last verified against live endpoints: July 23, 2026

URL style: endpoints are resource-oriented — a noun for the thing you're working with, and the HTTP method (GET/POST/DELETE) conveys what you're doing to it, the same convention Stripe and Twilio use. No ?action= parameters, no file extensions in the path. POST /v1/leads creates a lead; GET /v1/leads lists them; POST /v1/leads/{id}/status changes one lead's status. Once you've seen the pattern in one resource, every other resource in this API follows it identically.


1. The Version Guarantee — Read This First

Every StructureHub API key is permanently pinned to the version of the API that was active when the key was created. Version 2026-07-23 — the one this document describes — will never change its behavior for any key pinned to it. Not the response shapes, not the required fields, not the status codes, not the validation rules.

If StructureHub ever needs to make a breaking change in the future, it ships as a new dated version. Existing integrations built against 2026-07-23 keep working exactly as documented here, forever, with zero code changes required on your end — even five years from now.

You never need to think about this in your code. It happens automatically based on when your API key was created. The only thing to know: don't ask us to modify how this version behaves for something you've already built against it — that request would become a new version instead, and your key would keep working on the old one unless you deliberately migrate.


2. Getting Started — Registering a New Website as an "App"

Every website, tool, or system that talks to StructureHub is called an app. Each app gets its own unique API key. A dealer's website, their inventory dashboard, and their CRM portal could each be separate apps, or one — that's your call.

Step 1: Register the app

You need an existing StructureHub account with management rights over the organization (dealer) the new app belongs to — either Defyned staff, or someone with admin rights on that dealer's account.

POST https://api.structurehub.net/v1/apps
Header: X-App-Key: <an existing, authorized key>
Body (JSON):
{
  "organization_id": 8205,
  "app_name": "Yoder Cabins Website"
}

Real response, exactly as returned:

{
  "ok": true,
  "app_id": 12,
  "app_key": "31d89ca1dffd8aaafc1cd155ede3ebd61303061f93d9ec92",
  "pinned_api_version": "2026-07-23",
  "note": "Save this key now — it is shown only once and cannot be retrieved again. Store it securely and never expose it in client-side/browser code."
}

This is critical: the app_key value is shown exactly once, at creation. StructureHub never stores it in a retrievable form — only a one-way hash. If it's lost, the only fix is to revoke the app and register a new one. Store it in a server-side secret store or environment variable, never in a .js file, never in a public repo, never anywhere a browser can read it.

Step 2: Use the key

Every request to StructureHub (except public read endpoints — see §7) needs this header:

X-App-Key: 31d89ca1dffd8aaafc1cd155ede3ebd61303061f93d9ec92

That's the entire authentication mechanism. No OAuth handshake, no expiring tokens to refresh. The key identifies your app, which organization it belongs to, and (invisibly) which API version it's pinned to.

Managing apps

GET    /v1/apps?organization_id=8205
DELETE /v1/apps/{app_id}

GET shows every app under an organization (key prefix only — never the full key). DELETE permanently disables an app's key.


3. Response Format — Every Endpoint Follows This

Success:

{ "ok": true, ...endpoint-specific fields... }

Error:

{
  "ok": false,
  "error": {
    "type": "invalid_request_error",
    "code": "missing_organization_id",
    "message": "organization_id is required"
  }
}

type is a broad category your code can branch on without parsing English: | type | meaning | typical HTTP status | |---|---|---| | invalid_request_error | something about your request was malformed or missing | 400 | | authentication_error | missing or invalid X-App-Key | 401 | | permission_error | valid key, but not authorized for this action | 403 | | not_found_error | the thing you referenced doesn't exist | 404 | | rate_limit_error | too many requests, slow down | 429 | | idempotency_error | a request with this idempotency key is already being processed | 409 | | api_error | something broke on our end | 500 |

code is a stable, specific machine-readable string (e.g. missing_organization_id) — safe to match on in code. message is human-readable and may change wording over time; don't parse it.


4. Submitting Lead Data (the core use case)

This is the endpoint a website's contact/inquiry form should call.

POST https://api.structurehub.net/v1/leads
Header: X-App-Key: <your app's key>
Body (JSON):
{
  "organization_id": 8205,
  "contact_name": "John Bennington",
  "contact_email": "john@example.com",
  "contact_phone": "555-0100",
  "lead_source": "web_form",
  "about_shin": 1103549,
  "notes": "Interested in the 12x20 cabin",
  "contact_city": "Nicholasville",
  "contact_state": "KY"
}
field required? notes
organization_id yes which dealer this lead belongs to
contact_name yes
contact_email at least one of email/phone validated as a real email format
contact_phone at least one of email/phone
lead_source no, defaults to web_form one of landing_page, phone_call, text_message, web_form, import, other
about_shin no if the inquiry is about a specific piece of inventory
notes no
contact_city / contact_state no

Real success response:

{ "ok": true, "lead_id": 20, "is_likely_duplicate": false }

What happens automatically on submission — none of this requires extra API calls from you: - The lead is stored, with the contact's identity handled per our privacy architecture (see §9) - A lead.created webhook event fires (see §5) — for anything you've subscribed to it - Every configured alert recipient for that organization gets notified immediately (email live now; text/WhatsApp routes correctly but has no live provider yet) - Basic spam is rejected (obvious bot patterns, link-stuffed submissions) with a clean 422 error - If the same email/phone submitted to the same organization in the last 24 hours, this submission is still stored but flagged is_likely_duplicate: true and does not trigger a second alert — the dealer already knows

Honeypot field (recommended, optional)

Include a hidden form field named website_field_hp that real visitors never see or fill in (e.g., display:none in CSS — never type="hidden", which some bots skip). If it arrives non-empty, StructureHub returns a normal-looking success ({"ok": true, "lead_id": null}) but silently creates nothing. This keeps bots from learning to route around the check.

Extra fields (anything you send that we don't explicitly recognize)

Real forms ask questions StructureHub can't anticipate — a delivery date preference, a budget range, whatever's specific to a client's site. Just include them in the request. Anything that isn't one of the known fields above is automatically preserved and shown to the dealer, no special wrapper or advance coordination needed:

{
  "organization_id": 8205,
  "contact_name": "John Bennington",
  "contact_email": "john@example.com",
  "preferred_delivery_date": "2026-09-15",
  "budget_range": "8000-12000",
  "wants_electrical": true
}

Those three extra fields come back exactly as sent when the dealer views the lead — types preserved (wants_electrical stays a real boolean, not a string).

Attaching files (photos, PDFs)

Once a lead exists, attach one or more files to it — a photo of the property, a signed document, whatever the customer sent:

POST /v1/leads/{lead_id}/attachments
Multipart form: files[]=<file1>, files[]=<file2>, ...

Accepts images (jpeg/png/webp/gif) and PDFs, up to 30 files per call, 25MB each. Responds almost immediately regardless of how many files — measured live with 20 full-size real photos in one call: 0.79 seconds, all 20 succeeded. There's no waiting for processing; images are stored and served directly (no multi-size resize pipeline here, unlike inventory photos — a lead attachment just needs to be viewable, not displayed at several sizes in a public gallery). PDFs and other non-image files are stored privately, not served through a public URL.

GET  /v1/leads?organization_id=8205&lead_status=new
POST /v1/leads/{lead_id}/status     body: {"lead_status": "won"}

lead_status is one of: new, contacted, qualified, won, lost, bad_lead. These endpoints require the calling app's key to belong to a user with management rights over that organization — a dealer only ever sees their own leads, never another dealer's.


5. Receiving Events (Webhooks)

Instead of polling, register a URL and StructureHub will POST to it the moment something happens.

POST /v1/webhooks
Body: { "target_url": "https://yoursite.com/webhooks/structurehub", "event_types": ["lead.created", "lead.status_changed"] }

Response (save the secret — shown once):

{
  "ok": true,
  "webhook_endpoint_id": 1,
  "signing_secret": "aa22bab6...",
  "note": "Save this signing secret now — verify it against the X-StructureHub-Signature header on every delivery to confirm it genuinely came from us."
}

Every delivery to your endpoint looks like this:

POST https://yoursite.com/webhooks/structurehub
X-StructureHub-Signature: <hex HMAC-SHA256>
X-StructureHub-Event-Id: <event id>
Content-Type: application/json

{"id": 123, "type": "lead.created", "data": {"lead_id": 20, "organization_id": 8205, "contact_name": "John Bennington", "lead_source": "web_form", "is_likely_duplicate": false}}

Verify every delivery by recomputing the signature yourself and comparing:

expected_signature = hex(HMAC-SHA256(raw_request_body, your_signing_secret))

Reject anything that doesn't match — that's what proves it genuinely came from StructureHub and wasn't spoofed.

Currently emitted events: lead.created, lead.status_changed, communication.sent, communication.failed. Your endpoint has 15 seconds to respond. Anything other than a 2xx is retried automatically with backoff (roughly: 1 min, 5 min, 30 min, 2 hr, 12 hr — 6 attempts total before giving up).

GET    /v1/webhooks
DELETE /v1/webhooks/{webhook_endpoint_id}

6. Configuring Alerts

Instead of a single hardcoded contact, an organization can have any number of alert recipients across channels.

POST /v1/alert-configs
Body: { "organization_id": 8205, "trigger_event": "lead.created", "channel": "email", "destination": "sales@yodercabins.com" }

channel is email (live now), text_message or whatsapp (routing works, no live provider connected yet — will start working automatically the moment one is). If nothing is configured for an org, StructureHub falls back to that organization's primary contact email, so a new dealer is never silently unalerted.

If a channel fails 3 times in a row, it's automatically disabled and an internal notice goes out — it stops wasting attempts on a broken destination rather than retrying forever.

GET    /v1/alert-configs?organization_id=8205
DELETE /v1/alert-configs/{alert_config_id}

7. Inventory & Locations (public-facing website data)

These power a dealer's own public pages — no X-App-Key required, these are public reads.

Locations:

GET /v1/locations?organization_id=8205

Filterable inventory search (GET, query params):

GET /v1/inventory?organization_id=8205&location_id=3&min_price=5000&max_price=8000&main_color=Beige&style=Cabin&limit=24&offset=0

Filterable by: location_id, min_price, max_price, availability_status, and any attribute on the listing card — width, length, main_color, roof_color, trim_color, door_color, style, siding.

Response: {"ok": true, "total": 261, "limit": 24, "offset": 0, "results": [ ...listing cards... ]}

Single listing detail (with full photo gallery):

GET /v1/inventory/{shin}

Managing inventory (write, requires X-App-Key + management rights over the dealer org):

POST /v1/inventory                    — create: allocates a new SHIN, creates as an unpublished draft
POST /v1/inventory/{shin}             — update attributes/price/description (never overwrites history)
POST /v1/inventory/{shin}/publish     — make it visible on the public site
POST /v1/inventory/{shin}/unpublish
POST /v1/inventory/{shin}/mark-sold

Photo upload — one at a time, or many at once:

POST /v1/inventory/{shin}/photos            — one image
POST /v1/inventory/{shin}/photos/bulk       — many at once, multipart form: files[]=<file1>, files[]=<file2>, ...

Responds almost immediately (measured: ~0.8 seconds for 4 full-size photos in one bulk call) — resizing happens automatically afterward, not synchronously in the request.


8. Idempotency

For any write request, add an Idempotency-Key header with a unique value your code generates per logical operation (a UUID is fine):

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

If the same key is sent twice — a retry after a timeout, a double-click — StructureHub returns the exact original response instead of doing the work again. Safe to always include on writes; costs nothing if you don't need it.


9. Privacy & Identity (good to know, not usually something you call directly)

Contact identity (name/email/phone) is never duplicated across StructureHub's tables — it lives in exactly one place per person. This is what lets a privacy/erasure request be resolved cleanly. As an integrator, you don't need to do anything differently — just submit real contact info normally; StructureHub handles the architecture behind it.


10. Rate Limits

120 requests per minute per app key. If exceeded, you'll get a 429 with type: rate_limit_error. This is generous for real usage; if a legitimate integration needs more, ask and it can be raised for that specific key.


11. Quick Reference — All Endpoints

Endpoint Auth Purpose
POST /v1/apps · GET /v1/apps · DELETE /v1/apps/{id} Key + org rights Register/list/revoke apps
POST /v1/organizations · POST /v1/organizations/{id}/parent Key + org rights Create/reparent dealer organizations
POST /v1/organizations/{id}/members · DELETE .../members/{user_id} Key + org rights Grant/revoke user roles on an org
GET /v1/locations · POST /v1/locations · POST /v1/locations/{id} Public read / Key for write Manage a dealer's physical locations
GET /v1/inventory Public Filterable inventory search
GET /v1/inventory/{shin} Public Single listing detail + gallery
POST /v1/inventory · POST /v1/inventory/{shin} · .../{shin}/publish · .../{shin}/unpublish · .../{shin}/mark-sold Key + org rights Create/update/publish/mark_sold inventory
POST /v1/inventory/{shin}/photos · .../photos/bulk Key Upload one or many images
POST /v1/leads · GET /v1/leads · POST /v1/leads/{id}/status Key (submit is app-only; list/update need org rights) Submit/view/update leads
POST /v1/leads/{id}/attachments Key Attach files (images/PDFs) to a lead
POST /v1/alert-configs · GET /v1/alert-configs · DELETE .../{id} Key + org rights Configure who gets alerted, on what channel
POST /v1/webhooks · GET /v1/webhooks · DELETE /v1/webhooks/{id} Key Register to receive real-time events
POST /v1/messages Key Send an email/text directly

Questions or requests to change how an existing endpoint behaves should go through Defyned SEO — remember, any change to v1.0's documented behavior becomes a new version, never a silent change to this one.