Developers · REST API v1

    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:

    Permissions of an API key and what they unlock
    PermissionUnlocks
    Any valid keyRead 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.

    Header
    X-Api-Key: <YOUR_API_KEY>

    Alternative header
    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:

    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:

    Request (cURL)
    curl "https://api.structify.solutions/functions/v1/public-api/v1/projekte?limit=2" \
      -H "X-Api-Key: <YOUR_API_KEY>"

    Request (JavaScript, Node.js 18 or later)
    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;

    Response 200 (shortened to one entry)
    {
      "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.

    Endpoints of the Structify API v1
    EndpointPurposePermission
    GET /v1/projekteList projectsany valid key
    GET /v1/projekte/{id}One project with all fieldsany valid key
    GET /v1/ticketsList defectsany valid key
    GET /v1/tickets/{id}One defect with all fieldsany valid key
    POST /v1/ticketsCreate a defectWrite tickets
    PUT /v1/tickets/{id}Update a defect (for example its status)Write tickets
    GET /v1/protokolleList reportsany valid key
    GET /v1/bautagebuchList legacy entries of the former site diaryany valid key
    GET /v1/dokumenteList project file detailsRead or Read documents
    GET /v1/webhooksList webhooksany valid key
    POST /v1/webhooksRegister a webhookWrite tickets
    DELETE /v1/webhooks/{id}Delete a webhookany 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).

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

    Headers of every successful response
    X-RateLimit-Limit: 1000
    X-RateLimit-Remaining: 997

    When the limit is reached, the API responds with status 429:

    Response 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:

    Response 401
    {
      "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 codes of the Structify API
    StatusMeaning
    200Success.
    400Invalid 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.
    401Key missing, invalid or deactivated.
    403Permission 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.
    404Unknown path or record not found; for single retrievals also for invalid identifiers and identifiers of other organizations.
    429Rate limit reached.
    503The 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:

    Webhook events
    EventTriggered when
    ticket.erstellta defect is created via POST /v1/tickets
    ticket.status_geaendertPUT /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.

    Headers
    POST /structify-webhook HTTP/1.1
    Host: your-company.example
    Content-Type: application/json
    X-Structify-Signature: sha256=<HEX>

    Body
    {
      "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:

    Verify the signature (Node.js)
    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

    1. Sign in to the web app as a member of management and open Settings → Integrations → API & third parties.
    2. Create a key with a descriptive name (for example "ERP connection") and select only the permissions your system needs.
    3. Copy the key immediately (it is shown only once) and store it on your server, for example as the environment variable STRUCTIFY_API_KEY.
    4. Call GET /v1/projekte as shown in the Sample request section.
    5. 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:

    Create a defect
    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"}'

    Response 200 (shortened)
    {
      "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:

    Change the status
    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:

    Retrieve the open defects of a project
    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>"

    Response 200
    {
      "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:

    Register a webhook
    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"]}'

    Response 200 (shortened)
    {
      "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.

    Get in touch

    Changelog

    1. 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.
    2. 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.
    Integration

    Questions about integration?

    We help you connect Structify to your systems, factually and within the scope the API offers today.