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.
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.
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.
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.
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.
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.
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.
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
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.
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).
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.
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}
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}
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.
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.
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.
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.
| 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.