{
  "x-looma-integration-certification": {
    "revision": "2026-10-23.2",
    "publishedOn": "2026-10-23",
    "policy": "Exact combinations and covered flows only. neutral means protocol-only, not country certification. contract-tested means local compatibility fixtures, not provider certification. unverified and unlisted combinations are not certified or supported by this matrix. No vendor endorsement, implementation SLA or statutory compliance is implied.",
    "defaultSupportState": "unsupported",
    "entries": [
      {
        "provider": "Looma",
        "country": "neutral",
        "product": "Integration read API",
        "version": "v1",
        "capability": "PI08",
        "flows": [
          "collection filter, sort, page and limit contract"
        ],
        "supportState": "contract-tested",
        "limitations": "Only the shared collection query contract; not certification of a customer connector, country payroll or all API resources.",
        "supportOwner": "Looma: published API contract; customer: consumer configuration and reconciliation",
        "evidence": [
          {
            "package": "internal/core/integrations",
            "test": "TestListParamsFilterSortAndPageTheDocumentedCollections",
            "kind": "local-contract"
          }
        ],
        "reconciliation": {
          "id": "looma-api-id-v1",
          "fields": [
            "id"
          ],
          "supportState": "contract-tested"
        }
      },
      {
        "provider": "Looma",
        "country": "neutral",
        "product": "Signed webhooks",
        "version": "v1",
        "capability": "PI09",
        "flows": [
          "event delivery and retries"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": [],
        "reconciliation": {
          "id": "looma-webhook-delivery-v1",
          "fields": [
            "delivery_id"
          ],
          "supportState": "unverified"
        }
      },
      {
        "provider": "Customer SFTP server",
        "country": "neutral",
        "product": "SSH file transfer",
        "version": "unverified",
        "capability": "PI10",
        "flows": [
          "inbound and outbound file transfer"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Looma",
        "country": "neutral",
        "product": "Integration field mapping",
        "version": "unverified",
        "capability": "PI11",
        "flows": [
          "import and export mapping"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": [],
        "reconciliation": {
          "id": "looma-mapping-employee-date-v1",
          "fields": [
            "employee_number",
            "effective_date"
          ],
          "supportState": "unverified"
        }
      },
      {
        "provider": "Looma",
        "country": "neutral",
        "product": "Integration health",
        "version": "unverified",
        "capability": "PI12",
        "flows": [
          "transfer failure and health reporting"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Microsoft",
        "country": "neutral",
        "product": "Teams meetings",
        "version": "unverified",
        "capability": "PI13",
        "flows": [
          "meeting connection"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Google",
        "country": "neutral",
        "product": "Meet",
        "version": "unverified",
        "capability": "PI13",
        "flows": [
          "meeting connection"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Zoom",
        "country": "neutral",
        "product": "Meetings",
        "version": "unverified",
        "capability": "PI13",
        "flows": [
          "meeting connection"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Slack",
        "country": "neutral",
        "product": "Incoming webhooks",
        "version": "unverified",
        "capability": "PI14",
        "flows": [
          "chat notification delivery"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Microsoft",
        "country": "neutral",
        "product": "Teams webhooks",
        "version": "unverified",
        "capability": "PI14",
        "flows": [
          "chat notification delivery"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Microsoft",
        "country": "neutral",
        "product": "Entra ID company SSO",
        "version": "unverified",
        "capability": "IT07",
        "flows": [
          "company sign-in"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Okta",
        "country": "neutral",
        "product": "Company SSO",
        "version": "unverified",
        "capability": "IT07",
        "flows": [
          "company sign-in"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Microsoft",
        "country": "neutral",
        "product": "Entra ID SCIM",
        "version": "2.0",
        "capability": "IT08",
        "flows": [
          "account provisioning and deactivation"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Okta",
        "country": "neutral",
        "product": "SCIM",
        "version": "2.0",
        "capability": "IT08",
        "flows": [
          "account provisioning and deactivation"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Google",
        "country": "neutral",
        "product": "Workspace",
        "version": "unverified",
        "capability": "IT09",
        "flows": [
          "outbound account lifecycle"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      },
      {
        "provider": "Microsoft",
        "country": "neutral",
        "product": "Microsoft 365",
        "version": "unverified",
        "capability": "IT09",
        "flows": [
          "outbound account lifecycle"
        ],
        "supportState": "unverified",
        "limitations": "No provider/country/product-release compatibility evidence recorded. Existing configuration options do not establish certification.",
        "supportOwner": "Customer and Looma: agree exact provider release, country, flows and validation before implementation; no certified-provider SLA",
        "evidence": []
      }
    ],
    "sha256": "4f6da36d412b23e9a41a819c97bb575346ab9fd6cf35eafde51baebe4ccdf070"
  },
  "openapi": "3.1.0",
  "info": {
    "title": "Looma Integration API",
    "version": "1.0.0",
    "description": "The versioned REST API for Looma. Authenticate with a scoped bearer token created in Settings → Configuration → Integrations. The tenant is derived only from the token; a token can never read or write another company. Employee fields pass the field-visibility resolver under the dedicated \"integration\" viewer role. Read endpoints need a read scope. Write endpoints need the matching write scope, run the same integrity checks and the same configurable approval routing as the equivalent form in the app, attribute the change to the token in the HR audit trail, and accept an Idempotency-Key header so a retry replays the stored response instead of creating a second change."
  },
  "servers": [
    {
      "url": "https://api.looma.hr",
      "description": "The integrations API host. Any valid token is served; it answers only the API, MCP and OAuth token endpoint."
    },
    {
      "url": "https://{company-host}",
      "description": "Your company workspace host. The token must belong to that company."
    }
  ],
  "tags": [
    {
      "name": "People",
      "description": "Directory and individual employee records."
    },
    {
      "name": "Objectives",
      "description": "Employee objectives and goals for the current cycle."
    },
    {
      "name": "Organisation",
      "description": "Legal entities, workforce catalogue and reporting lines."
    },
    {
      "name": "Time off",
      "description": "Approved leave requests."
    },
    {
      "name": "Time",
      "description": "Approved timecards."
    },
    {
      "name": "Payroll",
      "description": "Payroll runs, payslips and the pay inputs that feed a run."
    },
    {
      "name": "Recruiting",
      "description": "Jobs, candidates and applications."
    },
    {
      "name": "Expenses",
      "description": "Employee expense claims."
    },
    {
      "name": "Documents",
      "description": "Employee document metadata."
    },
    {
      "name": "Webhooks",
      "description": "Webhook delivery replay."
    },
    {
      "name": "Meta",
      "description": "API description and discovery."
    }
  ],
  "paths": {
    "/api/v1/analytics/headcount": {
      "get": {
        "operationId": "getHeadcount",
        "summary": "Read current headcount by dimension",
        "tags": [
          "People"
        ],
        "x-looma-scope": "analytics:read",
        "responses": {
          "200": {
            "description": "Tenant-scoped headcount analytics; small groups are withheld."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/analytics/headcount/trend": {
      "get": {
        "operationId": "getHeadcountTrend",
        "summary": "Read monthly headcount trend",
        "tags": [
          "People"
        ],
        "x-looma-scope": "analytics:read",
        "responses": {
          "200": {
            "description": "Tenant-scoped headcount analytics; small groups are withheld."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/applications": {
      "get": {
        "operationId": "listApplications",
        "summary": "List vacancy applications",
        "description": "Vacancy applications by existing people, newest first by id. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Recruiting"
        ],
        "x-looma-scope": "recruiting:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of applications.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplicationPage"
                },
                "example": {
                  "hasMore": false,
                  "items": [
                    {
                      "id": 1101,
                      "personId": "p-1001",
                      "status": "submitted",
                      "vacancyId": 5001
                    }
                  ],
                  "nextCursor": "1101"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/automation-sources/{source}/events": {
      "post": {
        "operationId": "submitAutomationBusinessEvent",
        "summary": "Submit a typed external business event",
        "description": "Maximum 4096 bytes. The source must bind this bearer token and event type. An explicit employee-number/person mapping must still match the tenant record. externalEventId is idempotent per source; conflicting duplicates return 409. Accepted active subscriptions are pinned to their rule version. Revocation cancels pending deliveries. Activating a rule never replays old events. Operators can inspect deliveries and request a bounded replay through the source management API.",
        "tags": [
          "Meta"
        ],
        "x-looma-scope": "automations.events:write",
        "parameters": [
          {
            "name": "source",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Only these five fields are accepted.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "employeeNumber": {
                    "type": "string"
                  },
                  "eventType": {
                    "type": "string",
                    "enum": [
                      "service_desk_cleared",
                      "training_completed",
                      "compliance_check_completed"
                    ]
                  },
                  "externalEventId": {
                    "type": "string"
                  },
                  "occurredAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "sourceRecordId": {
                    "type": "string"
                  }
                },
                "required": [
                  "externalEventId",
                  "eventType",
                  "sourceRecordId",
                  "employeeNumber",
                  "occurredAt"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Persisted event receipt (id and duplicate); execution is asynchronous."
          },
          "400": {
            "description": "Invalid type, schema, timestamp or oversized body."
          },
          "401": {
            "description": "Invalid bearer credential."
          },
          "403": {
            "description": "Missing scope, revoked or unbound source, or denied employee mapping."
          },
          "409": {
            "description": "External event ID already has a different payload."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/benefits": {
      "get": {
        "operationId": "listBenefits",
        "summary": "List benefit plans",
        "tags": [
          "People"
        ],
        "x-looma-scope": "benefits:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A tenant-scoped cursor page."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/benefits/{benefit}/participants": {
      "get": {
        "operationId": "listBenefitParticipants",
        "summary": "List benefit participants",
        "tags": [
          "People"
        ],
        "x-looma-scope": "benefits:read",
        "parameters": [
          {
            "name": "benefit",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A tenant-scoped cursor page."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/candidates": {
      "get": {
        "operationId": "listCandidates",
        "summary": "List candidates",
        "description": "Candidate contact records, newest first by id. Candidate personal fields pass the integration field-visibility policy and are blank (and listed in hiddenFields) where that policy denies them. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Recruiting"
        ],
        "x-looma-scope": "recruiting:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of candidates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CandidatePage"
                },
                "example": {
                  "hasMore": false,
                  "items": [
                    {
                      "email": "alex@example.com",
                      "firstName": "Alex",
                      "id": 9001,
                      "lastName": "Nguyen",
                      "stage": "interview"
                    }
                  ],
                  "nextCursor": "9001"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/changelog": {
      "get": {
        "operationId": "getChangelog",
        "summary": "Published release record of this API",
        "description": "Returns the contract releases, newest first, with the current version, so an integration can reconcile against the X-API-Version header it was served. It carries no tenant data and needs no token.",
        "tags": [
          "Meta"
        ],
        "responses": {
          "200": {
            "description": "The published release record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "apiVersion": "1.0.0",
                  "current": {
                    "version": "1.0.0",
                    "date": "2026-10-14",
                    "title": "Every response names the contract version, and the release record is published",
                    "body": "Every response from the versioned API now carries the X-API-Version header naming the semver contract release that produced it, and GET /api/v1/changelog publishes this release record, so an integration can see which contract era it is running against and what changed since. The first public release of the versioned API ships in this same era: scoped bearer tokens, field visibility applied per person, keyset pagination, per-token rate limiting and HMAC-signed outbound webhooks, with the write surface staging a hire, an employment, a reporting-line and a pay change under an Idempotency-Key."
                  },
                  "entries": [
                    {
                      "version": "1.0.0",
                      "date": "2026-10-14",
                      "title": "Every response names the contract version, and the release record is published",
                      "body": "Every response from the versioned API now carries the X-API-Version header naming the semver contract release that produced it, and GET /api/v1/changelog publishes this release record, so an integration can see which contract era it is running against and what changed since. The first public release of the versioned API ships in this same era: scoped bearer tokens, field visibility applied per person, keyset pagination, per-token rate limiting and HMAC-signed outbound webhooks, with the write surface staging a hire, an employment, a reporting-line and a pay change under an Idempotency-Key."
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/collections/bruno.json": {
      "get": {
        "operationId": "getBrunoCollection",
        "summary": "Download the Bruno request collection",
        "description": "The whole /api/v1 surface as a Bruno collection, generated from this document. It carries no credential: the base URL and the bearer token are collection variables the developer fills in.",
        "tags": [
          "Meta"
        ],
        "responses": {
          "200": {
            "description": "A Bruno 1.x collection document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/collections/postman.json": {
      "get": {
        "operationId": "getPostmanCollection",
        "summary": "Download the Postman request collection",
        "description": "The whole /api/v1 surface as a Postman 2.1 collection, generated from this document with the same requests as the Bruno collection. It carries no credential.",
        "tags": [
          "Meta"
        ],
        "responses": {
          "200": {
            "description": "A Postman 2.1 collection document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/employees/{person}/goals": {
      "get": {
        "operationId": "listEmployeeGoals",
        "summary": "List an employee's visible current goals",
        "tags": [
          "Objectives"
        ],
        "x-looma-scope": "objectives:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–200; default 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "person",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor-paginated page of visible current-cycle objectives."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/employees/{person}/objectives": {
      "get": {
        "operationId": "listEmployeeObjectives",
        "summary": "List an employee's current-cycle objectives",
        "tags": [
          "Objectives"
        ],
        "x-looma-scope": "objectives:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–200; default 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "person",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor-paginated page of visible current-cycle objectives."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/expense-receipts": {
      "post": {
        "operationId": "receiveExpenseReceipt",
        "summary": "Receive an expense receipt",
        "description": "Delivers one receipt attachment as a private proposal for the employee identified by an enabled routing token. The bearer token determines the company. Retries with the same deliveryKey return the existing intake id. This does not submit an expense claim. The JSON body is limited to 14 MiB and the decoded PDF, PNG or JPEG attachment to 10 MiB.",
        "tags": [
          "Expenses"
        ],
        "x-looma-scope": "expenses:receive",
        "requestBody": {
          "description": "The recipient routing token, stable delivery key, filename and base64 attachment content.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReceiveExpenseReceiptRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The receipt was captured, or an existing delivery was replayed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "intakeId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "draft"
                      ]
                    }
                  },
                  "required": [
                    "intakeId",
                    "status"
                  ]
                },
                "example": {
                  "intakeId": "receipt-1001",
                  "status": "draft"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "The receipt delivery or attachment is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "409": {
            "description": "The employee has reached the pending receipt limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "413": {
            "description": "The request body exceeds the 14 MiB limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/expenses": {
      "get": {
        "operationId": "listExpenses",
        "summary": "List expense claims",
        "description": "Employee expense claims, newest first by id. The claimant's name is blank and listed in hiddenFields when the integration field-visibility policy denies it. Totals are per currency and exact. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Expenses"
        ],
        "x-looma-scope": "expenses:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor (claim id) from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of expense claims.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExpenseClaimPage"
                },
                "example": {
                  "hasMore": true,
                  "items": [
                    {
                      "employeeNumber": "E-1001",
                      "id": 8201,
                      "lineCount": 2,
                      "personId": "p-1001",
                      "personName": "Alex Nguyen",
                      "revision": 2,
                      "status": "pending",
                      "title": "Client lunch"
                    }
                  ],
                  "nextCursor": "8201"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
      "post": {
        "operationId": "submitExpense",
        "summary": "Submit an expense claim",
        "description": "Creates a new expense claim for the token's linked employee and submits it through the same path as the app's claim form: the same normalisation, integrity checks, configurable EXPENSE_CLAIM approval routing, audit event and webhooks. The claim enters the company's approval flow and is not reimbursed until that flow completes. Replaying the same Idempotency-Key with the same body returns the first response instead of submitting a second claim.",
        "tags": [
          "Expenses"
        ],
        "x-looma-scope": "expenses:write",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "An opaque caller-chosen key, 1 to 200 characters. Replaying the same key with the same body returns the first response with replayed=true instead of creating a second claim. Replaying it with a different body is a 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "requestBody": {
          "description": "The claim title and line items.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitExpenseRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The claim was staged and is now in the company's configured EXPENSE_CLAIM approval flow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                },
                "example": {
                  "personId": "p-1001",
                  "status": "PENDING_APPROVAL",
                  "transactionId": 9211
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "The body or the claim inputs are invalid. Unknown body fields are rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "409": {
            "description": "The Idempotency-Key was used for a different body, or the claim could not be submitted in the expected state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/expenses/{claim}": {
      "get": {
        "operationId": "getExpense",
        "summary": "Read one expense claim",
        "description": "One expense claim with its full line items and event history. The claimant's name is blank and listed in hiddenFields when the integration field-visibility policy denies it.",
        "tags": [
          "Expenses"
        ],
        "x-looma-scope": "expenses:read",
        "parameters": [
          {
            "name": "claim",
            "in": "path",
            "description": "The claim id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The claim and its lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExpenseClaimDetail"
                },
                "example": {
                  "claim": {
                    "id": 8201,
                    "lineCount": 2,
                    "personId": "p-1001",
                    "personName": "Alex Nguyen",
                    "revision": 2,
                    "status": "pending",
                    "title": "Client lunch"
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "404": {
            "description": "No expense claim matched that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/goals": {
      "get": {
        "operationId": "listGoals",
        "summary": "List visible current goals",
        "tags": [
          "Objectives"
        ],
        "x-looma-scope": "objectives:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–200; default 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor-paginated page of visible current-cycle objectives."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/jobs": {
      "get": {
        "operationId": "listJobs",
        "summary": "List jobs (vacancies)",
        "description": "Vacancies, newest first by id, with their opening date, closing date and current state. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Recruiting"
        ],
        "x-looma-scope": "recruiting:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of jobs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecruitingJobPage"
                },
                "example": {
                  "hasMore": true,
                  "items": [
                    {
                      "department": "People",
                      "id": 5001,
                      "openingDate": "2026-03-01",
                      "status": "open",
                      "title": "People Partner"
                    }
                  ],
                  "nextCursor": "5001"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/objectives": {
      "get": {
        "operationId": "listObjectives",
        "summary": "List current-cycle objectives",
        "tags": [
          "Objectives"
        ],
        "x-looma-scope": "objectives:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–200; default 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor-paginated page of visible current-cycle objectives."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "operationId": "getOpenAPIDocument",
        "summary": "OpenAPI 3.1 description of this API",
        "description": "Returns this document. It carries no tenant data and needs no token, so tooling and the admin developer-docs page can read the contract.",
        "tags": [
          "Meta"
        ],
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/organisation": {
      "get": {
        "operationId": "getOrganisation",
        "summary": "Read the organisation",
        "description": "Legal entities, the active workforce catalogue (up to 1000 entries) and the current reporting line per person, keyset-paginated by person id in groups of up to 500.",
        "tags": [
          "Organisation"
        ],
        "x-looma-scope": "org:read",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque reporting-line cursor: the last person id from the previous response. Omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The organisation snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganisationResponse"
                },
                "example": {
                  "catalogue": [
                    {
                      "code": "PEOPLE",
                      "id": "dep-1",
                      "kind": "department",
                      "name": "People"
                    }
                  ],
                  "legalEntities": [
                    {
                      "id": "le-1",
                      "name": "Northwind Sweden AB"
                    }
                  ],
                  "reportingLines": [
                    {
                      "effectiveFrom": "2025-01-01",
                      "managerId": "p-1000",
                      "personId": "p-1001"
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/payroll/inputs": {
      "get": {
        "operationId": "listPayrollInputs",
        "summary": "List payroll inputs",
        "description": "The approved pay inputs that feed each payroll run (approved timecard minutes, approved flexible-benefit amounts and approved-unpaid leave units), newest first by run id. Values are withheld and redacted is set when the integration viewer's field policy restricts compensation. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Payroll"
        ],
        "x-looma-scope": "payroll:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor (run id) from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of payroll inputs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollInputPage"
                },
                "example": {
                  "hasMore": true,
                  "items": [
                    {
                      "flexAmount": "2500.0000",
                      "leaveUnits": "8",
                      "runId": 7001,
                      "timecardMinutes": "4800.0000"
                    }
                  ],
                  "nextCursor": "7001"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/payroll/runs": {
      "get": {
        "operationId": "listPayrollRuns",
        "summary": "List payroll runs",
        "description": "Payroll runs, newest first by run id. Totals are withheld and totalsRedacted is set when the integration viewer's field policy restricts compensation. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Payroll"
        ],
        "x-looma-scope": "payroll:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor (run id) from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of payroll runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollRunPage"
                },
                "example": {
                  "hasMore": true,
                  "items": [
                    {
                      "id": 7001,
                      "legalEntityId": "le-1",
                      "legalEntityName": "Northwind Sweden AB",
                      "lineCount": 12,
                      "periodEnd": "2026-06-30",
                      "periodId": 410,
                      "periodStart": "2026-06-01",
                      "runKind": "regular",
                      "status": "approved",
                      "totalGross": "120000.0000",
                      "totalPayout": "96000.0000"
                    }
                  ],
                  "nextCursor": "7001"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/payroll/runs/{run}/payslips": {
      "get": {
        "operationId": "listPayrollRunPayslips",
        "summary": "List a payroll run's payslips",
        "description": "Metadata of the published payslips in the run's payroll period, newest first. The confidential PDF bytes never appear in a paged list. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Payroll"
        ],
        "x-looma-scope": "payroll:read",
        "parameters": [
          {
            "name": "run",
            "in": "path",
            "description": "The payroll run id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor (payslip sequence) from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of payslip metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayslipPage"
                },
                "example": {
                  "hasMore": false,
                  "items": [
                    {
                      "byteSize": 20480,
                      "filename": "payslip-june.pdf",
                      "id": "ps-1001",
                      "personId": "p-1001",
                      "publishedAt": "2026-07-01T08:00:00",
                      "status": "published",
                      "version": 1
                    }
                  ],
                  "nextCursor": "17"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "The run id or cursor is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request is invalid"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "No payroll run matched that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No payroll run matched that id"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people": {
      "get": {
        "operationId": "listPeople",
        "summary": "List the employee directory",
        "description": "Current employees as of today, ordered by employee number then id. Uses the same effective-dated primary-employment semantics as the app, then applies the integration field-visibility resolver per person. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "People"
        ],
        "x-looma-scope": "people:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor from a previous response's nextCursor. Do not construct or parse it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeFormer",
            "in": "query",
            "description": "Include people whose employment has ended.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of employees.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployeePage"
                },
                "example": {
                  "hasMore": true,
                  "items": [
                    {
                      "countryCode": "SE",
                      "department": "People",
                      "employeeNumber": "E-1001",
                      "firstName": "Alex",
                      "id": "p-1001",
                      "jobTitle": "People Partner",
                      "lastName": "Nguyen",
                      "legalEntity": "Northwind Sweden AB",
                      "location": "Stockholm",
                      "revision": 1,
                      "startDate": "2024-03-01",
                      "status": "active",
                      "workEmail": "alex.nguyen@example.com",
                      "workerType": "employee"
                    }
                  ],
                  "nextCursor": "RS0xMDAxH3AtMTAwMQ"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
      "post": {
        "operationId": "createPerson",
        "summary": "Create a person and start the hire flow",
        "description": "Stages a hire through the same path as the app's People form: the same normalisation, integrity checks, configurable hire approval routing, audit event, notifications and webhooks. The person is created when the flow completes, not before. The token's creating account must still be an active HR or administrator member of the company.",
        "tags": [
          "People"
        ],
        "x-looma-scope": "people:write",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "An opaque caller-chosen key, 1 to 200 characters. Replaying the same key with the same body returns the first response with replayed=true instead of creating a second change. Replaying it with a different body is a 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "requestBody": {
          "description": "The person to hire.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePersonRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The change was staged and is now in the company's configured approval flow. It is not applied until that flow completes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                },
                "example": {
                  "personId": "p-1001",
                  "status": "PENDING_APPROVAL",
                  "transactionId": 4211
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "The body, the person id, the effective date or the revision is invalid. Unknown body fields are rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request is invalid"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "409": {
            "description": "The revision is stale, the record already exists, or the Idempotency-Key was used for a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request conflicts with the current state"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people/changes": {
      "get": {
        "operationId": "getPeopleChanges",
        "summary": "Synchronize people incrementally",
        "description": "Requires people:read and a live HR/admin owner. Start without checkpoint against an empty local store; persist each page and its returned checkpoint atomically. Continue with checkpoint until hasMore=false; that checkpoint is the lossless snapshot/delta handoff. Poll it for subsequent deltas. Always use this endpoint for bootstrap, not the directory listing. Checkpoints are opaque, tenant/token/resource-bound and expire after seven days, including unfinished batches. Invalid, foreign or expired checkpoints return 410 resync_required: discard the local projection and bootstrap again. Events are retained eight days; expired metadata is removed on polling. IDs in bootstrap are frozen; delta batches have a fixed upper commit sequence and order by sequence. Apply items in response order as replacements by id; repeated identities and replay are expected. Only identities and invalidations are stored: pages project current committed fields with current field policies, so retries may contain newer values. Removed or erased or no-current-primary people return only id and action=remove; delete all locally held fields. Former employees remain upserts with person.status=Former. Backdated changes invalidate immediately; future changes may invalidate immediately and all people are invalidated on the first poll of each new company day to activate effective dates even without writes. Policy and legal-entity changes invalidate all people. A demoted owner is refused; resume after reauthorization or bootstrap with a new token. A withheld field is blank and listed in hiddenFields; replace the whole previous projection so revoked fields are deleted. Do not apply concurrent page responses out of order.",
        "tags": [
          "People"
        ],
        "x-looma-scope": "people:read",
        "parameters": [
          {
            "name": "checkpoint",
            "in": "query",
            "description": "Opaque checkpoint returned by the previous page; omit only for a fresh bootstrap.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1–200 identities per page, default 50.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "items: ordered {id, action: upsert, person: Employee} or minimal {id, action: remove}; checkpoint: opaque string; hasMore: boolean; phase: snapshot or delta. Requires people:read and a live HR/admin owner. Start without checkpoint against an empty local store; persist each page and its returned checkpoint atomically. Continue with checkpoint until hasMore=false; that checkpoint is the lossless snapshot/delta handoff. Poll it for subsequent deltas. Always use this endpoint for bootstrap, not the directory listing. Checkpoints are opaque, tenant/token/resource-bound and expire after seven days, including unfinished batches. Invalid, foreign or expired checkpoints return 410 resync_required: discard the local projection and bootstrap again. Events are retained eight days; expired metadata is removed on polling. IDs in bootstrap are frozen; delta batches have a fixed upper commit sequence and order by sequence. Apply items in response order as replacements by id; repeated identities and replay are expected. Only identities and invalidations are stored: pages project current committed fields with current field policies, so retries may contain newer values. Removed or erased or no-current-primary people return only id and action=remove; delete all locally held fields. Former employees remain upserts with person.status=Former. Backdated changes invalidate immediately; future changes may invalidate immediately and all people are invalidated on the first poll of each new company day to activate effective dates even without writes. Policy and legal-entity changes invalidate all people. A demoted owner is refused; resume after reauthorization or bootstrap with a new token. A withheld field is blank and listed in hiddenFields; replace the whole previous projection so revoked fields are deleted. Do not apply concurrent page responses out of order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "checkpoint": {
                      "type": "string"
                    },
                    "hasMore": {
                      "type": "boolean"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "action": {
                            "type": "string"
                          },
                          "id": {
                            "type": "string"
                          },
                          "person": {
                            "$ref": "#/components/schemas/Employee"
                          }
                        },
                        "required": [
                          "id",
                          "action"
                        ]
                      }
                    },
                    "phase": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "items",
                    "checkpoint",
                    "hasMore",
                    "phase"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Credential or membership is no longer live."
          },
          "403": {
            "description": "Missing people:read or live HR/admin role."
          },
          "410": {
            "description": "resync_required. Discard the projection and bootstrap again."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people/{person}": {
      "get": {
        "operationId": "getPerson",
        "summary": "Read one employee record",
        "description": "One employee's effective-dated primary employment as of an optional date. Fields denied by the integration field-visibility policy are blank and listed in hiddenFields.",
        "tags": [
          "People"
        ],
        "x-looma-scope": "people:read",
        "parameters": [
          {
            "name": "person",
            "in": "path",
            "description": "The person id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "on",
            "in": "query",
            "description": "Effective date (YYYY-MM-DD). Defaults to today (UTC).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The employee record as of the requested date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Employee"
                },
                "example": {
                  "countryCode": "SE",
                  "department": "People",
                  "employeeNumber": "E-1001",
                  "firstName": "Alex",
                  "id": "p-1001",
                  "jobTitle": "People Partner",
                  "lastName": "Nguyen",
                  "legalEntity": "Northwind Sweden AB",
                  "location": "Stockholm",
                  "revision": 1,
                  "startDate": "2024-03-01",
                  "status": "active",
                  "workEmail": "alex.nguyen@example.com",
                  "workerType": "employee"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "The effective date or person id is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request is invalid"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "No employee matched that id on the requested date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No employee matched that id on the requested date"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people/{person}/benefits": {
      "get": {
        "operationId": "listPersonBenefits",
        "summary": "List a person's benefit enrolments",
        "tags": [
          "People"
        ],
        "x-looma-scope": "benefits:read",
        "parameters": [
          {
            "name": "person",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A tenant-scoped cursor page."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people/{person}/compensation": {
      "get": {
        "operationId": "getCompensation",
        "summary": "Read a person's compensation history",
        "description": "One person's effective-dated pay history and current pay as of an optional date. HR and administrator tokens only, exactly as in the app. Amounts, the pay-zone range and the market context honour the integration field-visibility policy; a denied policy blanks them and names salary in hiddenFields. Cross-tenant reads are impossible because the tenant comes only from the token.",
        "tags": [
          "People"
        ],
        "x-looma-scope": "compensation:read",
        "parameters": [
          {
            "name": "person",
            "in": "path",
            "description": "The person id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "on",
            "in": "query",
            "description": "Effective date (YYYY-MM-DD). Defaults to today (UTC).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The effective-dated pay history and current pay as of the requested date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompensationRecord"
                },
                "example": {
                  "current": {
                    "amount": "65000.00",
                    "currency": "SEK",
                    "effectiveFrom": "2025-03-01",
                    "employmentId": "e-1",
                    "payBasis": "annual",
                    "revision": 3
                  },
                  "employmentId": "e-1",
                  "history": [
                    {
                      "amount": "62500.00",
                      "currency": "SEK",
                      "effectiveFrom": "2024-03-01",
                      "employmentId": "e-1",
                      "payBasis": "annual",
                      "revision": 2
                    }
                  ],
                  "personId": "p-1001",
                  "revision": 3
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "The effective date or person id is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request is invalid"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "No employee matched that id on the requested date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No employee matched that id on the requested date"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
      "post": {
        "operationId": "changeCompensation",
        "summary": "Submit an effective-dated pay change",
        "description": "Stages a pay change through the same path as the app's compensation form, with the same revision check and configurable compensation-change approval routing. The amount is an exact decimal string and is stored with its exact precision. Pay is HR and administrator only, exactly as in the app.",
        "tags": [
          "People"
        ],
        "x-looma-scope": "compensation:write",
        "parameters": [
          {
            "name": "person",
            "in": "path",
            "description": "The person id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "An opaque caller-chosen key, 1 to 200 characters. Replaying the same key with the same body returns the first response with replayed=true instead of creating a second change. Replaying it with a different body is a 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "requestBody": {
          "description": "The pay to change from effectiveFrom.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompensationChangeRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The change was staged and is now in the company's configured approval flow. It is not applied until that flow completes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                },
                "example": {
                  "personId": "p-1001",
                  "status": "PENDING_APPROVAL",
                  "transactionId": 4211
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "The body, the person id, the effective date or the revision is invalid. Unknown body fields are rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request is invalid"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "409": {
            "description": "The revision is stale, the record already exists, or the Idempotency-Key was used for a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request conflicts with the current state"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people/{person}/documents": {
      "get": {
        "operationId": "listEmployeeDocuments",
        "summary": "List employee documents",
        "description": "Employee document metadata for a person, newest first by id. The document bytes are not returned by this endpoint; use the download endpoint in the self-service API for file access. Employee fields pass the integration field-visibility policy. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Documents"
        ],
        "x-looma-scope": "documents:read",
        "parameters": [
          {
            "name": "person",
            "in": "path",
            "description": "The person id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor (document id) from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of employee document metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployeeDocumentPage"
                },
                "example": {
                  "hasMore": true,
                  "items": [
                    {
                      "archived": false,
                      "byteSize": 102400,
                      "category": "employment",
                      "contentType": "application/pdf",
                      "createdAt": "2026-01-15T10:30:00Z",
                      "employeeVisible": true,
                      "filename": "contract.pdf",
                      "id": "doc-1001",
                      "personId": "p-1001",
                      "revision": 1,
                      "title": "Employment Contract",
                      "uploadedBy": "hr-account"
                    }
                  ],
                  "nextCursor": "doc-1001"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people/{person}/employment": {
      "post": {
        "operationId": "changeEmployment",
        "summary": "Submit an effective-dated employment change",
        "description": "Stages a job, assignment or employment change through the same path as the app's employment form, including the same revision check and configurable employment-change approval routing. The change takes effect on effectiveFrom when the flow completes. HR and administrator tokens may change any person; a manager token only a current direct report.",
        "tags": [
          "People"
        ],
        "x-looma-scope": "people:write",
        "parameters": [
          {
            "name": "person",
            "in": "path",
            "description": "The person id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "An opaque caller-chosen key, 1 to 200 characters. Replaying the same key with the same body returns the first response with replayed=true instead of creating a second change. Replaying it with a different body is a 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "requestBody": {
          "description": "The employment values to change from effectiveFrom.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmploymentChangeRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The change was staged and is now in the company's configured approval flow. It is not applied until that flow completes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                },
                "example": {
                  "personId": "p-1001",
                  "status": "PENDING_APPROVAL",
                  "transactionId": 4211
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "The body, the person id, the effective date or the revision is invalid. Unknown body fields are rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request is invalid"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "409": {
            "description": "The revision is stale, the record already exists, or the Idempotency-Key was used for a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request conflicts with the current state"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people/{person}/history": {
      "get": {
        "operationId": "getPersonLifecycleHistory",
        "summary": "Read a person's lifecycle history",
        "tags": [
          "People"
        ],
        "x-looma-scope": "lifecycle:read",
        "parameters": [
          {
            "name": "person",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor-paginated lifecycle history."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people/{person}/manager": {
      "post": {
        "operationId": "changeManager",
        "summary": "Submit an effective-dated reporting-line change",
        "description": "Stages a manager change through the same path as the app's manager-change form, with the same revision check and configurable manager-change approval routing. An empty managerId removes the line manager. HR and administrator tokens may change any person; a manager token only a current direct report.",
        "tags": [
          "Organisation"
        ],
        "x-looma-scope": "org:write",
        "parameters": [
          {
            "name": "person",
            "in": "path",
            "description": "The person id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "An opaque caller-chosen key, 1 to 200 characters. Replaying the same key with the same body returns the first response with replayed=true instead of creating a second change. Replaying it with a different body is a 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "requestBody": {
          "description": "The reporting line to change from effectiveFrom.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerChangeRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The change was staged and is now in the company's configured approval flow. It is not applied until that flow completes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                },
                "example": {
                  "personId": "p-1001",
                  "status": "PENDING_APPROVAL",
                  "transactionId": 4211
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "The body, the person id, the effective date or the revision is invalid. Unknown body fields are rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request is invalid"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "409": {
            "description": "The revision is stale, the record already exists, or the Idempotency-Key was used for a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request conflicts with the current state"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/people/{person}/reviews": {
      "get": {
        "operationId": "listPersonReviews",
        "summary": "List a person's review assignments",
        "tags": [
          "People"
        ],
        "x-looma-scope": "performance:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–200; default 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "person",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor-paginated page of visible review cycles or reviews."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/performance/reviews": {
      "get": {
        "operationId": "listPerformanceReviewCycles",
        "summary": "List review cycles",
        "tags": [
          "People"
        ],
        "x-looma-scope": "performance:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–200; default 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor-paginated page of visible review cycles or reviews."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/positions": {
      "get": {
        "operationId": "listPositions",
        "summary": "List organisation positions",
        "tags": [
          "Organisation"
        ],
        "x-looma-scope": "org:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–200; default 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor-paginated page."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/teams": {
      "get": {
        "operationId": "listTeams",
        "summary": "List organisation teams",
        "tags": [
          "Organisation"
        ],
        "x-looma-scope": "org:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–200; default 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor-paginated page."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/time-off": {
      "get": {
        "operationId": "listTimeOff",
        "summary": "List approved time off",
        "description": "Approved, uncancelled time-off requests, newest first by transaction id. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Time off"
        ],
        "x-looma-scope": "timeoff:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor (transaction id) from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of leave requests.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimeOffPage"
                },
                "example": {
                  "hasMore": true,
                  "items": [
                    {
                      "endDate": "2026-07-10",
                      "halfDay": false,
                      "personId": "p-1001",
                      "planId": "plan-annual",
                      "startDate": "2026-07-01",
                      "transactionId": 42,
                      "units": 8
                    }
                  ],
                  "nextCursor": "41"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/timecards": {
      "get": {
        "operationId": "listTimecards",
        "summary": "List approved timecards",
        "description": "Approved timecards, newest first by id. Pass nextCursor back as cursor unchanged.",
        "tags": [
          "Time"
        ],
        "x-looma-scope": "timecards:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return. Values above the maximum are clamped.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque keyset cursor (timecard id) from a previous response's nextCursor.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated top-level properties to return for each record, for example fields=id,firstName,workEmail. A page's items, nextCursor and hasMore and each record's hiddenFields are always returned. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated nested collections to return, for example include=reportingLines. Naming one keeps it and drops the answer's other collections; it never adds data the endpoint would not otherwise return. A name this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Comma-separated field:value matches, all of which must hold, for example filter=status:active,department:People. It narrows the items of the page this read returns, so a filtered page may be shorter than limit; nextCursor still continues the collection. A field this resource does not carry, or a malformed match, is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Comma-separated fields to order the returned page by, descending for a field prefixed with '-', for example sort=-startDate,lastName. It orders the items of the page; the collection's own order still governs which rows the page contains and how nextCursor continues it. A field this resource does not carry is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "The 1-based page of the collection to return, up to the maximum. It is the cursor-free way to traverse a collection: the answer is this read's own answer for that page, with its own nextCursor and hasMore. Page N reads the collection N times, so it spends N of the token's request budget and issues N queries; a client traversing many pages should pass nextCursor back as cursor instead. It cannot be combined with cursor. A page past the collection's last one is an empty page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A cursor page of timecards.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimecardPage"
                },
                "example": {
                  "hasMore": false,
                  "items": [
                    {
                      "id": 5001,
                      "periodEnd": "2026-06-15",
                      "periodStart": "2026-06-01",
                      "personId": "p-1001",
                      "status": "approved",
                      "totalMinutes": 4800,
                      "transactionId": 42
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/webhooks/{webhook}/deliveries/replay": {
      "post": {
        "operationId": "replayWebhookDeliveries",
        "summary": "Replay webhook deliveries from the delivery log",
        "description": "Re-enqueues the exact stored payload of every delivery to this endpoint in the date range, bounded to the last 30 days. Receivers deduplicate replayed deliveries on the X-Looma-Delivery header. Administrator tokens only; this is integration administration, not an HR action.",
        "tags": [
          "Webhooks"
        ],
        "x-looma-scope": "webhooks:write",
        "parameters": [
          {
            "name": "webhook",
            "in": "path",
            "description": "The webhook endpoint id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "An opaque caller-chosen key, 1 to 200 characters. Replaying the same key with the same body returns the first response with replayed=true instead of creating a second change. Replaying it with a different body is a 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "requestBody": {
          "description": "The inclusive date range to replay.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeliveryReplayRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The number of deliveries re-enqueued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "replayed": 12
                }
              }
            }
          },
          "400": {
            "description": "The body, the person id, the effective date or the revision is invalid. Unknown body fields are rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request is invalid"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or unknown token. Every authentication failure returns the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "A valid API token is required"
                }
              }
            }
          },
          "403": {
            "description": "The token does not include the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The token is missing the required scope for this endpoint"
                }
              }
            }
          },
          "404": {
            "description": "The company workspace host does not match the token's company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "No company matches this workspace host"
                }
              }
            }
          },
          "409": {
            "description": "The revision is stale, the record already exists, or the Idempotency-Key was used for a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "The request conflicts with the current state"
                }
              }
            }
          },
          "429": {
            "description": "The per-token rate limit was exceeded. Retry after the current rolling minute. The budget is 600 read or 60 write requests per token in one rolling 1m0s.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Rate limit exceeded. Retry after the current minute."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "For a throttled (429) response, the number of seconds until the next rolling minute starts. Omitted otherwise.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-API-Version": {
                "description": "The semver release of this API contract that produced this response, so an integration can name the era it is running against.",
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "description": "The per-token request budget for this surface in one rolling minute: 600 for a read and 60 for a write.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "The number of requests the token may still make in the current minute. A running request has already spent one unit, and a refused request reports 0.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "The integration API is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Integration API is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "looma_pat_<token>",
        "description": "A scoped API token created in Settings → Configuration → Integrations. The plaintext is shown once at creation; only its SHA-256 hash is stored."
      }
    },
    "schemas": {
      "Application": {
        "type": "object",
        "description": "One vacancy application by an existing person.",
        "properties": {
          "coverNote": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          },
          "id": {
            "type": "integer"
          },
          "personId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          },
          "vacancyId": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "vacancyId",
          "personId",
          "status",
          "createdAt",
          "updatedAt"
        ]
      },
      "ApplicationPage": {
        "type": "object",
        "description": "A cursor page of Application records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Application"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "Candidate": {
        "type": "object",
        "description": "One candidate contact record. Candidate personal fields are blank and listed in hiddenFields under the integration field-visibility policy.",
        "properties": {
          "createdAt": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "firstName": {
            "type": "string"
          },
          "hiddenFields": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "id": {
            "type": "integer"
          },
          "lastName": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "stage": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "firstName",
          "lastName",
          "email",
          "stage",
          "createdAt",
          "updatedAt"
        ]
      },
      "CandidatePage": {
        "type": "object",
        "description": "A cursor page of Candidate records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Candidate"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "CompensationChangeRequest": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "string",
            "description": "An exact decimal string, for example 62500.00. It is stored with its exact precision."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 currency code."
          },
          "effectiveFrom": {
            "type": "string",
            "format": "date"
          },
          "employmentId": {
            "type": "string"
          },
          "payBasis": {
            "type": "string",
            "enum": [
              "annual",
              "monthly",
              "hourly"
            ]
          },
          "revision": {
            "type": "integer"
          }
        },
        "required": [
          "amount",
          "currency",
          "payBasis",
          "effectiveFrom",
          "revision"
        ]
      },
      "CompensationRecord": {
        "type": "object",
        "description": "One person's effective-dated pay history and current pay as of the requested date. The salary fields, the pay-zone range and the market context honour the integration field-visibility policy; a denied policy blanks them and names salary in hiddenFields. HR and administrator tokens only.",
        "properties": {
          "current": {
            "type": [
              "object",
              "null"
            ]
          },
          "employmentId": {
            "type": "string"
          },
          "hiddenFields": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "history": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "amount": {
                  "type": "string"
                },
                "currency": {
                  "type": "string"
                },
                "effectiveFrom": {
                  "type": "string",
                  "format": "date"
                },
                "employmentId": {
                  "type": "string"
                },
                "payBasis": {
                  "type": "string"
                },
                "revision": {
                  "type": "integer"
                }
              },
              "required": [
                "amount",
                "currency",
                "payBasis",
                "effectiveFrom",
                "revision"
              ]
            }
          },
          "market": {
            "type": [
              "object",
              "null"
            ]
          },
          "oneOffPayments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "amount": {
                  "type": "string"
                },
                "approvedAt": {
                  "type": [
                    "object",
                    "null"
                  ]
                },
                "createdAt": {
                  "type": "object"
                },
                "currency": {
                  "type": "string"
                },
                "employeeNumber": {
                  "type": "string"
                },
                "hiddenFields": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "id": {
                  "type": "integer"
                },
                "kind": {
                  "type": "string"
                },
                "payDate": {
                  "type": "string"
                },
                "payHidden": {
                  "type": "boolean"
                },
                "personId": {
                  "type": "string"
                },
                "personName": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                },
                "requestedBy": {
                  "type": "string"
                },
                "revision": {
                  "type": "integer"
                },
                "status": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "transactionId": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "updatedAt": {
                  "type": "object"
                }
              },
              "required": [
                "id",
                "personId",
                "kind",
                "title",
                "currency",
                "amount",
                "payDate",
                "status",
                "revision",
                "createdAt",
                "updatedAt"
              ]
            }
          },
          "personId": {
            "type": "string"
          },
          "priorPayChangePercent": {
            "type": "string"
          },
          "promotionLinked": {
            "type": "boolean"
          },
          "revision": {
            "type": "integer"
          },
          "totalCash": {
            "type": [
              "object",
              "null"
            ]
          },
          "zone": {
            "type": [
              "object",
              "null"
            ]
          }
        },
        "required": [
          "personId",
          "current",
          "revision",
          "promotionLinked"
        ]
      },
      "CreatePersonRequest": {
        "type": "object",
        "properties": {
          "businessTitle": {
            "type": "string"
          },
          "costCenter": {
            "type": "string"
          },
          "countryCode": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 country code."
          },
          "department": {
            "type": "string"
          },
          "effectiveFrom": {
            "type": "string",
            "format": "date",
            "description": "The employment's start date. The hire runs the company's configured hire approval flow."
          },
          "employeeNumber": {
            "type": "string",
            "description": "The employee's number, unique in the company."
          },
          "firstName": {
            "type": "string"
          },
          "grade": {
            "type": "string"
          },
          "initialCompensation": {
            "$ref": "#/components/schemas/CompensationChangeRequest"
          },
          "jobTitle": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "legalEntityId": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "workEmail": {
            "type": "string",
            "format": "email"
          },
          "workFromHome": {
            "type": "boolean"
          },
          "workerType": {
            "type": "string",
            "enum": [
              "employee",
              "manager",
              "contractor"
            ]
          }
        },
        "required": [
          "employeeNumber",
          "firstName",
          "lastName",
          "workEmail",
          "legalEntityId",
          "jobTitle",
          "department",
          "location",
          "effectiveFrom"
        ]
      },
      "DeliveryReplayRequest": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date",
            "description": "First delivery date to replay, within the last 30 days."
          },
          "until": {
            "type": "string",
            "format": "date",
            "description": "Last delivery date to replay, inclusive. Today or earlier."
          }
        },
        "required": [
          "from",
          "until"
        ]
      },
      "Employee": {
        "type": "object",
        "description": "One employee record with the effective-dated primary employment as of the requested date. Fields denied by the integration field-visibility policy are blank and listed in hiddenFields.",
        "properties": {
          "accountId": {
            "type": "string"
          },
          "businessTitle": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "department": {
            "type": "string"
          },
          "employeeNumber": {
            "type": "string"
          },
          "employmentId": {
            "type": "string"
          },
          "firstName": {
            "type": "string"
          },
          "hasPhoto": {
            "type": "boolean"
          },
          "hiddenFields": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "id": {
            "type": "string"
          },
          "jobTitle": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "legalEntity": {
            "type": "string"
          },
          "lifecycle": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "revision": {
            "type": "integer"
          },
          "startDate": {
            "type": "string",
            "format": "date"
          },
          "status": {
            "type": "string"
          },
          "workEmail": {
            "type": "string"
          },
          "workFromHome": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "workerType": {
            "type": "string"
          }
        },
        "required": [
          "status",
          "id",
          "employeeNumber",
          "firstName",
          "lastName",
          "workEmail",
          "startDate",
          "revision",
          "legalEntity",
          "jobTitle",
          "department",
          "location"
        ]
      },
      "EmployeeDocument": {
        "type": "object",
        "description": "One employee document's metadata. File bytes are not included; use the self-service download endpoint for file access. Employee fields are blank and listed in hiddenFields under the integration field-visibility policy.",
        "properties": {
          "archived": {
            "type": "boolean"
          },
          "byteSize": {
            "type": "integer"
          },
          "category": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          },
          "employeeVisible": {
            "type": "boolean"
          },
          "filename": {
            "type": "string"
          },
          "hiddenFields": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "id": {
            "type": "string"
          },
          "personId": {
            "type": "string"
          },
          "revision": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "uploadedBy": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "personId",
          "title",
          "category",
          "filename",
          "contentType",
          "byteSize",
          "employeeVisible",
          "archived",
          "createdAt",
          "uploadedBy",
          "revision"
        ]
      },
      "EmployeeDocumentPage": {
        "type": "object",
        "description": "A cursor page of EmployeeDocument records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EmployeeDocument"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "EmployeePage": {
        "type": "object",
        "description": "A cursor page of Employee records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Employee"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "EmploymentChangeRequest": {
        "type": "object",
        "properties": {
          "businessTitle": {
            "type": "string"
          },
          "costCenter": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "department": {
            "type": "string"
          },
          "effectiveFrom": {
            "type": "string",
            "format": "date",
            "description": "The date this change takes effect. A past date corrects history; a future date schedules it."
          },
          "employmentId": {
            "type": "string",
            "description": "The employment to change. Omit when the person has exactly one."
          },
          "grade": {
            "type": "string"
          },
          "jobTitle": {
            "type": "string"
          },
          "legalEntityId": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "revision": {
            "type": "integer",
            "description": "The revision the caller last read. A stale revision is a conflict, never a silent overwrite."
          },
          "workFromHome": {
            "type": "boolean"
          },
          "workerType": {
            "type": "string",
            "enum": [
              "employee",
              "manager",
              "contractor"
            ]
          }
        },
        "required": [
          "legalEntityId",
          "jobTitle",
          "department",
          "location",
          "effectiveFrom",
          "revision"
        ]
      },
      "Error": {
        "type": "object",
        "description": "A generic error. Messages never reveal whether a token exists, which scope was missing or whether a tenant matched.",
        "properties": {
          "message": {
            "type": "string",
            "description": "A human-readable, non-probing error message."
          }
        },
        "required": [
          "message"
        ]
      },
      "ExpenseClaim": {
        "type": "object",
        "description": "One expense claim with its headers and per-currency totals. The claimant's name is blank and listed in hiddenFields under the integration field-visibility policy.",
        "properties": {
          "createdAt": {
            "type": "string"
          },
          "employeeNumber": {
            "type": "string"
          },
          "hiddenFields": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "id": {
            "type": "integer"
          },
          "lineCount": {
            "type": "integer"
          },
          "personId": {
            "type": "string"
          },
          "personName": {
            "type": "string"
          },
          "revision": {
            "type": "integer"
          },
          "status": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "totals": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "currency": {
                  "type": "string"
                },
                "total": {
                  "type": "string"
                }
              },
              "required": [
                "currency",
                "total"
              ]
            }
          },
          "updatedAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "personId",
          "title",
          "status",
          "revision",
          "lineCount",
          "totals",
          "createdAt",
          "updatedAt"
        ]
      },
      "ExpenseClaimDetail": {
        "type": "object",
        "description": "One expense claim with its full line items and event history.",
        "properties": {
          "claim": {
            "$ref": "#/components/schemas/ExpenseClaim"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actorId": {
                  "type": "string"
                },
                "body": {
                  "type": "string"
                },
                "createdAt": {
                  "type": "string"
                },
                "id": {
                  "type": "integer"
                },
                "kind": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "kind",
                "createdAt"
              ]
            }
          },
          "lines": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "amount": {
                  "type": "string"
                },
                "category": {
                  "type": "string"
                },
                "categoryId": {
                  "type": "integer"
                },
                "currency": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "expenseDate": {
                  "type": "string"
                },
                "id": {
                  "type": "integer"
                },
                "merchant": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "expenseDate",
                "categoryId",
                "merchant",
                "currency",
                "amount"
              ]
            }
          },
          "totals": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "currency": {
                  "type": "string"
                },
                "total": {
                  "type": "string"
                }
              },
              "required": [
                "currency",
                "total"
              ]
            }
          },
          "transactionId": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "claim",
          "lines",
          "totals"
        ]
      },
      "ExpenseClaimPage": {
        "type": "object",
        "description": "A cursor page of ExpenseClaim records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExpenseClaim"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "LegalEntity": {
        "type": "object",
        "description": "A legal entity (company registration) within the tenant.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "ManagerChangeRequest": {
        "type": "object",
        "properties": {
          "effectiveFrom": {
            "type": "string",
            "format": "date"
          },
          "expectedRevision": {
            "type": "integer",
            "description": "The reporting-line revision the caller last read."
          },
          "managerId": {
            "type": "string",
            "description": "The new line manager's person id. An empty string removes the line manager."
          }
        },
        "required": [
          "managerId",
          "effectiveFrom",
          "expectedRevision"
        ]
      },
      "OrganisationCatalogueEntry": {
        "type": "object",
        "description": "An active workforce-catalogue entry (department, location, job, grade or cost centre).",
        "properties": {
          "code": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "kind",
          "code",
          "name"
        ]
      },
      "OrganisationReportingLine": {
        "type": "object",
        "description": "The current primary reporting line for a person at the requested effective date.",
        "properties": {
          "effectiveFrom": {
            "type": "string",
            "format": "date"
          },
          "managerId": {
            "type": "string"
          },
          "personId": {
            "type": "string"
          }
        },
        "required": [
          "personId",
          "effectiveFrom"
        ]
      },
      "OrganisationResponse": {
        "type": "object",
        "description": "Legal entities, the active workforce catalogue and current reporting lines.",
        "properties": {
          "catalogue": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganisationCatalogueEntry"
            }
          },
          "legalEntities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LegalEntity"
            }
          },
          "reportingLines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganisationReportingLine"
            }
          }
        },
        "required": [
          "legalEntities",
          "catalogue",
          "reportingLines"
        ]
      },
      "PayrollInput": {
        "type": "object",
        "description": "One payroll run's aggregate approved pay inputs (timecard minutes, flexible-benefit amount and approved-unpaid leave units) that feed its people.",
        "properties": {
          "flexAmount": {
            "type": "string"
          },
          "leaveUnits": {
            "type": "string"
          },
          "redacted": {
            "type": "boolean"
          },
          "runId": {
            "type": "integer"
          },
          "timecardMinutes": {
            "type": "string"
          }
        },
        "required": [
          "runId"
        ]
      },
      "PayrollInputPage": {
        "type": "object",
        "description": "A cursor page of PayrollInput records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PayrollInput"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "PayrollRun": {
        "type": "object",
        "description": "One payroll run with its period and legal entity. The totals are withheld and totalsRedacted is set when the integration viewer's field policy restricts compensation.",
        "properties": {
          "approvedAt": {
            "type": "string"
          },
          "id": {
            "type": "integer"
          },
          "legalEntityId": {
            "type": "string"
          },
          "legalEntityName": {
            "type": "string"
          },
          "lineCount": {
            "type": "integer"
          },
          "periodEnd": {
            "type": "string",
            "format": "date"
          },
          "periodId": {
            "type": "integer"
          },
          "periodStart": {
            "type": "string",
            "format": "date"
          },
          "revision": {
            "type": "integer"
          },
          "runKind": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "totalGross": {
            "type": "string"
          },
          "totalPayout": {
            "type": "string"
          },
          "totalsRedacted": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "legalEntityId",
          "periodId",
          "periodStart",
          "periodEnd",
          "status",
          "revision",
          "lineCount"
        ]
      },
      "PayrollRunPage": {
        "type": "object",
        "description": "A cursor page of PayrollRun records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PayrollRun"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "Payslip": {
        "type": "object",
        "description": "Metadata of one published payslip in a payroll run's period. The confidential PDF bytes are never part of a paged list.",
        "properties": {
          "byteSize": {
            "type": "integer"
          },
          "filename": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "personId": {
            "type": "string"
          },
          "publishedAt": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "version": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "personId",
          "version",
          "filename",
          "status",
          "byteSize"
        ]
      },
      "PayslipPage": {
        "type": "object",
        "description": "A cursor page of Payslip records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Payslip"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "ReceiveExpenseReceiptRequest": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string",
            "format": "byte",
            "description": "Base64-encoded PDF, PNG or JPEG attachment, at most 10 MiB after decoding."
          },
          "deliveryKey": {
            "type": "string",
            "description": "Stable key per attachment, 1 to 200 characters. Retries return the original intake id."
          },
          "filename": {
            "type": "string",
            "description": "Attachment filename, 1 to 180 characters, without path separators."
          },
          "routingToken": {
            "type": "string",
            "description": "The employee's enabled 64-character hexadecimal recipient capability."
          }
        },
        "required": [
          "routingToken",
          "deliveryKey",
          "filename",
          "content"
        ]
      },
      "RecruitingJob": {
        "type": "object",
        "description": "One job (vacancy) and its current state.",
        "properties": {
          "closingDate": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          },
          "department": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "id": {
            "type": "integer"
          },
          "openingDate": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "title",
          "openingDate",
          "status",
          "createdAt",
          "updatedAt"
        ]
      },
      "RecruitingJobPage": {
        "type": "object",
        "description": "A cursor page of RecruitingJob records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecruitingJob"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "SubmitExpenseRequest": {
        "type": "object",
        "properties": {
          "lines": {
            "type": "array",
            "description": "At least one line item (up to 50).",
            "items": {
              "type": "object",
              "properties": {
                "amount": {
                  "type": "string",
                  "description": "The exact amount, up to four decimal digits."
                },
                "categoryId": {
                  "type": "integer"
                },
                "currency": {
                  "type": "string",
                  "description": "ISO 4217 three-letter currency code."
                },
                "description": {
                  "type": "string"
                },
                "expenseDate": {
                  "type": "string",
                  "format": "date"
                },
                "merchant": {
                  "type": "string"
                }
              },
              "required": [
                "expenseDate",
                "categoryId",
                "merchant",
                "currency",
                "amount"
              ]
            }
          },
          "title": {
            "type": "string",
            "description": "The claim's title, 1 to 160 characters."
          }
        },
        "required": [
          "title",
          "lines"
        ]
      },
      "TimeOffPage": {
        "type": "object",
        "description": "A cursor page of TimeOffRequest records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TimeOffRequest"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "TimeOffRequest": {
        "type": "object",
        "description": "One approved, uncancelled time-off request.",
        "properties": {
          "endDate": {
            "type": "string",
            "format": "date"
          },
          "halfDay": {
            "type": "boolean"
          },
          "personId": {
            "type": "string"
          },
          "planId": {
            "type": "string"
          },
          "startDate": {
            "type": "string",
            "format": "date"
          },
          "transactionId": {
            "type": "integer"
          },
          "units": {
            "type": "integer"
          }
        },
        "required": [
          "transactionId",
          "personId",
          "planId",
          "startDate",
          "endDate",
          "units",
          "halfDay"
        ]
      },
      "Timecard": {
        "type": "object",
        "description": "One approved timecard with its period and total worked minutes.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "periodEnd": {
            "type": "string",
            "format": "date"
          },
          "periodStart": {
            "type": "string",
            "format": "date"
          },
          "personId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "totalMinutes": {
            "type": "integer"
          },
          "transactionId": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "personId",
          "periodStart",
          "periodEnd",
          "status",
          "totalMinutes"
        ]
      },
      "TimecardPage": {
        "type": "object",
        "description": "A cursor page of Timecard records.",
        "properties": {
          "hasMore": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Timecard"
            }
          },
          "nextCursor": {
            "type": "string"
          }
        },
        "required": [
          "items",
          "hasMore"
        ]
      },
      "WriteResult": {
        "type": "object",
        "description": "The outcome of a write request. Replaying the same Idempotency-Key returns the same transactionId with replayed=true.",
        "properties": {
          "personId": {
            "type": "string"
          },
          "replayed": {
            "type": "boolean"
          },
          "status": {
            "type": "string"
          },
          "transactionId": {
            "type": "integer"
          }
        },
        "required": [
          "transactionId",
          "status"
        ]
      }
    }
  },
  "x-looma-scopes": [
    {
      "key": "people:read",
      "label": "People and directory",
      "description": "Read employee directory records and a single employee record."
    },
    {
      "key": "org:read",
      "label": "Organisation",
      "description": "Read legal entities, workforce catalogues and current reporting lines."
    },
    {
      "key": "timeoff:read",
      "label": "Time off",
      "description": "Read approved time-off requests."
    },
    {
      "key": "timecards:read",
      "label": "Approved time",
      "description": "Read approved timecards and their totals."
    },
    {
      "key": "people:write",
      "label": "People and directory (write)",
      "description": "Create a person and submit effective-dated job, assignment and employment changes. Every change runs the company's configured approval routing."
    },
    {
      "key": "org:write",
      "label": "Organisation (write)",
      "description": "Submit effective-dated reporting-line changes. Every change runs the company's configured manager-change approval routing."
    },
    {
      "key": "compensation:write",
      "label": "Compensation (write)",
      "description": "Submit effective-dated pay changes for a person. HR and administrator tokens only. Every change runs the company's configured pay approval routing."
    },
    {
      "key": "webhooks:write",
      "label": "Webhooks (write)",
      "description": "Replay webhook deliveries from the delivery log. Administrator tokens only."
    },
    {
      "key": "assistant:read",
      "label": "Assistant policy Q&A (read-only)",
      "description": "Use the assistant policy Q&A surface at /assistant/qa. The assistant gathers the token owner's in-audience handbook policies, help articles, readable development plans and assigned learning courses as cited sources; every call is rate limited and audited, and the company must have the assistant enabled. No HR data is sent to an unenabled provider."
    },
    {
      "key": "analytics:read",
      "label": "Analytics (read)",
      "description": "Read tenant headcount and workforce trends."
    },
    {
      "key": "benefits:read",
      "label": "Benefits (read)",
      "description": "Read benefit plans and enrolments. Enrolment details are limited to HR and administrators, or an employee's own record."
    },
    {
      "key": "automations.events:write",
      "label": "Automation events (write)",
      "description": "Submit typed business events to explicitly registered sources."
    },
    {
      "key": "compensation:read",
      "label": "Compensation (read)",
      "description": "Read a person's effective-dated pay history and current pay. HR and administrator tokens only. Amounts honour the integration field-visibility policy."
    },
    {
      "key": "documents:read",
      "label": "Employee documents (read)",
      "description": "Read employee document metadata. Employee fields pass the integration field-visibility policy."
    },
    {
      "key": "expenses:receive",
      "label": "Expense receipts (receive)",
      "description": "Deliver receipt attachments to an employee's enabled routing token as private proposals. Never submits claims."
    },
    {
      "key": "expenses:read",
      "label": "Expenses (read)",
      "description": "Read expense claims. The claimant's name passes the field-visibility resolver under the integration viewer role."
    },
    {
      "key": "expenses:write",
      "label": "Expenses (write)",
      "description": "Submit a new expense claim for the token's linked employee through the company's configured EXPENSE_CLAIM approval flow."
    },
    {
      "key": "lifecycle:read",
      "label": "Lifecycle (read)",
      "description": "Read a person's hire, employment change, leave and termination history."
    },
    {
      "key": "objectives:read",
      "label": "Objectives (read)",
      "description": "Read current-cycle objectives within the token owner's visibility."
    },
    {
      "key": "payroll:read",
      "label": "Payroll",
      "description": "Read payroll runs, a run's payslip metadata and the approved pay inputs that feed a run. Compensation fields pass the field-visibility resolver under the integration viewer role."
    },
    {
      "key": "performance:read",
      "label": "Performance (read)",
      "description": "Read performance review cycles and a person's review assignments. Company tokens see all cycles and reviews; a manager sees their direct reports' reviews; an employee sees their own. Ratings only appear once released."
    },
    {
      "key": "recruiting:read",
      "label": "Recruiting (read)",
      "description": "Read jobs, candidates and applications. Candidate personal fields pass the integration field-visibility policy."
    },
    {
      "key": "mcp:read",
      "label": "MCP server (AI assistants)",
      "description": "Call the read-only MCP server at /mcp from an AI assistant. Every tool runs with this token owner's own permissions and field visibility, each call is rate limited and audited, and the company must have the MCP server enabled. No write tools are reachable without a data scope."
    },
    {
      "key": "performance:write",
      "label": "MCP performance review write",
      "description": "Draft or submit a performance-review answer set through the MCP server, always as the token owner and under the same in-product role, employment, effective-date, revision and approval rules that the review screen applies. The performance:read scope is unchanged and separate; this scope only buys the write tools, which the product refuses exactly where it refuses the same save in-app."
    },
    {
      "key": "oneonones:write",
      "label": "MCP 1:1 note write",
      "description": "Save the token owner's private 1:1 notes through the one-on-one module."
    },
    {
      "key": "feedback:write",
      "label": "MCP feedback write",
      "description": "Give feedback through the MCP server as the token owner under the feedback module's own recipient, visibility and company-setting rules. Feedback text is excluded from MCP call audit details."
    },
    {
      "key": "timeoff:write",
      "label": "MCP time-off request write",
      "description": "Submit the token owner's own time-off request through the leave module's validation and approval rules. Request notes are excluded from MCP call audit details."
    }
  ],
  "x-looma-webhook-events": [
    {
      "key": "employee.created",
      "label": "Employee created",
      "description": "A hire transaction completed and created an employee record."
    },
    {
      "key": "employee.updated",
      "label": "Employee updated",
      "description": "An approved change updated employment, reporting, pay or working time."
    },
    {
      "key": "employee.terminated",
      "label": "Employee terminated",
      "description": "An approved end-of-employment transaction completed."
    },
    {
      "key": "leave.approved",
      "label": "Leave approved",
      "description": "A leave request was approved and booked."
    },
    {
      "key": "transaction.decided",
      "label": "Transaction decided",
      "description": "Any transaction reached a final approved or declined decision."
    },
    {
      "key": "candidate.status",
      "label": "Candidate status changed",
      "description": "A candidate moved to a new pipeline stage."
    },
    {
      "key": "audit.event",
      "label": "Audit trail event",
      "description": "Every recorded HR audit event, projected to identifiers and the audit browser's fields, near-real-time with retry and a bounded replay window."
    },
    {
      "key": "payroll.run.approved",
      "label": "Payroll run approved",
      "description": "A payroll run reached the approved state after HR sign-off."
    },
    {
      "key": "expense.submitted",
      "label": "Expense claim submitted",
      "description": "A new expense claim was submitted and entered the company's configured approval flow."
    }
  ]
}
