The Structify API
Pull projects, defects, reports and file details from Structify into your own systems, and create defects from your systems. This page describes exactly what the interface offers today.
As of 9 October 2026 · Version v1
Overview
The Structify API is a REST interface: you send HTTPS requests and receive JSON. Every API key belongs to exactly one organization and only reaches that organization's data.
- Protocol
- HTTPS, requests and responses as JSON (UTF-8)
- Version
- v1, part of every request path
- Sign-in
- API key in the X-Api-Key header
- Areas
- Projects, defects, reports, project file details, webhooks; plus legacy entries of the former site diary
- Limit
- 1,000 requests per hour per key
Note Only what works today.
This documentation only describes functions that are available today. What is missing (so far) is listed openly in the section "What the API cannot do today".
Access & permissions
To connect, you need an API key that your management creates in the web app: Settings → Integrations → API & third parties (Einstellungen → Integrationen → API & Drittanbieter).
Important Only management manages keys.
In the web app, API keys and webhooks are created exclusively by members of management; that is also where they deactivate keys and delete webhooks. A key is valid only as long as the person who created it is an active member of your organization's management. If that person leaves or changes role, the key stops working immediately.
Caution A key works across the organization.
A key is not limited to projects or to the permissions of individual people: it reaches all projects of your organization in the areas the API offers, regardless of project membership. A single retrieval of a project or defect returns all stored fields, including fee, budget and billing details, the client's contact person, internal notes and contact details (see "Available endpoints"). Issue keys sparingly and only for systems you trust with this data.
Permissions of a key
When you create a key, you choose what it may do. The server checks these permissions on every request:
| Permission | Unlocks |
|---|---|
| Any valid key | Read projects, defects, reports and legacy entries of the former site diary; list and delete webhooks |
| Read (Lesen) or Read documents (Dokumente lesen) | Retrieve project file details |
| Write tickets (Tickets schreiben) | Create and update defects; register webhooks |
The option "Export invoices" (Rechnungen exportieren) offered in the interface does not unlock any endpoint today.
Authentication
Send the key with every request in the X-Api-Key header. Alternatively, the API accepts it as a bearer token in the Authorization header.
X-Api-Key: <YOUR_API_KEY>Authorization: Bearer <YOUR_API_KEY>- Keys start with sk_. Structify shows a key only once, right after it is created. Copy it immediately.
- Only a check value (SHA-256) and the first characters for recognition are stored, not the key itself. Nobody can display it in full later; if you lose it, create a new key and deactivate the old one.
- When you deactivate a key in the settings, it becomes invalid immediately.
Caution Keys belong on the server.
Never use API keys in browser or app code, in source code repositories or in URLs. Store them, for example, as an environment variable on your server.
Base URL
Append every path on this page to this base URL:
https://api.structify.solutions/functions/v1/public-api- Example: …/public-api/v1/projekte
- HTTPS only. The version (v1) is part of the path.
- Identifiers (id) are UUIDs; timestamps use ISO 8601 with a time zone, date fields use YYYY-MM-DD.
- Field names and values are passed through from Structify unchanged, so some field names and many values are German; error messages are in German. Exception: if registering a webhook fails in the database, the message may contain English database text.
Sample request
This is how you retrieve the first two projects of your organization:
curl "https://api.structify.solutions/functions/v1/public-api/v1/projekte?limit=2" \
-H "X-Api-Key: <YOUR_API_KEY>"const response = await fetch(
"https://api.structify.solutions/functions/v1/public-api/v1/projekte?limit=2",
{ headers: { "X-Api-Key": process.env.STRUCTIFY_API_KEY } }
);
const body = await response.json();
if (!response.ok) throw new Error(`${body.status}: ${body.error}`);
const { data, total } = body;{
"data": [
{
"id": "<PROJECT_ID>",
"project_name": "Neubau Bürogebäude Musterstraße",
"project_number": "2026-014",
"client_id": "<CLIENT_ID>",
"clients": {
"name": "Muster GmbH"
},
"project_type": "Neubau",
"current_phase": "LP5",
"status": "aktiv",
"priority": "normal",
"construction_start": "2026-03-02",
"construction_end": "2027-06-30",
"created_at": "2026-01-15T09:12:44.512+00:00"
}
],
"total": 12,
"limit": 2,
"offset": 0
}Lists return data (entries), total (total number of matches), limit and offset, with one exception: GET /v1/webhooks returns data only. Single retrievals return data with all fields of the record.
Available endpoints
These endpoints work today. Paths are relative to the base URL; {id} stands for the identifier of the record.
| Endpoint | Purpose | Permission |
|---|---|---|
GET /v1/projekte | List projects | any valid key |
GET /v1/projekte | One project with all fields | any valid key |
GET /v1/tickets | List defects | any valid key |
GET /v1/tickets | One defect with all fields | any valid key |
POST /v1/tickets | Create a defect | Write tickets |
PUT /v1/tickets | Update a defect (for example its status) | Write tickets |
GET /v1/protokolle | List reports | any valid key |
GET /v1/bautagebuch | List legacy entries of the former site diary | any valid key |
GET /v1/dokumente | List project file details | Read or Read documents |
GET /v1/webhooks | List webhooks | any valid key |
POST /v1/webhooks | Register a webhook | Write tickets |
DELETE /v1/webhooks | Delete a webhook | any valid key |
Important Defects, reports and site diary: which data is meant.
The path /v1/tickets works with the defects from the project area "Defect Management" (Mangelmanagement). Entries from "Project Tickets" (Projekttickets) and warranty defects from "Warranty Defects" (Gewährleistungsmangel) cannot be reached through the API today. /v1/protokolle returns only the reports and minutes from the document assistant (page "Documents" (Dokumente) → "New document" (Neues Dokument)). It does not include documents created from templates in the project area "Protocols & Forms" (Protokolle und Formulare), even though the button there is also called "New document", nor the meeting minutes from "Basic Assessment" (Grundlagenermittlung), "Planning Meeting" (Planungsgespräch) and "Site Meeting" (Baustellengespräch). /v1/bautagebuch returns only legacy entries of the former site diary area, for which there is no way to record entries today. Site diary entries that you create today as a project ticket of the type "Site diary entry" (Bautagebuch-Eintrag) belong to the "Project Tickets" and are not included.
Note Short overview at GET /v1.
GET /v1 returns a short list of paths. It also names POST /v1/projekte and GET /v1/suche; neither can be used today, and POST /v1/projekte currently answers with 403 (see "What the API cannot do today").
Projects
- GET
/v1/projekteList projects - GET
/v1/projekte/{id}One project with all fields
Read projects as a list (newest first) or individually. The list contains the fields named below. A single retrieval, however, returns all stored fields of the project, including fee, budget and billing details (such as fees, hourly rates, discounts and invoice recipients), the client's contact person and address, internal notes and other stored contact details. Creating and updating projects is not offered by the API today.
- Fields in the list
id, project_name, project_number, client_id, clients.name (client), project_type, current_phase, status, priority, construction_start, construction_end, created_at- Filters and pages
- status (vorbereitung, aktiv, pausiert, abgeschlossen, archiviert); limit (default 20, at most 100), offset
Defects (/v1/tickets)
- GET
/v1/ticketsList defects - GET
/v1/tickets/{id}One defect with all fields - POST
/v1/ticketsCreate a defect - PUT
/v1/tickets/{id}Update a defect (for example its status)
Read defects as a list (newest first) or individually, create them and update them. A single retrieval returns all stored fields of the defect, including the contact e-mail, the responsible company and notes; the same applies to the responses to create and update and to webhook messages. When creating, building_location (location, component) and description are required. project_id is optional but must belong to your organization. When updating, id and project_id remain unchanged. Create and update return the stored record without the embedded project name. You assign the defect number (defect_number) yourself: the API does not assign one and does not check whether a number is already in use; if you omit it, the field stays empty (null).
- Fields in the list
id, defect_number, building_location, description, status, severity, discipline, remediation_deadline, project_id, created_at, projects.project_name- Filters and pages
- status, projekt_id, severity (also prioritaet), discipline (also gewerk); limit (default 50, at most 100), offset
- Allowed values
- status (default offen):
offen, in_behebung, behoben, abgenommen, verjährt - severity (default wesentlich):
unwesentlich, wesentlich, erheblich, sicherheitsrelevant - discipline (optional):
Heizung, Lüftung, Sanitär, Elektro, MSR, Sonstiges - remediation_deadline is a date (YYYY-MM-DD).
- status (default offen):
Reports
- GET
/v1/protokolleList reports
Reports and minutes from the document assistant (page "Documents" → "New document") as a list (newest first). Read only. The type (session_type) is one of: Baubesprechung, Planungsbesprechung, Abnahmebegehung, Mängelbegehung, Abstimmungsgespräch, Telefonnotiz, Aktennotiz, Inbetriebnahmeprotokoll, Übergabeprotokoll.
- Fields in the list
id, title, session_type, status (entwurf, in_bearbeitung, freigabe_ausstehend, freigegeben, final, versendet), created_at, projects.project_name- Filters and pages
- projekt_id; limit (default 20, at most 100), offset
Site diary (former area)
- GET
/v1/bautagebuchList legacy entries of the former site diary
Legacy entries of the former site diary area as a list, by date descending. Read only. There is no way to record entries in this area today, so the list may be empty. Site diary entries that you create today as a project ticket of the type "Site diary entry" (Bautagebuch-Eintrag) are not returned by this path.
- Fields in the list
id, datum, wetter_typ, temperatur_grad, freitext, taetigkeiten (list), arbeitskraefte (list with firma, gewerk, anzahl), projects.project_name- Filters and pages
- projekt_id; limit (default 20, at most 100), offset
Project files
- GET
/v1/dokumenteList project file details
Details of the files in the project area "Document database" (Dokumentdatenbank), newest first; deleted files are not included. Plans and photos from the photo gallery are not included. The API does not download the files themselves today.
- Fields in the list
id, dateiname, dateityp, dateigroesse, storage_path, ordner_id, projekt_id, hochgeladen_am, hochgeladen_von, projects.project_name- Filters and pages
- projekt_id, ordner_id; limit (default 20, at most 100), offset
Webhooks
- GET
/v1/webhooksList webhooks - POST
/v1/webhooksRegister a webhook - DELETE
/v1/webhooks/{id}Delete a webhook
List registered webhooks, register new ones and delete them. When registering, name and url are required; events is the list of the events you want. Without events, the webhook receives no messages. The API does not check the names in events: an unknown name is stored but never triggers a message. Details in the Webhooks section.
- Fields in the list
id, name, url, events, ist_aktiv, letzter_versuch, letzte_antwort- Filters and pages
- none; the list contains all webhooks of your organization
Core concepts
- Organization
- Every key belongs to exactly one organization. Records of other organizations are not visible: single retrievals with a foreign identifier return 404.
- Paging
- limit sets the number of entries per response, offset the number of skipped entries. total gives the overall count, so you page until offset + limit ≥ total.
- Sorting
- Lists have a fixed order: newest first (former site diary by date descending; webhooks in no fixed order). The API does not offer custom sorting.
- Filters
- Filters compare for exact equality. A value that does not exist returns an empty list; an invalid identifier in a filter returns 400.
- Responses
- List: data, total, limit, offset (webhooks: data only) · Single retrieval: data · Create and update: data and message · Delete: message.
- Methods
- Reports, the former site diary and project files are read only. Send only GET to them: the API does not reject other methods there today but answers them like GET with the list; nothing is created or changed.
Rate limiting
Each key can make 1,000 requests per hour. The count covers the most recent 60 minutes, including requests that end with an error inside an endpoint (for example 400 or 404).
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997When the limit is reached, the API responds with status 429:
{
"error": "Rate Limit überschritten (1000/h)",
"status": 429
}Important No reset time.
The API does not state when the limit frees up again and only sends the headers with successful responses. After a 429 response, wait a few minutes and extend the wait with every further 429 response.
Errors & status codes
Errors that the API itself detects are answered with a matching HTTP status and this JSON:
{
"error": "API-Key fehlt. Header: X-Api-Key: sk_...",
"status": 401
}The status code is what counts. The message text is in German and may change, so do not evaluate it programmatically.
| Status | Meaning |
|---|---|
200 | Success. |
400 | Invalid request: a required field is missing, a value is not allowed (for example an unknown status), an identifier in a filter is invalid, the record to be updated does not exist, the method is not intended for this path (projects, defects, webhooks), or the request could not be processed. |
401 | Key missing, invalid or deactivated. |
403 | Permission missing for this action; the person who created the key no longer belongs to management; or their consent to new legal texts is still outstanding. |
404 | Unknown path or record not found; for single retrievals also for invalid identifiers and identifiers of other organizations. |
429 | Rate limit reached. |
503 | The permission could not be checked right now. Please try again later. |
In case of operational disruptions, for example 502 or 504 after a timeout, the response may contain different JSON or none at all. In that case evaluate only the status and try again later. Before repeating POST /v1/tickets, check with GET whether the defect was created after all.
Webhooks
A webhook notifies your system of an event by HTTP POST to an address you specify. Today Structify sends exactly two events:
| Event | Triggered when |
|---|---|
ticket.erstellt | a defect is created via POST /v1/tickets |
ticket.status_geaendert | PUT /v1/tickets/{id} contains the status field, even if the value does not change |
Important Only changes made through the API itself.
Defects created or changed in the web app or in Structify on iPhone & Android do not trigger a webhook today. Further events that can be selected in the settings are currently not sent.
Structure of a message
data contains the stored record of the defect with all fields (shortened here; this includes the contact e-mail, the responsible company and notes), timestamp the time of sending.
POST /structify-webhook HTTP/1.1
Host: your-company.example
Content-Type: application/json
X-Structify-Signature: sha256=<HEX>{
"event": "ticket.status_geaendert",
"data": {
"id": "<DEFECT_ID>",
"organization_id": "<ORGANIZATION_ID>",
"project_id": "<PROJECT_ID>",
"defect_number": "M-ERP-001",
"building_location": "2. OG, Raum 2.14",
"description": "Brandschutzklappe ohne Revisionsöffnung",
"status": "behoben",
"severity": "erheblich",
"discipline": "Lüftung",
"remediation_deadline": "2026-11-15",
"created_at": "2026-10-08T07:41:03.118+00:00"
},
"timestamp": "2026-10-09T13:05:12.402Z"
}Delivery
- Structify sends the message while the triggering API request is being processed. A slow receiver therefore delays that request's response.
- Time limit of 5 seconds per receiver; at most 10 webhooks per event. If more than 10 webhooks are registered for an event, which 10 are delivered is not defined.
- No retry on errors. GET /v1/webhooks shows the time and HTTP status of the last attempt (letzter_versuch, letzte_antwort; 0 means no response).
- Use HTTPS for the target address.
Verify the signature
The X-Structify-Signature header contains sha256= followed by the HMAC-SHA256 of the unmodified request body, computed with the webhook's signing secret. Compare in constant time:
import crypto from "node:crypto";
// rawBody: the unmodified request body, not re-serialized
export function isValidSignature(rawBody, signatureHeader, signingSecret) {
const expected = "sha256=" + crypto
.createHmac("sha256", signingSecret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Important Signing secret only when registering through the API.
You receive the signing secret once, in the response to POST /v1/webhooks (field signing_secret; the same value also appears there in the stored record as data.secret, so do not log this response). Webhooks you create in the web app settings do not show their signing secret today, so register webhooks through the API if you want to verify the signature.
The signature does not include a timestamp. If you want to reject replayed messages, also check the timestamp field.
Tutorials
First request in five steps
- Sign in to the web app as a member of management and open Settings → Integrations → API & third parties.
- Create a key with a descriptive name (for example "ERP connection") and select only the permissions your system needs.
- Copy the key immediately (it is shown only once) and store it on your server, for example as the environment variable STRUCTIFY_API_KEY.
- Call GET /v1/projekte as shown in the Sample request section.
- Check the response: status 200, your projects in data and the remaining requests in X-RateLimit-Remaining.
Create and track defects from your system
You need a key with the Write tickets permission. Create the defect and keep the returned id:
curl -X POST "https://api.structify.solutions/functions/v1/public-api/v1/tickets" \
-H "X-Api-Key: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"project_id":"<PROJECT_ID>","defect_number":"M-ERP-001","building_location":"2. OG, Raum 2.14","description":"Brandschutzklappe ohne Revisionsöffnung","severity":"erheblich","discipline":"Lüftung","remediation_deadline":"2026-11-15"}'{
"data": {
"id": "<DEFECT_ID>",
"organization_id": "<ORGANIZATION_ID>",
"project_id": "<PROJECT_ID>",
"defect_number": "M-ERP-001",
"building_location": "2. OG, Raum 2.14",
"description": "Brandschutzklappe ohne Revisionsöffnung",
"status": "offen",
"severity": "erheblich",
"discipline": "Lüftung",
"remediation_deadline": "2026-11-15",
"created_at": "2026-10-08T07:41:03.118+00:00"
},
"message": "Ticket erstellt"
}Later you set the status, which triggers the ticket.status_geaendert event:
curl -X PUT "https://api.structify.solutions/functions/v1/public-api/v1/tickets/<DEFECT_ID>" \
-H "X-Api-Key: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"status":"behoben"}'Retrieve the open defects of a project, filtered and page by page:
curl "https://api.structify.solutions/functions/v1/public-api/v1/tickets?projekt_id=<PROJECT_ID>&status=offen&limit=50&offset=0" \
-H "X-Api-Key: <YOUR_API_KEY>"{
"data": [
{
"id": "<DEFECT_ID>",
"defect_number": "M-ERP-001",
"building_location": "2. OG, Raum 2.14",
"description": "Brandschutzklappe ohne Revisionsöffnung",
"status": "offen",
"severity": "erheblich",
"discipline": "Lüftung",
"remediation_deadline": "2026-11-15",
"project_id": "<PROJECT_ID>",
"created_at": "2026-10-08T07:41:03.118+00:00",
"projects": {
"project_name": "Neubau Bürogebäude Musterstraße"
}
}
],
"total": 1,
"limit": 50,
"offset": 0
}Set up a webhook and verify the signature
Register the webhook through the API and store signing_secret from the response securely on your server:
curl -X POST "https://api.structify.solutions/functions/v1/public-api/v1/webhooks" \
-H "X-Api-Key: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"ERP-Anbindung","url":"https://your-company.example/structify-webhook","events":["ticket.erstellt","ticket.status_geaendert"]}'{
"data": {
"id": "<WEBHOOK_ID>",
"name": "ERP-Anbindung",
"url": "https://your-company.example/structify-webhook",
"events": [
"ticket.erstellt",
"ticket.status_geaendert"
],
"ist_aktiv": true
},
"message": "Webhook registriert",
"signing_secret": "<SIGNING_SECRET>"
}Verify the signature of every incoming message (see the Webhooks section) and respond within 5 seconds with a 2xx status.
What the API cannot do today
So that you can plan reliably, we state openly what the interface does not offer today:
- Read or write entries from "Project Tickets" (Projekttickets) or warranty defects from "Warranty Defects" (Gewährleistungsmangel).
- Read or write site diary entries that you create today as a project ticket of the type "Site diary entry" (Bautagebuch-Eintrag). GET /v1/bautagebuch returns only legacy entries of the former site diary area.
- Read documents created from templates in the project area "Protocols & Forms" (for example notices of defects, notices of obstruction or minutes), or meeting minutes from "Basic Assessment", "Planning Meeting" and "Site Meeting".
- Assign defect numbers automatically.
- Create or update projects; projects are read only.
- A full-text search across projects.
- Download or upload files; transfer photos, plans and attachments.
- Retrieve details of plans or of photos from the photo gallery; /v1/dokumente covers only the document database.
- Create or update reports, entries of the former site diary or file details.
- Retrieve users, roles and members; retrieve contacts, invoices, fee calculation, contract award and change orders as separate areas. Fee, budget and billing details and contact persons stored in the project itself are, however, included in a single retrieval of a project.
- Webhooks for changes in the web app or in Structify on iPhone & Android; redelivery of failed webhooks.
- Sign-in via OAuth, personal access tokens or keys restricted to individual projects.
- A test environment, a machine-readable interface description (OpenAPI) or ready-made client libraries.
Note Missing something?
Tell us about your use case. It helps us plan the next steps.
Changelog
- 8 Oct 2026 In the web app, API keys and webhooks are now managed by management only. A key is valid only as long as the person who created it is an active member of management.
- 6 Oct 2026 New base URL https://api.structify.solutions/functions/v1/public-api after operations moved to Hetzner in Germany. The previous address is no longer reachable.
Questions about integration?
We help you connect Structify to your systems, factually and within the scope the API offers today.