{
  "openapi": "3.1.0",
  "info": {
    "title": "Majster partner API",
    "version": "1.1.0",
    "summary": "Create work requests in a Majster organisation from your own system, follow the work, answer proposals and decisions, and receive signed webhooks.",
    "description": "The partner API (ADR-022) lets another product drive work in a Majster organisation: a customer request made in YOUR system becomes a request in the organisation's inbox; its staff accept it (or propose other terms), the request becomes a job with subtasks and client-visible lines, and every step comes back to you as a signed webhook.\n\n**Authentication.** Every call under `/partner/v1` carries `Authorization: Bearer mj_pk_live_…`, a key the organisation's owner creates in Settings → Integrations. The key is shown once; Majster stores only its hash. A revoked key answers `401`.\n\n**Versioning.** The path segment (`v1`) is the LINE; it changes only for a breaking change. Within a line the version is semver (this document: `1.1.0`) — additive changes bump the minor. Every response carries `x-majster-api-version`; `GET /partner/versions` lists the lines. Unknown fields in a response must be ignored.\n\n**Boundaries.** A key sees only requests it created (`channel: partner`, its own key) and jobs converted from them. An end client's own booking on the organisation's public page, or another integrator's request, is `404` — no oracle.\n\n**Idempotency.** `external_ref` is unique per key: repeating `POST /requests` with a reference you already sent answers `200` with the existing request, so a timed-out call can be retried safely.\n\n**Rate limit.** 120 requests per minute per client IP; `429` with `retry-after`.\n\n**Errors** are `{\"error\": \"<code>\"}` with the codes listed per operation; `422 invalid_field` also carries `field`.\n\n**Webhooks** are `POST`s of a JSON envelope to the endpoints the owner registered, signed Stripe-style: `X-Majster-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" with the endpoint's whsec_… secret>`. Reject a signature older than five minutes. Deliveries are retried with backoff (1 min, 5 min, 30 min, 2 h, 12 h) until a `2xx`; they may arrive out of order and a retry may arrive after a later event — dedupe by `id` and read the payload's own `status`/`history`.",
    "contact": {
      "name": "Majster",
      "url": "https://majster.io"
    }
  },
  "servers": [
    {
      "url": "https://staging-api.majster.ducker.pl/partner/v1",
      "description": "Staging (demo organisations)"
    },
    {
      "url": "https://api.majster.ducker.pl/partner/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "partnerKey": []
    }
  ],
  "tags": [
    {
      "name": "Organisation",
      "description": "Who the key belongs to, and what the organisation offers."
    },
    {
      "name": "Requests",
      "description": "Work requests you create, and their negotiation."
    },
    {
      "name": "Work",
      "description": "Jobs converted from your requests: status, lines, subtasks, decisions."
    },
    {
      "name": "Decisions",
      "description": "Questions the business asks your client mid-job (extra work, a part)."
    },
    {
      "name": "Meta",
      "description": "Version and documentation — no key needed."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": ["Organisation"],
        "operationId": "me",
        "summary": "The organisation this key belongs to, its branches and departments",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "No or revoked key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unauthorized": {
                    "value": {
                      "error": "unauthorized"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/services": {
      "get": {
        "tags": ["Organisation"],
        "operationId": "listServices",
        "summary": "The bookable catalogue with prices, durations and additions",
        "description": "What your app may offer. `price` is `null` for a service priced on site; `add_ons` are the additions offered WITH that service (only these are accepted in `add_on_ids`).",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Catalogue"
                }
              }
            }
          },
          "401": {
            "description": "No or revoked key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unauthorized": {
                    "value": {
                      "error": "unauthorized"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/requests": {
      "post": {
        "tags": ["Requests"],
        "operationId": "createRequest",
        "summary": "Create a work request (idempotent per external_ref)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRequest"
              },
              "example": {
                "external_ref": "GR-1042",
                "service_id": "maintenance",
                "add_on_ids": ["extra-sensor"],
                "starts_at": "2026-10-02T08:00:00Z",
                "description": "The living-room sensor stopped reporting temperature.",
                "site_address": "ul. Długa 12/3, 31-147 Kraków",
                "requester": {
                  "name": "Anna Nowak",
                  "email": "anna@example.com",
                  "phone": "+48 600 100 200"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — pending in the organisation's inbox",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/Request"
                    }
                  },
                  "required": ["request"]
                }
              }
            }
          },
          "200": {
            "description": "This key already sent this `external_ref` — the existing request, unchanged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/Request"
                    }
                  },
                  "required": ["request"]
                }
              }
            }
          },
          "422": {
            "description": "Refused at the border",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "external_ref_required": {
                    "value": {
                      "error": "external_ref_required"
                    }
                  },
                  "requester_name_required": {
                    "value": {
                      "error": "requester_name_required"
                    }
                  },
                  "invalid_field": {
                    "value": {
                      "error": "invalid_field",
                      "field": "requester.email"
                    }
                  },
                  "service_not_found": {
                    "value": {
                      "error": "service_not_found"
                    }
                  },
                  "location_not_found": {
                    "value": {
                      "error": "location_not_found"
                    }
                  },
                  "department_not_found": {
                    "value": {
                      "error": "department_not_found"
                    }
                  },
                  "invalid_slot": {
                    "value": {
                      "error": "invalid_slot"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "The sync engine refused the write — nothing was created; retry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sync_push_refused": {
                    "value": {
                      "error": "sync_push_refused"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or revoked key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unauthorized": {
                    "value": {
                      "error": "unauthorized"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": ["Requests"],
        "operationId": "listRequests",
        "summary": "List this key's requests",
        "description": "Newest first, or oldest-changed first when `updated_since` is given — page by passing the last item's `updated_at` back as `updated_since`.",
        "parameters": [
          {
            "name": "external_ref",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Exactly this reference (at most one result)."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/RequestStatus"
            },
            "description": "Only requests whose current status is this."
          },
          {
            "name": "updated_since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time",
              "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`)."
            },
            "description": "Only requests changed at or after this instant, oldest change first."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Request"
                      }
                    }
                  },
                  "required": ["requests"]
                }
              }
            }
          },
          "422": {
            "description": "A filter that does not parse (`field` names it)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_field": {
                    "value": {
                      "error": "invalid_field"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or revoked key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unauthorized": {
                    "value": {
                      "error": "unauthorized"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/requests/{id}": {
      "get": {
        "tags": ["Requests"],
        "operationId": "getRequest",
        "summary": "One of this key's requests, with its job when converted",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/Request"
                    }
                  },
                  "required": ["request"]
                }
              }
            }
          },
          "404": {
            "description": "Not this key's request (a public booking, another integrator's, or unknown)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "value": {
                      "error": "not_found"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or revoked key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unauthorized": {
                    "value": {
                      "error": "unauthorized"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/requests/{id}/terms": {
      "post": {
        "tags": ["Requests"],
        "operationId": "answerTerms",
        "summary": "Relay your client's answer to the terms the business proposed",
        "description": "Only while the request is `proposed`. `accept` converts it into a job on the proposed terms; `decline` withdraws it (`cancelled`).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": ["accept", "decline"]
                  }
                },
                "required": ["decision"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Request"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "work_id": {
                              "type": "string",
                              "format": "uuid",
                              "nullable": true
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The request is not `proposed` any more",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "request_not_pending": {
                    "value": {
                      "error": "request_not_pending"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`decision` is not accept/decline",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_decision": {
                    "value": {
                      "error": "invalid_decision"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not this key's request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "value": {
                      "error": "not_found"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "The engine refused the write; retry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sync_push_refused": {
                    "value": {
                      "error": "sync_push_refused"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/requests/{id}/messages": {
      "post": {
        "tags": ["Requests"],
        "operationId": "postMessage",
        "summary": "Post a message from your client on the request's thread",
        "description": "The organisation's staff see it in their inbox thread and are notified; their replies come back on `request.messages` (poll the request or wait for `request.proposed`/`request.accepted`).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "maxLength": 1000
                  }
                },
                "required": ["text"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/Request"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Blank text",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "message_required": {
                    "value": {
                      "error": "message_required"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not this key's request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "value": {
                      "error": "not_found"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "The engine refused the write; retry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sync_push_refused": {
                    "value": {
                      "error": "sync_push_refused"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/work": {
      "get": {
        "tags": ["Work"],
        "operationId": "listWork",
        "summary": "List jobs converted from this key's requests",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["open", "closed"]
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time",
              "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`)."
            },
            "description": "Only jobs changed at or after this instant, oldest change first."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "work": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Work"
                      }
                    }
                  },
                  "required": ["work"]
                }
              }
            }
          },
          "422": {
            "description": "A filter that does not parse",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_field": {
                    "value": {
                      "error": "invalid_field"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or revoked key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unauthorized": {
                    "value": {
                      "error": "unauthorized"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/work/{id}": {
      "get": {
        "tags": ["Work"],
        "operationId": "getWork",
        "summary": "A job: status, timeline, client-visible lines, subtasks, open decisions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "work": {
                      "$ref": "#/components/schemas/Work"
                    }
                  },
                  "required": ["work"]
                }
              }
            }
          },
          "404": {
            "description": "Not a job converted from this key's request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "value": {
                      "error": "not_found"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or revoked key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unauthorized": {
                    "value": {
                      "error": "unauthorized"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/decisions/{id}/respond": {
      "post": {
        "tags": ["Decisions"],
        "operationId": "respondDecision",
        "summary": "Relay your client's answer to a question the business asked mid-job",
        "description": "Only a `pending` decision on one of this key's jobs. For a `line_items` decision, `responses` may carry a per-item verdict.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": ["approve", "reject"]
                  },
                  "responses": {
                    "type": "object",
                    "description": "Optional, ≤ 100 keys / 16 KB — e.g. `{\"<line_id>\": true}` per item.",
                    "additionalProperties": true
                  }
                },
                "required": ["decision"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "decision": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": ["approved", "rejected"]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Already answered",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "decision_not_pending": {
                    "value": {
                      "error": "decision_not_pending"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`decision` is not approve/reject (or `responses` too large: invalid_responses)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_decision": {
                    "value": {
                      "error": "invalid_decision"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not a decision on this key's job",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "value": {
                      "error": "not_found"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "The engine refused the write; retry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sync_push_refused": {
                    "value": {
                      "error": "sync_push_refused"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "request.received": {
      "post": {
        "summary": "request.received",
        "description": "Your request reached the inbox (`status: pending`).",
        "operationId": "on_request_received",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "b3d9…",
                "event": "request.received",
                "created_at": "2026-09-21T09:15:00Z",
                "attempt": 1,
                "data": {
                  "request": {
                    "…": "…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged — any other status (or a timeout of 10 s) is retried with backoff."
          }
        }
      }
    },
    "request.proposed": {
      "post": {
        "summary": "request.proposed",
        "description": "Staff proposed other terms — `data.request.terms` carries them; answer with `POST /requests/{id}/terms`.",
        "operationId": "on_request_proposed",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "b3d9…",
                "event": "request.proposed",
                "created_at": "2026-09-21T09:15:00Z",
                "attempt": 1,
                "data": {
                  "request": {
                    "…": "…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged — any other status (or a timeout of 10 s) is retried with backoff."
          }
        }
      }
    },
    "request.accepted": {
      "post": {
        "summary": "request.accepted",
        "description": "The request became a job: `data.request.work_id`, and `data.request.work` with the planned slot.",
        "operationId": "on_request_accepted",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "b3d9…",
                "event": "request.accepted",
                "created_at": "2026-09-21T09:15:00Z",
                "attempt": 1,
                "data": {
                  "request": {
                    "…": "…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged — any other status (or a timeout of 10 s) is retried with backoff."
          }
        }
      }
    },
    "request.declined": {
      "post": {
        "summary": "request.declined",
        "description": "Staff declined; `data.request.messages` may carry why.",
        "operationId": "on_request_declined",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "b3d9…",
                "event": "request.declined",
                "created_at": "2026-09-21T09:15:00Z",
                "attempt": 1,
                "data": {
                  "request": {
                    "…": "…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged — any other status (or a timeout of 10 s) is retried with backoff."
          }
        }
      }
    },
    "request.cancelled": {
      "post": {
        "summary": "request.cancelled",
        "description": "Withdrawn (by your `decline` of a proposal, or the client on the tracking page).",
        "operationId": "on_request_cancelled",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "b3d9…",
                "event": "request.cancelled",
                "created_at": "2026-09-21T09:15:00Z",
                "attempt": 1,
                "data": {
                  "request": {
                    "…": "…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged — any other status (or a timeout of 10 s) is retried with backoff."
          }
        }
      }
    },
    "work.status_changed": {
      "post": {
        "summary": "work.status_changed",
        "description": "The job moved (`data.work.status`); also fires with `work.closed` when it closes.",
        "operationId": "on_work_status_changed",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "b3d9…",
                "event": "work.status_changed",
                "created_at": "2026-09-21T09:15:00Z",
                "attempt": 1,
                "data": {
                  "work": {
                    "…": "…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged — any other status (or a timeout of 10 s) is retried with backoff."
          }
        }
      }
    },
    "work.decision_requested": {
      "post": {
        "summary": "work.decision_requested",
        "description": "The business asks your client something mid-job — `data.decision` (with `items` for a `line_items` question) and `data.work`; answer with `POST /decisions/{id}/respond`.",
        "operationId": "on_work_decision_requested",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "b3d9…",
                "event": "work.decision_requested",
                "created_at": "2026-09-21T09:15:00Z",
                "attempt": 1,
                "data": {
                  "decision": {
                    "…": "…"
                  },
                  "work": {
                    "…": "…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged — any other status (or a timeout of 10 s) is retried with backoff."
          }
        }
      }
    },
    "decision.answered": {
      "post": {
        "summary": "decision.answered",
        "description": "A decision on one of your jobs was answered — by you through the API, or by the client on the portal (`data.decision.status`, `decided_via`).",
        "operationId": "on_decision_answered",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "b3d9…",
                "event": "decision.answered",
                "created_at": "2026-09-21T09:15:00Z",
                "attempt": 1,
                "data": {
                  "decision": {
                    "…": "…"
                  },
                  "work": {
                    "…": "…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged — any other status (or a timeout of 10 s) is retried with backoff."
          }
        }
      }
    },
    "work.closed": {
      "post": {
        "summary": "work.closed",
        "description": "The job is finished and released.",
        "operationId": "on_work_closed",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "b3d9…",
                "event": "work.closed",
                "created_at": "2026-09-21T09:15:00Z",
                "attempt": 1,
                "data": {
                  "work": {
                    "…": "…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged — any other status (or a timeout of 10 s) is retried with backoff."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "partnerKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "mj_pk_live_…",
        "description": "A partner API key created by the organisation's owner (Settings → Integrations)."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "field": {
            "type": "string",
            "description": "With `invalid_field`: the offending field, dotted (`requester.email`)."
          },
          "reason": {
            "type": "string",
            "description": "With `sync_push_refused`: the engine's reason."
          }
        },
        "required": ["error"]
      },
      "Me": {
        "type": "object",
        "properties": {
          "org": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "key": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "locations": {
            "type": "array",
            "description": "The organisation's branches; `location_id` on a request names one (default: the first).",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "departments": {
            "type": "array",
            "description": "Routing units inside branches; `department_id` on a request names one of the chosen branch's.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "location_id": {
                  "type": "string",
                  "format": "uuid",
                  "nullable": true
                }
              }
            }
          },
          "api_version": {
            "type": "string",
            "example": "1.1.0"
          }
        }
      },
      "Catalogue": {
        "type": "object",
        "properties": {
          "currency": {
            "type": "string",
            "example": "PLN"
          },
          "services": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Service"
            }
          }
        }
      },
      "Service": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "duration_minutes": {
            "type": "integer"
          },
          "price": {
            "type": "number",
            "description": "Net, in the catalogue currency; `null` = priced on site.",
            "nullable": true
          },
          "add_ons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AddOn"
            }
          }
        }
      },
      "AddOn": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "duration_minutes": {
            "type": "integer"
          },
          "price": {
            "type": "number",
            "nullable": true
          }
        }
      },
      "Requester": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "email": {
            "type": "string",
            "format": "email",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "description": "Any national/international form; stored as E.164.",
            "nullable": true
          }
        },
        "required": ["name"]
      },
      "CreateRequest": {
        "type": "object",
        "required": ["external_ref", "requester"],
        "properties": {
          "external_ref": {
            "type": "string",
            "maxLength": 128,
            "description": "Your own id for this request — unique per key, the idempotency key."
          },
          "requester": {
            "$ref": "#/components/schemas/Requester"
          },
          "service_id": {
            "type": "string",
            "description": "One of `/services`; default: the organisation's first offered service."
          },
          "add_on_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Additions offered with the service; anything else is dropped silently."
          },
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`)."
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "description": "Optional; defaults to the start plus the quoted duration. Must be after `starts_at`; the start must not be in the past."
          },
          "description": {
            "type": "string",
            "maxLength": 4000
          },
          "site_address": {
            "type": "string",
            "maxLength": 500,
            "description": "Where the work happens (an installation address). Shown to staff on the request and the job."
          },
          "location_id": {
            "type": "string",
            "format": "uuid",
            "description": "A branch from `/me`; default: the first."
          },
          "department_id": {
            "type": "string",
            "description": "A department of that branch from `/me`."
          }
        }
      },
      "RequestStatus": {
        "type": "string",
        "enum": ["pending", "proposed", "accepted", "declined", "cancelled"]
      },
      "Terms": {
        "type": "object",
        "description": "What the request currently says: the newest proposal, else the client's own ask.",
        "properties": {
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`).",
            "nullable": true
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`).",
            "nullable": true
          },
          "add_on_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "estimated_cost": {
            "type": "number",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "HistoryEntry": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string"
          },
          "by": {
            "type": "string",
            "description": "`client` (your side / the end client), or a staff id."
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`)."
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "by": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`)."
          }
        }
      },
      "Request": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "external_ref": {
            "type": "string"
          },
          "channel": {
            "type": "string",
            "enum": ["partner", "public"]
          },
          "status": {
            "$ref": "#/components/schemas/RequestStatus"
          },
          "service_id": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string"
          },
          "site_address": {
            "type": "string",
            "nullable": true
          },
          "requester": {
            "$ref": "#/components/schemas/Requester"
          },
          "requested": {
            "type": "object",
            "description": "The client's original ask.",
            "properties": {
              "starts_at": {
                "type": "string",
                "format": "date-time",
                "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`).",
                "nullable": true
              },
              "ends_at": {
                "type": "string",
                "format": "date-time",
                "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`).",
                "nullable": true
              }
            }
          },
          "add_on_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "estimated_cost": {
            "type": "number",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "nullable": true
          },
          "terms": {
            "$ref": "#/components/schemas/Terms"
          },
          "history": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HistoryEntry"
            }
          },
          "proposals": {
            "type": "array",
            "description": "Every set of terms staff proposed, oldest first.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Message"
            }
          },
          "work_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "work": {
            "$ref": "#/components/schemas/Work",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the request's row last changed (list endpoints)."
          }
        }
      },
      "Line": {
        "type": "object",
        "description": "A client-visible line of the job.",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": ["labor", "part", "expense"]
          },
          "name": {
            "type": "string"
          },
          "qty": {
            "type": "number"
          },
          "unit_price": {
            "type": "number",
            "nullable": true
          }
        }
      },
      "Subtask": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "readiness": {
            "type": "string",
            "enum": ["ready", "waiting", "done"]
          }
        }
      },
      "DecisionItem": {
        "type": "object",
        "properties": {
          "line_id": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "qty": {
            "type": "number"
          },
          "unit_price": {
            "type": "number"
          }
        }
      },
      "Decision": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "amount": {
            "type": "number",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": ["pending", "approved", "rejected"]
          },
          "kind": {
            "type": "string",
            "enum": ["line_items"],
            "description": "`line_items`: a per-item question — see `items`; absent: a free-text ask with an `amount`.",
            "nullable": true
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DecisionItem"
            }
          },
          "decided_via": {
            "type": "string",
            "enum": ["portal", "inperson", "partner"],
            "nullable": true
          }
        }
      },
      "Work": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "site_address": {
            "type": "string",
            "nullable": true
          },
          "state": {
            "type": "string",
            "enum": ["open", "closed"]
          },
          "status": {
            "type": "string",
            "description": "The job's current workflow status, e.g. `started`, `finished`; `null` before the first move.",
            "nullable": true
          },
          "history": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HistoryEntry"
            }
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`).",
            "nullable": true
          },
          "estimated_end_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`).",
            "nullable": true
          },
          "department_id": {
            "type": "string",
            "nullable": true
          },
          "transferred_to_id": {
            "type": "string",
            "format": "uuid",
            "description": "Set when the job moved to another branch; follow it there.",
            "nullable": true
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Line"
            }
          },
          "subtasks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Subtask"
            }
          },
          "decisions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Decision"
            }
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the job's row last changed (list endpoints)."
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "description": "Every delivery. `data` carries `request` and/or `work`/`decision` as the event says.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The delivery's id — dedupe on it."
          },
          "event": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601, UTC (`2026-09-21T08:00:00Z`)."
          },
          "attempt": {
            "type": "integer"
          },
          "data": {
            "type": "object",
            "properties": {
              "request": {
                "$ref": "#/components/schemas/Request"
              },
              "work": {
                "$ref": "#/components/schemas/Work"
              },
              "decision": {
                "$ref": "#/components/schemas/Decision"
              }
            }
          }
        },
        "required": ["id", "event", "created_at", "data"]
      }
    }
  }
}
