{"openapi":"3.1.0","info":{"title":"peppol.sh API","version":"2.1.0","description":"Send Peppol documents (invoices, credit notes, etc.) from your app. JSON in, e-invoice out, delivered via Peppol.\n\n## Quick Start (zero UI required)\n\n```bash\n# 1. Create an account — returns a sandbox API key instantly\ncurl -X POST https://api.peppol.sh/v1/signup \\\n  -d '{\"email\": \"agent@bot.com\"}'\n\n# 2. Create a company to send documents from\ncurl -X POST https://api.peppol.sh/v1/companies \\\n  -H \"Authorization: Bearer ps_test_...\" \\\n  -d '{\"name\": \"Acme BV\", \"tax_id\": \"BE0123456789\", \"country\": \"BE\"}'\n\n# 3. Send a test document via that company\ncurl -X POST https://api.peppol.sh/v1/documents \\\n  -H \"Authorization: Bearer ps_test_...\" \\\n  -d '{\"company_id\": \"com_...\", \"number\": \"INV-001\", ...}'\n\n# 4. Invite a team member to your workspace\ncurl -X POST https://api.peppol.sh/v1/workspaces/wsp_.../members \\\n  -H \"Authorization: Bearer ps_test_...\" \\\n  -d '{\"email\": \"jan@acme.be\", \"role\": \"admin\"}'\n```\n\n## Core Concepts\n\n### Accounts\nAn **account** represents any API consumer — an autonomous agent, a human\ndeveloper, or a service account. Every account authenticates with long-lived\nbearer API keys (`ps_test_*` for sandbox, `ps_live_*` for production).\n\n### Workspaces\nA **workspace** is the top-level aggregation, billing, and access-control boundary.\nWorkspaces contain companies, hold the credit balance, and own API keys and webhooks.\nOn signup, every account automatically gets a personal workspace where they are the owner.\n\n### Companies\nA **company** is a business entity that sends documents. Companies belong to\nexactly one workspace and hold their own provider credentials, tax IDs, and\nPeppol registrations.\n\n### Membership & Roles\nThe `workspace_members` relationship links accounts to workspaces with a role:\n- **owner** — full access, can manage members, workspaces, and all settings\n- **admin** — can manage members and workspace settings, can mint API keys\n- **member** — can send documents and view data, cannot manage the workspace\n\nThe account that creates a workspace automatically becomes its owner.\n\n## Authentication\n```\nAuthorization: Bearer ps_test_...   (sandbox)\nAuthorization: Bearer ps_live_...   (production)\n```\n\n**Public endpoints** (no key needed):\n- `POST /v1/signup` — create an account and get a sandbox API key\n- `GET /v1/health` — health check\n- `GET /v1/lookup/{peppol_id}` — lookup a Peppol participant (rate-limited per IP)\n- `GET /v1/lookup/{peppol_id}/dns` — DNS-only resolution (rate-limited per IP)\n- `GET /v1/openapi.json` — this spec\n\n**All other endpoints** require a valid `Authorization: Bearer <api_key>` header.\n\nKeys created in the dashboard or via the API work identically on all endpoints.\nSandbox keys (`ps_test_*`) route to the sandbox environment; live keys (`ps_live_*`) route to production.\nThere is no per-endpoint scoping — a valid key grants access to everything the account has access to.\n\n## Bootstrap Flow\n```\n1. POST /v1/signup           → account + personal workspace + sandbox API key\n2. POST /v1/companies        → company in the workspace\n3. POST /v1/documents        → send documents via that company\n4. POST /v1/workspaces/:id/members  → invite team members to the workspace\n```\n\n## Environments\n| Environment | Base URL | Delivery |\n|---|---|---|\n| Sandbox | `sandbox.peppol.sh` | Email only |\n| Production | `api.peppol.sh` | Real Peppol network |\n\n## Rate Limits\n- Standard: 100 requests/min\n- Pro: 1,000 requests/min\n\nRate-limited responses include `Retry-After` header.\n","contact":{"name":"peppol.sh Support","email":"support@peppol.sh","url":"https://peppol.sh"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://api.peppol.sh","description":"Production"},{"url":"https://sandbox.peppol.sh","description":"Sandbox (email delivery, no real Peppol)"},{"url":"http://localhost:8787","description":"Local development"}],"security":[{"BearerAuth":[]}],"tags":[{"name":"Workspace","description":"Workspace management: CRUD, membership, and billing.\nA workspace is the top-level boundary for companies, API keys, credits, and webhooks.\n"},{"name":"Account","description":"Signup, API key management, and account details.\n\nAn account is the identity you authenticate with. It can represent an\nAI agent, a human developer, or a service. Accounts are linked to\ncompanies via memberships.\n"},{"name":"Companies","description":"Create and manage companies (business entities).\n\nA company holds provider credentials, a tax ID, and Peppol registration.\nDocuments are always sent **from** a company. Team access is granted at\nthe workspace level — see the workspace member endpoints.\n"},{"name":"KYC","description":"Know-Your-Customer verification for going live on the real Peppol network.\n\n## Workspace-level model\n\nKYC is **workspace-level**, not company-level. One verification covers the\nwhole workspace and every company in it. The workspace carries a single\n`status`:\n\n```\nnone ──▶ pending ──▶ approved   (approved is terminal)\n            │\n            └──────▶ rejected ──▶ pending   (fix and resubmit)\n```\n\n- **none** — nothing submitted yet. This is where a fresh workspace starts.\n- **pending** — submitted and awaiting review by peppol.sh ops. Uploads are\n  locked while pending.\n- **approved** — ops accepted the submission. **Terminal.** On approval the\n  workspace organisation is onboarded with the e-invoice.be provider and\n  every existing company is provisioned for Tier-2 go-live. From then on,\n  any company you create in the workspace **auto-provisions** on creation\n  (see `POST /v1/companies`).\n- **rejected** — ops declined; `reject_reason` explains why. Fix the issue\n  and submit again (this returns the workspace to `pending`).\n\nReview is performed manually by peppol.sh operations. There is no\nprogrammatic approval — you submit, then wait for a human decision.\n\n## The own-company anchor\n\nOne company in the workspace is the **own company** — the legal entity that\noperates the workspace and signs the attestation. Its id is echoed as\n`company_id` in `GET /v1/kyc` and passed as `company_id` to\n`POST /v1/kyc/submit`. The own company is **exempt** from the mandate\nrequirement (you do not hold a mandate from yourself).\n\n## Attestation-only gate\n\nSubmission is gated by a versioned **attestation** — a checkbox statement\nthe signer accepts on behalf of the workspace legal entity. The current\nattestation version is `2026-08-26`. The full attestation text and the\ncurrent version are returned in `GET /v1/kyc` under `attestation.text` and\n`attestation.current_version`; echo that exact version back in\n`attestation_version` when you submit.\n\nThree workspace-level documents are **required** before you can submit:\n`registry_extract`, `representative_id`, and `authority_proof`. Per-company\n`mandate` documents are **optional supporting evidence** — they never gate\nsubmission (they are surfaced as `mandate_coverage` for ops context only).\n\n## Go live — walkthrough\n\n1. **Create your own company** with `POST /v1/companies` (the legal entity\n   that operates the workspace).\n2. **Upload the three workspace documents** with `POST /v1/kyc/documents`\n   (`registry_extract`, `representative_id`, `authority_proof`). Optionally\n   upload `mandate` documents for the other companies you represent.\n3. **Read the attestation** with `GET /v1/kyc` — take `attestation.text` and\n   `attestation.current_version`, and check `requirements.can_submit`.\n4. **Submit** with `POST /v1/kyc/submit`, passing the own `company_id`,\n   legal name, enterprise number, signer name/role, `attested: true`, and\n   `attestation_version` equal to the current version. Status becomes\n   `pending`.\n5. **Wait for review.** peppol.sh ops approve or reject.\n6. On **approval** the workspace org is onboarded and every company is\n   provisioned automatically. Newly created companies then go live on\n   creation with no further KYC.\n\n## Company liveness (Connect)\n\nA company is **live** once it has a provider tenant; each company reports\nthis as `is_live`. For Connect platforms the model is: verify the\nworkspace once, then onboard as many companies as you like — every one\ngoes live on `POST /v1/companies` with no per-company re-verification.\n"},{"name":"Documents","description":"Send Peppol documents (invoices, credit notes, etc.).\n\nAll document operations require a `company_id` — either in the request body\n(for POST) or as a query parameter (for GET). The calling account must be a\nmember of that company.\n"},{"name":"Lookup","description":"Check Peppol network registration for a tax ID"},{"name":"Validate","description":"Validate document payloads without sending"},{"name":"Events","description":"Chronological feed of document lifecycle events (queued, sending,\ndelivered, failed, retry) across the workspace, with cursor-based\npagination and filtering.\n"},{"name":"Webhooks","description":"Real-time HTTP notifications for document lifecycle and account events.\nRegister an endpoint with `POST /v1/webhooks`, subscribe to one or more\nevent types, and we POST a signed JSON payload to your URL every time a\nmatching event fires.\n\n## Delivery request\n\nEvery delivery is an HTTP `POST` with `Content-Type: application/json`\nand these headers:\n\n| Header | Meaning |\n|---|---|\n| `X-Peppol-Signature-V2` | **Recommended.** Timestamped HMAC signature (see below). Use this to verify. |\n| `X-Peppol-Signature` | Legacy HMAC of the raw body only. Kept byte-identical forever; no replay protection. |\n| `X-Peppol-Timestamp` | Unix epoch seconds when the delivery was signed. Same value as `t=` in the V2 header. |\n| `X-Peppol-Event` | The event type, e.g. `document.delivered`. |\n| `X-Peppol-Delivery-Id` | Unique delivery id (`whd_…`). **Stable across retries — use it as an idempotency key.** |\n\nThe request body is the exact JSON string that was signed. Verify the\nsignature against the **raw received bytes, before you JSON-parse** —\nre-serializing the parsed object can change byte-for-byte spacing and\nbreak the HMAC.\n\n## Signature algorithm\n\nBoth headers use **HMAC-SHA256**. The key is the webhook signing secret\nas UTF-8 bytes; the output is lowercase hex (64 characters).\n\n- **`X-Peppol-Signature-V2` (recommended).** Format:\n  `t=<timestamp>,v1=<hex>` with one `v1=` per active signing secret\n  (current secret first). The signed message is the timestamp, a literal\n  `.`, then the raw body:\n\n  ```\n  signed_string = \"<X-Peppol-Timestamp>\" + \".\" + <raw request body>\n  v1 = hex( HMAC_SHA256(secret, signed_string) )\n  ```\n\n  Because the timestamp is inside the HMAC, a receiver that rejects old\n  timestamps (e.g. more than 5 minutes of clock skew) gets true replay\n  protection. During a secret rotation the header carries two `v1=`\n  values — accept the delivery if **any** of them matches.\n\n- **`X-Peppol-Signature` (legacy).** HMAC of the raw body only; the\n  timestamp travels unsigned in `X-Peppol-Timestamp`, so it gives no\n  replay protection. Provided for backward compatibility — prefer V2 for\n  new integrations. During a secret rotation the legacy header\n  switches to the new secret immediately (no overlap), so a rotating\n  integration that still verifies the legacy header can miss deliveries.\n\n## Signing secret\n\nThe secret has the form `whsec_` followed by a 40-character token. It is\nreturned **once** in the `201` response from `POST /v1/webhooks` (and once\nagain from a rotation) and is never retrievable afterwards — `GET`\nresponses omit it. Store it securely.\n\n**Rotation.** `POST /v1/webhooks/{id}/rotate-secret` mints a new secret\n(returned once) and moves the previous one into a 24-hour overlap window.\nDuring that window `X-Peppol-Signature-V2` is signed with both secrets\n(new first, one `v1=` each) so you can roll the new secret out to your\nverifier without dropping deliveries. At most two secrets are active;\nrotating again before the window closes drops the older one.\n\n## Retries, idempotency & failure\n\nDeliveries are queued and retried with backoff on any non-2xx response or\nnetwork error, up to **5 attempts**. After the final failed attempt the\ndelivery is marked `failed` (and lands in a dead-letter queue). Redirects\nare never followed (SSRF hardening) and each attempt has a 10-second\ntimeout. Return a `2xx` status quickly to acknowledge receipt; do heavy\nwork asynchronously. Because `X-Peppol-Delivery-Id` is stable across\nretries, **deduplicate on it** so a retried delivery is processed once.\nInspect attempts via `GET /v1/webhooks/{id}/deliveries`.\n\n## Events & payload shape\n\nSubscribe to any of: `document.queued`, `document.sending`,\n`document.delivered`, `document.failed`, `credits.low` (or `*` for all).\nA synthetic `webhook.test` event is sent by `POST /v1/webhooks/{id}/test`.\n\n`document.*` payload:\n\n```json\n{\n  \"id\": \"evt_9fJ2kQ7bTx0Lm4Ne1Ry8\",\n  \"type\": \"document.delivered\",\n  \"created_at\": \"2026-03-05T12:00:00.000Z\",\n  \"data\": {\n    \"document_id\": \"doc_abc123\",\n    \"company_id\": \"com_abc123\",\n    \"status\": \"delivered\",\n    \"number\": \"INV-2026-001\",\n    \"error_message\": null\n  }\n}\n```\n\n`credits.low` payload:\n\n```json\n{\n  \"id\": \"evt_3aB7cD9eF1gH2iJ4kL6mN\",\n  \"type\": \"credits.low\",\n  \"created_at\": \"2026-03-05T12:00:00.000Z\",\n  \"data\": { \"company_id\": \"com_abc123\", \"balance\": 8, \"threshold\": 10 }\n}\n```\n\n## Verifying a delivery\n\nCompute the expected V2 signature over `timestamp + \".\" + rawBody` and\ncompare with a **constant-time** equality check. Reject stale timestamps\nto defeat replay.\n\n**Node.js / TypeScript** (`crypto.timingSafeEqual`):\n\n```ts\nimport crypto from \"node:crypto\";\n\n// rawBody is the exact received body string — verify BEFORE JSON.parse.\nexport function verifyWebhook(\n  rawBody: string,\n  headerV2: string, // X-Peppol-Signature-V2\n  secret: string,\n  toleranceSec = 300,\n): boolean {\n  const parts = headerV2.split(\",\");\n  const t = parts.find((p) => p.startsWith(\"t=\"))?.slice(2);\n  const sigs = parts.filter((p) => p.startsWith(\"v1=\")).map((p) => p.slice(3));\n  if (!t || sigs.length === 0) return false;\n  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;\n\n  const expected = crypto\n    .createHmac(\"sha256\", secret)\n    .update(`${t}.${rawBody}`)\n    .digest(\"hex\");\n  const expectedBuf = Buffer.from(expected);\n  return sigs.some(\n    (sig) =>\n      sig.length === expected.length &&\n      crypto.timingSafeEqual(expectedBuf, Buffer.from(sig)),\n  );\n}\n```\n\n**Python** (`hmac.compare_digest`):\n\n```python\nimport hashlib, hmac, time\n\ndef verify_webhook(raw_body: bytes, header_v2: str, secret: str,\n                   tolerance: int = 300) -> bool:\n    t, sigs = None, []\n    for part in header_v2.split(\",\"):\n        key, _, val = part.partition(\"=\")\n        if key == \"t\":\n            t = val\n        elif key == \"v1\":\n            sigs.append(val)\n    if t is None or not sigs:\n        return False\n    if abs(time.time() - int(t)) > tolerance:\n        return False\n    signed = f\"{t}.\".encode() + raw_body  # raw_body verified before json.loads\n    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()\n    return any(hmac.compare_digest(expected, sig) for sig in sigs)\n```\n\n**PHP** (`hash_equals`):\n\n```php\nfunction verify_webhook(string $rawBody, string $headerV2,\n                        string $secret, int $tolerance = 300): bool {\n    $t = null; $sigs = [];\n    foreach (explode(',', $headerV2) as $part) {\n        [$k, $v] = array_pad(explode('=', $part, 2), 2, '');\n        if ($k === 't')  $t = $v;\n        elseif ($k === 'v1') $sigs[] = $v;\n    }\n    if ($t === null || !$sigs) return false;\n    if (abs(time() - (int) $t) > $tolerance) return false;\n\n    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);\n    foreach ($sigs as $sig) {\n        if (hash_equals($expected, $sig)) return true;\n    }\n    return false;\n}\n```\n"},{"name":"Audit","description":"Append-only audit trail for security-relevant account and workspace events.\nRecords API key creation/revocation, membership changes, company mutations,\nand webhook lifecycle events.\n"},{"name":"Health","description":"Public health check for uptime monitoring and connectivity probes"}],"paths":{"/v1/health":{"get":{"operationId":"getHealth","tags":["Health"],"summary":"Health check","description":"Returns the operational status of the API. Probes database connectivity.\nReturns 200 when healthy, 503 when degraded. No authentication required.\n","security":[],"responses":{"200":{"description":"API is healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"status":"ok","version":"2.0.0","environment":"production","checks":{"db":"ok"}}}}},"503":{"description":"API is degraded (one or more checks failed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"status":"degraded","version":"2.0.0","environment":"production","checks":{"db":"error"}}}}}}}},"/v1/workspaces":{"post":{"operationId":"createWorkspace","tags":["Workspace"],"summary":"Create a workspace","description":"Creates a new workspace with the authenticated account as owner.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"201":{"description":"Workspace created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"}}},"get":{"operationId":"listWorkspaces","tags":["Workspace"],"summary":"List workspaces","description":"Returns all workspaces the authenticated account is a member of.","responses":{"200":{"description":"Workspace list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Workspace"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/v1/workspaces/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getWorkspace","tags":["Workspace"],"summary":"Get workspace details","description":"Returns the workspace including the caller's `role`, credit balance,\nsuspension flag, and operating `mode`. The calling account must be a\nmember of the workspace.\n","responses":{"200":{"description":"Workspace detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateWorkspace","tags":["Workspace"],"summary":"Update workspace","description":"Only workspace owners and admins can update workspace details.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteWorkspace","tags":["Workspace"],"summary":"Delete workspace","description":"Only workspace owners can delete a workspace. Deletion is refused if\nany member account would be left without an owner workspace.\n","responses":{"200":{"description":"Workspace deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/workspaces/{id}/members":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"listWorkspaceMembers","tags":["Workspace"],"summary":"List workspace members","description":"Returns every account that is a member of the workspace, with each\nmember's role. The calling account must be a member of the workspace.\n","responses":{"200":{"description":"Member list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMember"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"operationId":"inviteWorkspaceMember","tags":["Workspace"],"summary":"Invite a member to the workspace","description":"Only workspace owners and admins can invite members.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"account_id":{"type":"string"},"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","admin","member"],"default":"member"}}}}}},"responses":{"201":{"description":"Member invited"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/workspaces/{id}/members/{accountId}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"accountId","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"changeWorkspaceMemberRole","tags":["Workspace"],"summary":"Change a member's role","description":"Only workspace owners can change roles.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role"],"properties":{"role":{"type":"string","enum":["owner","admin","member"]}}}}}},"responses":{"200":{"description":"Role updated"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"removeWorkspaceMember","tags":["Workspace"],"summary":"Remove a member from the workspace","description":"Only workspace owners and admins can remove members.\nRemoving a member cascade-revokes their API keys for this workspace.\n","responses":{"200":{"description":"Member removed"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/workspaces/{id}/transfer-ownership":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"operationId":"transferWorkspaceOwnership","tags":["Workspace"],"summary":"Transfer workspace ownership","description":"Makes the target account an owner and demotes the caller to admin.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["account_id"],"properties":{"account_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Ownership transferred"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/signup":{"post":{"operationId":"signup","tags":["Account"],"summary":"Create account","description":"Create a new account and receive a sandbox API key instantly.\nNo UI, no email verification, no credit card required.\n\nThe returned `api_key` is the full key — **store it securely**, it is\nonly shown once. Use it in the `Authorization: Bearer <key>` header\nfor all subsequent requests.\n\nAfter signup, create a company with `POST /v1/companies` to start\nsending documents.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"Unique email address for this account"},"name":{"type":"string","description":"Display name (optional)"}}},"examples":{"agent":{"summary":"Agent signup","value":{"email":"agent@bot.com"}},"human":{"summary":"Human signup","value":{"email":"dev@company.com","name":"Jane Developer"}}}}}},"responses":{"201":{"description":"Account created with sandbox API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResponse"},"example":{"id":"acc_abc123def456","email":"agent@bot.com","status":"active","api_key":"ps_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"}}}},"400":{"description":"Invalid email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"409":{"description":"Email already registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"validation_error","message":"An account with this email already exists","code":"email_taken"}}}}}}}},"/v1/account":{"get":{"operationId":"getAccount","tags":["Account"],"summary":"Get account details","description":"Returns the authenticated account's profile, API keys (prefixes only —\nnever full keys), and aggregate usage statistics.\n","responses":{"200":{"description":"Account details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/v1/account/keys":{"post":{"operationId":"createKey","tags":["Account"],"summary":"Create API key","description":"Create an additional API key for this account.\n\n- `sandbox: true` (default) — creates a `ps_test_*` key for sandbox use\n- `sandbox: false` — creates a `ps_live_*` key for production use\n\nThe full key is returned **once** in the response. Store it securely.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","description":"Human-readable label to identify this key"},"sandbox":{"type":"boolean","default":true,"description":"`false` creates a production key"}}},"example":{"label":"CI/CD pipeline","sandbox":true}}}},"responses":{"201":{"description":"Key created (full key shown only once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/account/keys/{prefix}":{"delete":{"operationId":"revokeKey","tags":["Account"],"summary":"Revoke API key","description":"Permanently revoke an API key by its prefix. The key will immediately\nstop working for authentication. This action cannot be undone.\n","parameters":[{"name":"prefix","in":"path","required":true,"schema":{"type":"string"},"description":"Key prefix as shown in `GET /v1/account` (e.g., `ps_test_a1b2c3...`)","example":"ps_test_a1b2c3..."}],"responses":{"200":{"description":"Key revoked","content":{"application/json":{"schema":{"type":"object","properties":{"revoked":{"type":"boolean","example":true},"prefix":{"type":"string"}}}}}},"404":{"description":"Key not found or already revoked"}}}},"/v1/account/audit":{"get":{"operationId":"listAccountAuditEvents","tags":["Audit"],"summary":"List audit events for the calling account","description":"Returns paginated audit events from all workspaces where the calling\naccount is a member. Events include API key creation/revocation,\nmembership changes, company mutations, and webhook lifecycle events.\n","security":[{"bearerAuth":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response's `next_cursor` field","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list of audit events","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditEvent"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/v1/workspaces/{id}/audit":{"get":{"operationId":"listWorkspaceAuditEvents","tags":["Audit"],"summary":"List audit events for a workspace","description":"Returns paginated audit events scoped to a specific workspace.\nThe calling account must be a member of the workspace.\n","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","example":"wsp_abc123"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response's `next_cursor` field","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list of audit events","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditEvent"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/account/usage":{"get":{"operationId":"getUsage","tags":["Account"],"summary":"Get usage statistics","description":"Returns daily usage statistics for the authenticated account.\nIncludes documents sent and API calls per day, plus period totals.\n","parameters":[{"name":"days","in":"query","description":"Number of days of history to return (max 90)","schema":{"type":"integer","default":30,"maximum":90}}],"responses":{"200":{"description":"Usage statistics","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/v1/kyc":{"get":{"operationId":"getKyc","tags":["KYC"],"summary":"Get workspace KYC state","description":"Returns the complete KYC state for the caller's workspace: the current\n`status`, the own-company anchor, submitted metadata, the versioned\nattestation (including its full text and current version), every\nuploaded document, informational mandate coverage, and a `requirements`\nblock telling you exactly what is still missing before you can submit.\n\nRead this before uploading documents and before submitting — it drives\nthe whole go-live flow. `attestation.text` and\n`attestation.current_version` are what you echo back on submit, and\n`requirements.can_submit` tells you whether a submit will pass the gates.\n\nThe document `sha256` and `r2_key` are stored internally and are\n**never** returned.\n","responses":{"200":{"description":"Workspace KYC state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KycState"},"examples":{"none":{"summary":"Fresh workspace (nothing submitted)","value":{"status":"none","company_id":null,"submitted_at":null,"reviewed_at":null,"reject_reason":null,"review_reference":null,"legal_name":null,"enterprise_number":null,"attestation":{"name":null,"role":null,"version":null,"current_version":"2026-08-26","text":"**Workspace verification and mandate attestation**\n\nBy checking this box I confirm, on behalf of **[Workspace legal entity]**:\n\n1. **Authority.** I am an authorised representative of this entity and may accept these terms for it.\n...\n☐ I have read and agree. Signed: **[name] — [role] — [date]**"},"documents":[],"mandate_coverage":{"companies_with_mandate":0,"total_companies":0},"requirements":{"has_company":false,"own_company_id":null,"missing_workspace_doc_types":["registry_extract","representative_id","authority_proof"],"can_submit":false}}},"approved":{"$ref":"#/components/examples/KycStateApproved"}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"description":"Workspace not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"not_found","code":"workspace_not_found","message":"Workspace not found","retry":{"retryable":false}}}}}}}}},"/v1/kyc/documents":{"post":{"operationId":"uploadKycDocument","tags":["KYC"],"summary":"Upload a KYC document","description":"Upload one KYC document as base64-encoded bytes. Two families of\ndocument exist:\n\n- **Workspace-level** (`registry_extract`, `representative_id`,\n  `authority_proof`) — cover the whole workspace. `company_id` and\n  `mandate_grantor_name` **must be omitted**. All three are required\n  before you can submit.\n- **Company-level** (`mandate`) — one per company you represent.\n  Requires both `company_id` (a company in the workspace) and\n  `mandate_grantor_name`. Mandates are optional evidence and never gate\n  submission; the own company is exempt.\n\n**Constraints**\n\n- Max size **10 MB** per document.\n- Accepted types: PDF, PNG, JPEG, XLSX. The MIME type is **sniffed from\n  the file's magic bytes** — the server does not trust a declared\n  `mime_type`. If you send `mime_type` and it disagrees with the sniffed\n  type the upload is rejected (`kyc_mime_mismatch`); omit it to skip that\n  check.\n- Uploads are **locked** once the workspace status is `pending` or\n  `approved` (`kyc_locked`).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KycDocumentUpload"},"examples":{"workspace_doc":{"summary":"Workspace document (registry_extract)","value":{"doc_type":"registry_extract","filename":"kbo-extract.pdf","content":"JVBERi0xLjcK... (base64 of a %PDF file)","mime_type":"application/pdf"}},"mandate_doc":{"summary":"Company mandate document","value":{"doc_type":"mandate","filename":"mandate-bakery.pdf","content":"JVBERi0xLjcK... (base64 of a %PDF file)","mime_type":"application/pdf","company_id":"com_2mN4oP6qR8sT0uV2wX4yZ","mandate_grantor_name":"Marie Dubois"}}}}}},"responses":{"201":{"description":"Document stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KycDocument"},"examples":{"workspace_doc":{"summary":"registry_extract stored","value":{"id":"kyd_3aB7cD9eF1gH2iJ4kL6mN","company_id":null,"doc_type":"registry_extract","filename":"kbo-extract.pdf","mime_type":"application/pdf","size_bytes":184320,"mandate_grantor_name":null,"created_at":"2026-08-27T10:15:00.000Z"}},"mandate_doc":{"summary":"mandate stored","value":{"id":"kyd_9qR1sT3uV5wX7yZ9aB1cD","company_id":"com_2mN4oP6qR8sT0uV2wX4yZ","doc_type":"mandate","filename":"mandate-bakery.pdf","mime_type":"application/pdf","size_bytes":66000,"mandate_grantor_name":"Marie Dubois","created_at":"2026-08-27T10:16:20.000Z"}}}}}},"400":{"description":"Bad request. `code` distinguishes the cause:\n- `kyc_missing_content` — `content` absent or empty.\n- `kyc_invalid_encoding` — `content` is not valid base64.\n- `kyc_invalid_doc_type` — `doc_type` is not one of the four types.\n- `kyc_unsupported_type` — the sniffed magic bytes match no accepted type.\n- `kyc_mime_mismatch` — declared `mime_type` disagrees with the sniffed type.\n- `kyc_mandate_requires_company` — a `mandate` upload is missing `company_id` or `mandate_grantor_name`.\n- `kyc_doc_pairing_invalid` — a workspace document carries `company_id` or `mandate_grantor_name`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"validation_error","code":"kyc_mime_mismatch","message":"Declared application/pdf but content is image/png","param":"file","retry":{"retryable":false}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"description":"Not found. `workspace_not_found` if the workspace row is missing, or\n`company_not_found` if a `mandate` `company_id` is not a company of\nthis workspace.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"not_found","code":"company_not_found","message":"Company not found","retry":{"retryable":false}}}}}},"409":{"description":"`kyc_locked` — uploads are locked because the workspace status is\n`pending` or `approved`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"validation_error","code":"kyc_locked","message":"KYC documents cannot be changed once submitted","retry":{"retryable":false}}}}}},"413":{"description":"`kyc_document_too_large` — the decoded document exceeds the 10 MB cap.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"validation_error","code":"kyc_document_too_large","message":"Document exceeds the 10 MB limit","param":"file","retry":{"retryable":false}}}}}}}}},"/v1/kyc/submit":{"post":{"operationId":"submitKyc","tags":["KYC"],"summary":"Submit workspace KYC for review","description":"Submit the workspace for KYC review. Allowed only from `none` or\n`rejected`; on success the status becomes `pending` and the response is\nthe full `GET /v1/kyc` shape reflecting the new state.\n\nBefore submitting, all of the following must hold (check\n`requirements.can_submit` in `GET /v1/kyc`):\n\n- the workspace has at least one company;\n- `company_id` names the own company (the signing legal entity);\n- all three workspace document types have been uploaded\n  (`registry_extract`, `representative_id`, `authority_proof`);\n- `kyc_legal_name` and `kyc_enterprise_number` are provided\n  (the enterprise number is stored verbatim — no format check);\n- `attestation_name` and `attestation_role` are provided;\n- `attested` is exactly `true`;\n- `attestation_version` equals the current version from\n  `attestation.current_version` (currently `2026-08-26`).\n\nMandate documents are optional and are **not** required to submit. The\nown company is exempt from the mandate requirement.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KycSubmit"},"example":{"company_id":"com_9fJ2kQ7bTx0Lm4Ne1Ry8","kyc_legal_name":"Acme Trading BV","kyc_enterprise_number":"0123.456.789","attestation_name":"Jan Janssens","attestation_role":"Managing Director","attested":true,"attestation_version":"2026-08-26"}}}},"responses":{"200":{"description":"Submitted. Body is the full `GET /v1/kyc` state with `status`\nflipped to `pending` and the submitted metadata populated.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KycState"},"examples":{"submitted":{"$ref":"#/components/examples/KycStateSubmitted"}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"409":{"description":"`kyc_not_submittable` — the workspace is not in a state that can\ntransition to `pending` (it is already `pending` or `approved`, or a\nconcurrent submit won the race).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"validation_error","code":"kyc_not_submittable","message":"KYC is not in a submittable state","retry":{"retryable":false}}}}}},"422":{"description":"A submission gate failed. Gates are evaluated in order; `code`\nidentifies the first one that failed:\n- `kyc_no_companies` — the workspace has no companies.\n- `kyc_no_own_company` — `company_id` was blank.\n- `kyc_invalid_company` — `company_id` is not a company of this workspace.\n- `kyc_missing_documents` — a required workspace doc type is missing (`details.missing` lists them).\n- `kyc_missing_legal_name` — `kyc_legal_name` was blank.\n- `kyc_missing_enterprise_number` — `kyc_enterprise_number` was blank.\n- `kyc_attestation_incomplete` — `attestation_name` or `attestation_role` was blank.\n- `kyc_attestation_not_accepted` — `attested` was not `true`.\n- `kyc_attestation_version_mismatch` — `attestation_version` does not equal the current version.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"validation_error","code":"kyc_attestation_version_mismatch","message":"Attestation version must equal 2026-08-26","retry":{"retryable":false}}}}}}}}},"/v1/companies":{"post":{"operationId":"createCompany","tags":["Companies"],"summary":"Create a company","description":"Create a new company (business entity) in the caller's workspace. The\nresponse echoes the caller's workspace `role`.\n\nOnly `name` and `country` are required. `country` must be an ISO 3166-1\nalpha-2 code for a country with active Peppol support (e.g. `BE`);\ncodes that are unknown or not Peppol-active (e.g. `FR`) are rejected\nwith `invalid_country`. A missing `name` or `country` returns\n`missing_field`.\n\n**Auto-provisioning.** In a workspace whose KYC status is already\n`approved`, creating a company **auto-provisions** its e-invoice.be\nprovider tenant so it can go live immediately (`is_live: true`). This is\nbest-effort: if provisioning fails the company is still created and the\ncall still returns `201` with `is_live: false`. In workspaces that are\nnot yet approved, `is_live` is always `false` until the workspace passes\nKYC (see the KYC tag).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyCreate"},"examples":{"minimal":{"summary":"Minimal (name + country)","value":{"name":"Acme BV","country":"BE"}},"full":{"summary":"Full details","value":{"name":"Acme BV","country":"BE","company_registration_id":"0123456789","tax_id":"BE0123456789","email":"billing@acme.be","iban":"BE68539007547034","address":{"street":"Keizerslaan 1","city":"Brussels","postal_code":"1000"}}}}}}},"responses":{"201":{"description":"Company created. Caller is the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Company"},"example":{"id":"com_9fJ2kQ7bTx0Lm4Ne1Ry8","name":"Acme BV","workspace_id":"wsp_abc123def456","role":"owner","is_live":false,"company_registration_id":"0123456789","tax_id":"BE0123456789","email":"billing@acme.be","country":"BE","address":{"street":"Keizerslaan 1","city":"Brussels","postal_code":"1000"},"iban":"BE68539007547034","peppol_id":"0208:0123456789","created_at":"2026-03-05T12:00:00.000Z"}}}},"400":{"description":"Validation error — `missing_field` (`name` or `country` omitted),\n`invalid_country` (country unknown or not Peppol-active), or\n`invalid_peppol_id` (Peppol ID not in `scheme:id` form).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"validation_error","code":"invalid_country","message":"Country FR is not supported for Peppol","param":"country","retry":{"retryable":false}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}},"get":{"operationId":"listCompanies","tags":["Companies"],"summary":"List your companies","description":"Returns every company in the caller's workspace, newest first.\n\nList items are a summary shape — they carry `is_live` and\n`workspace_id` but omit the address, registration id, email, IBAN,\nPeppol id, and role. Fetch a single company for the full detail shape.\n","responses":{"200":{"description":"List of companies","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CompanyListItem"}}}},"example":{"data":[{"id":"com_9fJ2kQ7bTx0Lm4Ne1Ry8","name":"Acme BV","tax_id":"BE0123456789","country":"BE","is_live":true,"workspace_id":"wsp_abc123def456","created_at":"2026-03-05T12:00:00Z"}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/v1/companies/{id}":{"get":{"operationId":"getCompany","tags":["Companies"],"summary":"Get company details","description":"Returns full details for a company. The calling account must be a\nmember of the company.\n","parameters":[{"$ref":"#/components/parameters/CompanyId"}],"responses":{"200":{"description":"Company details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyDetail"},"example":{"id":"com_9fJ2kQ7bTx0Lm4Ne1Ry8","name":"Acme BV","workspace_id":"wsp_abc123def456","company_registration_id":"0123456789","tax_id":"BE0123456789","email":"billing@acme.be","country":"BE","address":{"street":"Keizerslaan 1","city":"Brussels","postal_code":"1000"},"iban":"BE68539007547034","peppol_id":"0208:0123456789","is_live":true,"created_at":"2026-03-05T12:00:00.000Z","updated_at":"2026-03-06T09:30:00.000Z"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"description":"Company not found (or you are not a member)"}}},"patch":{"operationId":"updateCompany","tags":["Companies"],"summary":"Update company","description":"Update company details. Only **owners** and **admins** can update.\nSend only the fields you want to change.\n","parameters":[{"$ref":"#/components/parameters/CompanyId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyUpdate"},"example":{"name":"Acme BV (new name)","tax_id":"BE0123456789"}}}},"responses":{"200":{"description":"Company updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyDetail"},"example":{"id":"com_9fJ2kQ7bTx0Lm4Ne1Ry8","name":"Acme BV (new name)","workspace_id":"wsp_abc123def456","company_registration_id":"0123456789","tax_id":"BE0123456789","email":"billing@acme.be","country":"BE","address":{"street":"Keizerslaan 1","city":"Brussels","postal_code":"1000"},"iban":"BE68539007547034","peppol_id":"0208:0123456789","is_live":true,"created_at":"2026-03-05T12:00:00.000Z","updated_at":"2026-03-07T14:05:00.000Z"}}}},"400":{"description":"No fields to update, `invalid_country`, or `invalid_peppol_id`"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"description":"Only owners and admins can update"},"404":{"description":"Company not found (or you are not a member)"},"502":{"description":"`provider_update_failed` — a live company's `peppol_id` change could\nnot be pushed to its provider tenant. No changes were made.\n"}}}},"/v1/documents":{"post":{"operationId":"sendDocument","tags":["Documents"],"summary":"Send a document","description":"Create and send a Peppol document in a single call.\n\n**Requires `company_id`** — the company whose provider credentials\nwill be used. The calling account must be a member of that company.\n\n- With `ps_test_` keys: delivered via email (sandbox)\n- With `ps_live_` keys: delivered via Peppol network (production)\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/DocumentCreate"},{"type":"object","required":["company_id"],"properties":{"company_id":{"type":"string","description":"ID of the company sending this document"}}}]},"example":{"company_id":"com_abc123","type":"invoice","number":"INV-2026-001","issue_date":"2026-03-01","due_date":"2026-03-31","currency":"EUR","from":{"name":"Acme BV","tax_id":"BE0123456789","address":{"street":"Keizerslaan 1","city":"Brussels","postal_code":"1000","country":"BE"}},"to":{"name":"Client NV","tax_id":"BE0987654321"},"lines":[{"description":"API Integration Services","quantity":1,"unit_price":500,"tax_rate":21}]}}}},"responses":{"200":{"description":"Idempotent replay — the same idempotency key (or the same document)\nwas already accepted, so the existing record is returned unchanged\nand no second send is queued.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}},"202":{"description":"Accepted for async sending. The document is persisted and queued;\npoll the document or subscribe to webhooks for the outcome.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"description":"Not a member of the specified company"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"operationId":"listDocuments","tags":["Documents"],"summary":"List sent documents","description":"List documents sent by a company. Results use cursor-based pagination.\n\n**Requires `company_id` query parameter.**\n","parameters":[{"name":"company_id","in":"query","required":true,"description":"Filter by company","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/DocumentStatus"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response's `next_cursor` field","schema":{"type":"string"}}],"responses":{"200":{"description":"List of documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentList"}}}},"403":{"description":"Not a member of the specified company"}}}},"/v1/documents/batch":{"post":{"operationId":"sendDocumentBatch","tags":["Documents"],"summary":"Send a batch of documents","description":"Send up to 100 documents in a single request. All documents in a batch\nmust use the same `company_id`.\n\nEach document is processed independently — partial failures are possible.\nThe response array maps 1:1 to the input array.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/DocumentCreate"},{"type":"object","required":["company_id"],"properties":{"company_id":{"type":"string"}}}]},"maxItems":100}}}},"responses":{"202":{"description":"Accepted — batch results, one entry per input document. When the\ncompany's remaining balance after the batch is below the\nlow-balance threshold, the response includes an\n`X-Credits-Remaining` header.\n","content":{"application/json":{"schema":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/Document"},{"$ref":"#/components/schemas/ErrorObject"}]}}}}},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/v1/documents/{id}":{"get":{"operationId":"getDocument","tags":["Documents"],"summary":"Get document details","description":"Retrieve details of a specific document. Requires `company_id` query\nparameter, and the calling account must be a member of that company.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"doc_a1b2c3d4"},{"name":"company_id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}},"403":{"description":"Not a member of the specified company"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/documents/{id}/history":{"get":{"operationId":"getDocumentHistory","tags":["Documents"],"summary":"Get delivery history","description":"Timeline of events for this document: created, validated,\nqueued, in transit, delivered, or failed. Scoped to the caller's\nworkspace.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Event history","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentEvent"}}}}}}}},"/v1/documents/{id}/ubl":{"get":{"operationId":"getDocumentUbl","tags":["Documents"],"summary":"Download the Send-ready UBL","description":"Stream the stored Send-ready UBL XML for a document, scoped to the\ncaller's workspace. Returns `404` until the document has been sent and\nits UBL persisted.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"doc_a1b2c3d4"}],"responses":{"200":{"description":"UBL document","content":{"application/xml":{"schema":{"type":"string"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/documents/{id}/attachments":{"get":{"operationId":"listDocumentAttachments","tags":["Documents"],"summary":"List document attachments","description":"List metadata for every attachment stored with a document, scoped to\nthe caller's workspace. Bytes are fetched via the per-attachment\nendpoint.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Attachment metadata","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DocumentAttachment"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/documents/{id}/attachments/{att_id}":{"get":{"operationId":"getDocumentAttachment","tags":["Documents"],"summary":"Download an attachment","description":"Stream one attachment's raw bytes from storage with its verified\n`Content-Type`, scoped to the caller's workspace.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"att_id","in":"path","required":true,"schema":{"type":"string"},"example":"att_a1b2c3d4"}],"responses":{"200":{"description":"Attachment bytes","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/events":{"get":{"operationId":"listEvents","tags":["Events"],"summary":"List document events","description":"Returns a chronological feed of document events (queued, sending,\ndelivered, failed) across the workspace, newest first. Workspace is\ndetermined from the API key. Optionally narrow to a single company\nwith the `company_id` query parameter.\n\nSupports filtering by event type, date range, document ID, and\nfree-text search on document number or recipient.\nUses cursor-based keyset pagination for stable results under\nconcurrent inserts.\n","parameters":[{"name":"company_id","in":"query","required":false,"schema":{"type":"string"},"description":"Optional company filter; must belong to the workspace"},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["queued","sending","delivered","failed","retry"]},"description":"Filter by event type"},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Start of date range (inclusive)"},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"End of date range (inclusive)"},{"name":"document_id","in":"query","required":false,"schema":{"type":"string"},"description":"Filter to events for a specific document"},{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Free-text search. Matches case-insensitively on document number or recipient name."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Pagination cursor from previous response"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Number of events per page"}],"responses":{"200":{"description":"Paginated list of events","content":{"application/json":{"schema":{"type":"object","required":["data","has_more","next_cursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/EventWithContext"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}}}}}}}},"/v1/lookup/{peppol_id}":{"get":{"operationId":"lookupParticipant","tags":["Lookup"],"summary":"Lookup Peppol participant via SMP","security":[],"description":"Resolve a Peppol participant directly via SMP (Service Metadata Publisher).\nUses NAPTR DNS lookup with SHA-256 hashing to discover the SMP,\nthen fetches ServiceGroup and ServiceMetadata to return supported\ndocument types and AS4 endpoints.\n\n**Public endpoint** — no API key required. Anonymous callers are\nrate-limited to 60 lookups/hour per IP. Pass a sandbox key for 600/h\nor a live key for 6000/h.\n\nEach endpoint in the response includes parsed X.509 certificate\nfields (`cert_subject`, `cert_issuer`, `cert_valid_from`,\n`cert_valid_to`, `cert_fingerprint_sha256`). Unparsable certificates\nreturn `null` for these fields.\n","parameters":[{"name":"peppol_id","in":"path","required":true,"description":"Peppol participant ID in `EAS:code` format (e.g., `0208:0123456789`)\n","schema":{"type":"string"},"example":"0208:0123456789"},{"name":"domain","in":"query","required":false,"description":"SML domain to query. `sml` for production, `smk` for test.\n","schema":{"type":"string","enum":["sml","smk"],"default":"sml"}}],"responses":{"200":{"description":"Participant found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmpLookupResult"}}}},"400":{"description":"Invalid peppol_id or scheme"},"404":{"description":"Participant not found in SML/SMP"},"429":{"description":"Rate limit exceeded. Check the `Retry-After` header.\nAnonymous limit is 60/h per IP — pass an API key for higher limits.\n"},"502":{"description":"SMP or DNS error"}}}},"/v1/lookup/{peppol_id}/dns":{"get":{"operationId":"lookupParticipantDns","tags":["Lookup"],"summary":"DNS-only Peppol participant resolution","security":[],"description":"Resolve just the NAPTR/SMP DNS layer for a Peppol participant without\nfetching the full ServiceGroup or business card. Returns the computed\nNAPTR hostname, the resolved SMP URL, and the domain used.\n\n**Public endpoint** — same rate limits as the full lookup.\n","parameters":[{"name":"peppol_id","in":"path","required":true,"description":"Peppol participant ID in `EAS:code` format (e.g., `0208:0123456789`)\n","schema":{"type":"string"},"example":"0208:0123456789"},{"name":"domain","in":"query","required":false,"description":"SML domain to query. `sml` for production, `smk` for test.\n","schema":{"type":"string","enum":["sml","smk"],"default":"sml"}}],"responses":{"200":{"description":"DNS resolution result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DnsLookupResult"}}}},"400":{"description":"Invalid peppol_id or scheme"},"404":{"description":"Participant not found (no NAPTR record)"},"429":{"description":"Rate limit exceeded"},"502":{"description":"DNS resolution error"}}}},"/v1/validate":{"post":{"operationId":"validateDocument","tags":["Validate"],"summary":"Validate a document payload","description":"Validate a document payload without creating or sending anything.\nReturns a `ValidationResult` with field-level errors and warnings —\nperfect for form validation in your UI.\n\nAccepts either:\n  - `application/json`: a `DocumentCreate` body plus `company_id`.\n  - `multipart/form-data`: a UBL XML `file` plus `company_id` field.\n\nField-level issues return a dot-path in `path` (e.g. `buyer.tax_id`,\n`lines[0].unit_price`) so SDK clients can highlight the right form field.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/DocumentCreate"},{"type":"object","required":["company_id"],"properties":{"company_id":{"type":"string","description":"Company whose provider credentials should validate this document."}}}]},"examples":{"invoice":{"summary":"Validate an invoice payload","value":{"company_id":"com_abc123","type":"invoice","number":"INV-2026-0001","issue_date":"2026-04-04","from":{"name":"Acme BV","tax_id":"BE0123456789"},"to":{"name":"Globex NV","tax_id":"BE0987654321"},"lines":[{"description":"Consulting","quantity":1,"unit_price":1000,"tax_rate":21}]}}}},"multipart/form-data":{"schema":{"type":"object","required":["file","company_id"],"properties":{"file":{"type":"string","format":"binary","description":"UBL XML document to validate."},"company_id":{"type":"string","description":"Company whose provider credentials should validate this document."}}}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationResult"},"examples":{"valid":{"summary":"Payload is valid","value":{"valid":true,"errors":[],"warnings":[]}},"invalid":{"summary":"Payload has field-level errors","value":{"valid":false,"errors":[{"path":"buyer.tax_id","code":"invalid_format","message":"VAT number is not in a valid format"}],"warnings":[{"path":"lines[0].description","code":"short_description","message":"Description is unusually short"}]}}}}}},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/webhooks":{"get":{"operationId":"listWebhooks","tags":["Webhooks"],"summary":"List webhooks","description":"List every webhook configured for the caller's workspace.\n","responses":{"200":{"description":"List of webhooks","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}},"post":{"operationId":"createWebhook","tags":["Webhooks"],"summary":"Create a webhook","description":"Create a webhook to receive real-time notifications for document events.\nThe webhook belongs to the caller's workspace.\n\nThe response contains the signing `secret` (`whsec_…`) **once** — store\nit now, it is never returned again. Use it to verify the\n`X-Peppol-Signature-V2` header on every delivery (see the Webhooks tag\ndescription for the algorithm and per-language verification examples).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreate"}}}},"responses":{"201":{"description":"Webhook created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/v1/webhooks/{id}":{"get":{"operationId":"getWebhook","tags":["Webhooks"],"summary":"Get a webhook","description":"Fetch a single webhook by id, scoped to the caller's workspace. The\nsigning `secret` is returned only on creation and is never included\nhere.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"},"example":{"id":"whk_abc123","url":"https://example.com/webhooks/peppol","events":["document.delivered","document.failed"],"active":true,"created_at":"2026-03-05T12:00:00.000Z"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"description":"Webhook not found for the authenticated account's workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"not_found","code":"webhook_not_found","message":"Webhook whk_abc123 not found","param":"id","retry":{"retryable":false}}}}}}}},"delete":{"operationId":"deleteWebhook","tags":["Webhooks"],"summary":"Delete a webhook","description":"Delete a webhook belonging to the caller's workspace.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook deleted","content":{"application/json":{"schema":{"type":"object","required":["deleted"],"properties":{"deleted":{"type":"boolean","description":"Always `true` when the webhook was removed."}}},"example":{"deleted":true}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/v1/webhooks/{id}/deliveries":{"get":{"operationId":"listWebhookDeliveries","tags":["Webhooks"],"summary":"List webhook deliveries","description":"Paginated delivery log for a webhook. Returns attempts with their HTTP\nstatus, retry state, and failure reason. Delivery records are retained\nfor both successful and failed attempts so customers can diagnose\nintegration issues.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size (default 50, max 100).","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Delivery id returned in the previous page's `next_cursor`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of webhook deliveries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryList"}}}},"404":{"description":"Webhook not found for the authenticated account"}}}},"/v1/webhooks/{id}/test":{"post":{"operationId":"testWebhook","tags":["Webhooks"],"summary":"Send a test event","description":"Send a synthetic `webhook.test` event to the webhook's configured URL\nright now. The response echoes the downstream HTTP status so you can\nverify your receiver is reachable and validating signatures correctly.\nThe attempt is recorded in the delivery log just like a real event.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Test event dispatched (check `success` + `status_code` for the receiver's response)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestResponse"}}}},"404":{"description":"Webhook not found for the authenticated account"}}}},"/v1/webhooks/{id}/rotate-secret":{"post":{"operationId":"rotateWebhookSecret","tags":["Webhooks"],"summary":"Rotate the signing secret","description":"Mint a new signing secret for this webhook and return it ONCE in the\nresponse — it is never retrievable again, exactly like the secret from\ncreation. The previous secret enters a 24-hour overlap window: during\nthat window `X-Peppol-Signature-V2` carries one `v1=` signature per\nactive secret (new first), so a receiver can accept either while it\nrolls out the new secret. The legacy `X-Peppol-Signature` switches to\nthe new secret immediately. Rotating again before the window closes\ndrops the older secret (max two active secrets).\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"New secret minted (returned once) and overlap window opened","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookRotateSecretResponse"}}}},"404":{"description":"Webhook not found for the authenticated account"}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"API key prefixed with `ps_live_` (production) or `ps_test_` (sandbox).\n\nPass in the `Authorization` header:\n```\nAuthorization: Bearer ps_test_a1b2c3...\n```\n"}},"parameters":{"CompanyId":{"name":"id","in":"path","required":true,"description":"Company ID (e.g., `com_abc123`)","schema":{"type":"string"},"example":"com_abc123"}},"examples":{"KycStateApproved":{"summary":"Approved workspace (live)","value":{"status":"approved","company_id":"com_9fJ2kQ7bTx0Lm4Ne1Ry8","submitted_at":"2026-08-20 09:14:02","reviewed_at":"2026-08-21 11:30:00","reject_reason":null,"review_reference":"OPS-2026-0042","legal_name":"Acme Trading BV","enterprise_number":"0123.456.789","attestation":{"name":"Jan Janssens","role":"Managing Director","version":"2026-08-26","current_version":"2026-08-26","text":"**Workspace verification and mandate attestation**\n\n...\n☐ I have read and agree. Signed: **[name] — [role] — [date]**"},"documents":[{"id":"kyd_3aB7cD9eF1gH2iJ4kL6mN","company_id":null,"doc_type":"registry_extract","filename":"kbo-extract.pdf","mime_type":"application/pdf","size_bytes":184320,"mandate_grantor_name":null,"created_at":"2026-08-20T09:10:11.000Z"},{"id":"kyd_5oP7qR9sT1uV2wX4yZ6aB","company_id":null,"doc_type":"representative_id","filename":"id-card.png","mime_type":"image/png","size_bytes":90210,"mandate_grantor_name":null,"created_at":"2026-08-20T09:11:00.000Z"},{"id":"kyd_7cD9eF1gH2iJ4kL6mN8oP","company_id":null,"doc_type":"authority_proof","filename":"board-resolution.pdf","mime_type":"application/pdf","size_bytes":51200,"mandate_grantor_name":null,"created_at":"2026-08-20T09:12:30.000Z"},{"id":"kyd_9qR1sT3uV5wX7yZ9aB1cD","company_id":"com_2mN4oP6qR8sT0uV2wX4yZ","doc_type":"mandate","filename":"mandate-bakery.pdf","mime_type":"application/pdf","size_bytes":66000,"mandate_grantor_name":"Marie Dubois","created_at":"2026-08-20T09:13:45.000Z"}],"mandate_coverage":{"companies_with_mandate":1,"total_companies":1},"requirements":{"has_company":true,"own_company_id":"com_9fJ2kQ7bTx0Lm4Ne1Ry8","missing_workspace_doc_types":[],"can_submit":false}}},"KycStateSubmitted":{"summary":"Just submitted (pending review)","value":{"status":"pending","company_id":"com_9fJ2kQ7bTx0Lm4Ne1Ry8","submitted_at":"2026-08-27 10:20:00","reviewed_at":null,"reject_reason":null,"review_reference":null,"legal_name":"Acme Trading BV","enterprise_number":"0123.456.789","attestation":{"name":"Jan Janssens","role":"Managing Director","version":"2026-08-26","current_version":"2026-08-26","text":"**Workspace verification and mandate attestation**\n\n...\n☐ I have read and agree. Signed: **[name] — [role] — [date]**"},"documents":[{"id":"kyd_3aB7cD9eF1gH2iJ4kL6mN","company_id":null,"doc_type":"registry_extract","filename":"kbo-extract.pdf","mime_type":"application/pdf","size_bytes":184320,"mandate_grantor_name":null,"created_at":"2026-08-20T09:10:11.000Z"}],"mandate_coverage":{"companies_with_mandate":0,"total_companies":0},"requirements":{"has_company":true,"own_company_id":"com_9fJ2kQ7bTx0Lm4Ne1Ry8","missing_workspace_doc_types":[],"can_submit":false}}}},"schemas":{"AuditEvent":{"type":"object","description":"A single append-only audit-trail entry for a security-relevant event.","required":["id","event_type","created_at"],"properties":{"id":{"type":"string","description":"Audit event ID (prefixed `aae_`).","example":"aae_abc123xyz"},"actor_account_id":{"type":"string","nullable":true,"description":"Account that performed the action, or null for system events.","example":"acc_abc123"},"workspace_id":{"type":"string","nullable":true,"description":"Workspace the event is scoped to, or null.","example":"wsp_abc123"},"event_type":{"type":"string","enum":["api_key.created","api_key.revoked","company.created","company.updated","membership.invited","membership.removed","membership.role_changed","membership.ownership_transferred","webhook.created","webhook.deleted","workspace.created"],"example":"api_key.created"},"subject_type":{"type":"string","nullable":true,"enum":["api_key","company","membership","webhook","workspace"],"example":"api_key"},"subject_id":{"type":"string","nullable":true,"example":"ps_test_abc123..."},"ip":{"type":"string","nullable":true},"user_agent":{"type":"string","nullable":true},"metadata":{"type":"string","nullable":true,"description":"JSON-encoded event-specific metadata"},"created_at":{"type":"string","format":"date-time"}}},"HealthResponse":{"type":"object","description":"Health-check result with overall status and per-dependency checks.","required":["status","version","environment","checks"],"properties":{"status":{"type":"string","enum":["ok","degraded"],"description":"Overall API health status"},"version":{"type":"string","description":"API version","example":"2.0.0"},"environment":{"type":"string","enum":["production","staging","development","test","unknown"],"description":"Current deployment environment"},"checks":{"type":"object","required":["db"],"properties":{"db":{"type":"string","enum":["ok","error"],"description":"Database connectivity status"}}}}},"Workspace":{"type":"object","description":"A workspace is the top-level aggregation, billing, and access-control boundary.","properties":{"id":{"type":"string","description":"Workspace ID (prefixed `wsp_`)","example":"wsp_abc123def456"},"name":{"type":"string","example":"My Workspace"},"connect_enabled":{"type":"boolean","readOnly":true,"description":"Whether the workspace is enabled to operate on behalf of customer\ncompanies (multi-company/agency use). Ops-gated. Not returned on the\ncreate response.\n","example":false},"credit_balance":{"type":"integer","description":"Current credit balance for the workspace","example":100},"suspended":{"type":"boolean","description":"Whether the workspace is suspended","example":false},"role":{"type":"string","enum":["owner","admin","member"],"description":"The authenticated account's role in this workspace"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"WorkspaceMember":{"type":"object","description":"An account's membership of a workspace, with its assigned role.","properties":{"account_id":{"type":"string","description":"ID of the member account (prefixed `acc_`)."},"email":{"type":"string","format":"email"},"name":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","admin","member"]},"created_at":{"type":"string","format":"date-time"}}},"SignupResponse":{"type":"object","description":"Response from `POST /v1/signup`. Contains the account ID and a one-time API key.","properties":{"id":{"type":"string","description":"Account ID (prefixed `acc_`)","example":"acc_abc123def456"},"email":{"type":"string","format":"email"},"status":{"type":"string","enum":["active"],"description":"New accounts start as `active`"},"api_key":{"type":"string","description":"Full sandbox API key — **shown only once**. Store it securely.\nUse in `Authorization: Bearer <key>` header.\n","example":"ps_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"}}},"Account":{"type":"object","description":"Full account details including API keys and usage.","properties":{"id":{"type":"string","description":"Account ID (prefixed `acc_`)"},"email":{"type":"string","format":"email"},"name":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","invited","disabled"],"description":"- `active` — fully operational\n- `invited` — created via company invite, pending activation\n- `disabled` — deactivated by admin\n"},"api_keys":{"type":"array","description":"API keys (prefix only — full key is never shown again)","items":{"type":"object","properties":{"prefix":{"type":"string","description":"First 15 chars + \"...\""},"sandbox":{"type":"boolean","description":"Whether this is a sandbox (`ps_test_`) key."},"label":{"type":"string","nullable":true},"last_used_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"revoked":{"type":"boolean"}}}},"usage":{"type":"object","description":"Aggregate usage totals for the account.","properties":{"total_documents_sent":{"type":"integer","description":"Lifetime count of documents sent."},"total_api_calls":{"type":"integer","description":"Lifetime count of API calls."}}},"created_at":{"type":"string","format":"date-time"}}},"ApiKeyCreated":{"type":"object","description":"Response when creating a new API key. The full key is shown only once.","properties":{"api_key":{"type":"string","description":"Full key — **store securely**, shown only once"},"prefix":{"type":"string","description":"Display prefix for identification"},"sandbox":{"type":"boolean"},"label":{"type":"string","nullable":true}}},"UsageResponse":{"type":"object","description":"Aggregate usage over a period with a daily breakdown.","properties":{"period":{"type":"object","properties":{"days":{"type":"integer"},"since":{"type":"string","format":"date"}}},"totals":{"type":"object","properties":{"documents_sent":{"type":"integer"},"api_calls":{"type":"integer"}}},"daily":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"documents_sent":{"type":"integer"},"api_calls":{"type":"integer"}}}}}},"CompanyCreate":{"type":"object","description":"Request body for creating a company.","required":["name","country"],"properties":{"name":{"type":"string","description":"Legal company name."},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code. Must be a country with active\nPeppol support (e.g. `BE`); trimmed and upper-cased server-side.\nUnknown or non-active codes are rejected with `invalid_country`.\n"},"company_registration_id":{"type":"string","description":"Company registration number (e.g. KBO/CBE number). Optional."},"tax_id":{"type":"string","description":"VAT number (e.g., `BE0123456789`). Optional."},"email":{"type":"string","format":"email","description":"Contact email for the company. Optional."},"iban":{"type":"string","description":"Bank account IBAN used as a payment default. Optional."},"peppol_id":{"type":"string","description":"Peppol participant identifier in `scheme:id` form (e.g.\n`0208:0123456789`). Optional. When omitted, a Belgian company\ndefaults to `0208:<company_registration_id>`; any other country\nstays unset until one is provided. An ill-formed value is rejected\nwith `invalid_peppol_id`.\n"},"address":{"type":"object","description":"Company address. Optional.","properties":{"street":{"type":"string"},"city":{"type":"string"},"postal_code":{"type":"string"}}}}},"CompanyUpdate":{"type":"object","description":"Partial update — send only the fields you want to change.\nAt least one updatable field must be provided.\n","properties":{"name":{"type":"string"},"company_registration_id":{"type":"string"},"tax_id":{"type":"string"},"email":{"type":"string","format":"email"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code; validated the same way as on create.\n"},"iban":{"type":"string"},"peppol_id":{"type":"string","description":"Peppol participant identifier in `scheme:id` form. Editable both\nbefore and after go-live: once the company's provider tenant is\nprovisioned, a change is pushed to that tenant BEFORE it is stored,\nso the tenant always owns the Peppol ID it sends from. An ill-formed\nvalue is rejected with `invalid_peppol_id`; a provider failure on the\npush returns `502 provider_update_failed` and leaves the value\nunchanged.\n"},"address":{"type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"},"postal_code":{"type":"string"}}}}},"Company":{"type":"object","description":"Company as returned by `POST /v1/companies`. This create shape carries\nthe caller's workspace `role` and omits `updated_at` (fetch the company\nfor that).\n","properties":{"id":{"type":"string","description":"Company ID (prefixed `com_`)."},"name":{"type":"string"},"workspace_id":{"type":"string","description":"ID of the workspace that owns this company (prefixed `wsp_`)."},"role":{"type":"string","enum":["owner","admin","member"],"description":"The calling account's role in the workspace."},"is_live":{"type":"boolean","readOnly":true,"description":"Whether the company is live on the real Peppol network — `true`\nonly once its e-invoice.be provider tenant has been provisioned. In\nan approved workspace the tenant auto-provisions on create; otherwise\nthis is `false` until the workspace passes KYC.\n"},"company_registration_id":{"type":"string","nullable":true,"description":"Company registration number (distinct from the VAT `tax_id`)."},"tax_id":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"country":{"type":"string","description":"Normalised ISO 3166-1 alpha-2 country code."},"address":{"type":"object","nullable":true,"properties":{"street":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true}}},"iban":{"type":"string","nullable":true},"peppol_id":{"type":"string","nullable":true,"description":"Peppol participant identifier in `scheme:id` form. Derived to\n`0208:<company_registration_id>` for a Belgian company when not\ngiven explicitly; null otherwise.\n"},"created_at":{"type":"string","format":"date-time"}}},"CompanyListItem":{"type":"object","description":"Summary of a company in `GET /v1/companies` list responses. A reduced\nshape — no address, registration id, email, IBAN, Peppol id, or role.\n","properties":{"id":{"type":"string","description":"Company ID (prefixed `com_`)."},"name":{"type":"string"},"tax_id":{"type":"string","nullable":true},"country":{"type":"string","nullable":true,"description":"ISO 3166-1 alpha-2 country code."},"is_live":{"type":"boolean","readOnly":true,"description":"Whether the company is live on the Peppol network — derived solely\nfrom whether its provider tenant has been provisioned.\n"},"workspace_id":{"type":"string","description":"ID of the owning workspace."},"created_at":{"type":"string","format":"date-time"}}},"CompanyDetail":{"type":"object","description":"Full company details from `GET /v1/companies/{id}` and\n`PATCH /v1/companies/{id}`. Includes `peppol_id` and `updated_at`;\nunlike the create shape it does not carry `role`.\n","properties":{"id":{"type":"string","description":"Company ID (prefixed `com_`)."},"name":{"type":"string"},"workspace_id":{"type":"string","description":"ID of the owning workspace."},"company_registration_id":{"type":"string","nullable":true,"description":"Company registration number (distinct from the VAT `tax_id`)."},"tax_id":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code."},"address":{"type":"object","properties":{"street":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true}}},"iban":{"type":"string","nullable":true},"peppol_id":{"type":"string","nullable":true,"description":"Peppol participant identifier in `scheme:id` form (e.g.\n`0208:0123456789`) — the identifier the company's provider tenant\nowns and sends from.\n"},"is_live":{"type":"boolean","readOnly":true,"description":"Whether the company is live on the Peppol network — derived solely\nfrom whether its provider tenant has been provisioned.\n"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"KycState":{"type":"object","description":"Complete workspace KYC state, returned by `GET /v1/kyc` and by\n`POST /v1/kyc/submit`. Drives the whole go-live flow.\n","required":["status","attestation","documents","mandate_coverage","requirements"],"properties":{"status":{"type":"string","enum":["none","pending","approved","rejected"],"description":"Workspace KYC status. `none` → nothing submitted; `pending` → awaiting\nops review (uploads locked); `approved` → accepted and provisioned\n(terminal); `rejected` → declined, resubmit after fixing.\n"},"company_id":{"type":"string","nullable":true,"description":"The own-company anchor — the company that operates the workspace and\nsigns the attestation. Null until first submit.\n"},"submitted_at":{"type":"string","nullable":true,"description":"When the workspace was last submitted for review. UTC timestamp in\n`YYYY-MM-DD HH:MM:SS` format — space separator, no timezone suffix\n(e.g. `2026-08-20 09:14:02`). Not RFC-3339.\n"},"reviewed_at":{"type":"string","nullable":true,"description":"When ops last reviewed the submission. UTC timestamp in\n`YYYY-MM-DD HH:MM:SS` format — space separator, no timezone suffix\n(e.g. `2026-08-21 11:30:00`). Not RFC-3339.\n"},"reject_reason":{"type":"string","nullable":true,"description":"Human-readable reason when `status` is `rejected`."},"review_reference":{"type":"string","nullable":true,"description":"Internal ops reference for the review, if set."},"legal_name":{"type":"string","nullable":true,"description":"Legal entity name captured at submission."},"enterprise_number":{"type":"string","nullable":true,"description":"Enterprise/company number captured at submission, stored verbatim."},"attestation":{"$ref":"#/components/schemas/KycAttestation"},"documents":{"type":"array","description":"All uploaded KYC documents, oldest first.","items":{"$ref":"#/components/schemas/KycDocument"}},"mandate_coverage":{"$ref":"#/components/schemas/KycMandateCoverage"},"requirements":{"$ref":"#/components/schemas/KycRequirements"}}},"KycAttestation":{"type":"object","description":"The versioned attestation the signer accepts on behalf of the workspace\nlegal entity.\n","required":["current_version","text"],"properties":{"name":{"type":"string","nullable":true,"description":"Signer name from the last submission, or null."},"role":{"type":"string","nullable":true,"description":"Signer role from the last submission, or null."},"version":{"type":"string","nullable":true,"description":"Attestation version accepted at the last submission (may lag current)."},"current_version":{"type":"string","description":"Current attestation version. Echo this exact value back as\n`attestation_version` when submitting. Currently `2026-08-26`.\n","example":"2026-08-26"},"text":{"type":"string","description":"Full attestation text (Markdown) that the signer must accept."}}},"KycMandateCoverage":{"type":"object","description":"Informational mandate coverage across the workspace's non-own companies.\nNever gates submission — surfaced for ops context only. The workspace's\nown company (the one referenced by the KYC record's `company_id`) is\nexempt from the mandate requirement — mandates concern companies you\noperate Peppol services for on behalf of others.\n","required":["companies_with_mandate","total_companies"],"properties":{"companies_with_mandate":{"type":"integer","description":"Number of non-own companies that have at least one mandate document."},"total_companies":{"type":"integer","description":"Number of companies other than the own company."}}},"KycRequirements":{"type":"object","description":"What is still required before the workspace can be submitted.","required":["has_company","missing_workspace_doc_types","can_submit"],"properties":{"has_company":{"type":"boolean","description":"Whether the workspace has at least one company."},"own_company_id":{"type":"string","nullable":true,"description":"The own-company anchor id, or null if not yet set."},"missing_workspace_doc_types":{"type":"array","description":"Which of the three required workspace document types are still\nmissing (subset of `registry_extract`, `representative_id`,\n`authority_proof`).\n","items":{"type":"string","enum":["registry_extract","representative_id","authority_proof"]}},"can_submit":{"type":"boolean","description":"Whether a submit would pass the gates now — the status is a legal\nedge to `pending`, a company and own company exist, and no workspace\ndoc types are missing.\n"}}},"KycDocument":{"type":"object","description":"A stored KYC document projection. The internal `sha256` and `r2_key` are\nnever returned.\n","required":["id","doc_type","filename","mime_type","size_bytes","created_at"],"properties":{"id":{"type":"string","description":"Document ID (prefixed `kyd_`).","example":"kyd_3aB7cD9eF1gH2iJ4kL6mN"},"company_id":{"type":"string","nullable":true,"description":"Company this document belongs to (mandate docs only); null for workspace docs."},"doc_type":{"type":"string","enum":["registry_extract","representative_id","authority_proof","mandate"],"description":"Document type. `registry_extract`, `representative_id`,\n`authority_proof` are workspace-level; `mandate` is company-level.\n"},"filename":{"type":"string","description":"Original filename, or `document` if none was supplied."},"mime_type":{"type":"string","enum":["application/pdf","image/png","image/jpeg","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],"description":"MIME type sniffed from the file's magic bytes."},"size_bytes":{"type":"integer","description":"Size of the stored document in bytes (max 10485760)."},"mandate_grantor_name":{"type":"string","nullable":true,"description":"Name of the mandate grantor (mandate docs only); null otherwise."},"created_at":{"type":"string","format":"date-time","description":"When the document was uploaded. RFC-3339 timestamp in UTC\n(e.g. `2026-08-27T10:15:00.000Z`).\n"}}},"KycDocumentUpload":{"type":"object","description":"Request body for `POST /v1/kyc/documents`.","required":["doc_type","content"],"properties":{"doc_type":{"type":"string","enum":["registry_extract","representative_id","authority_proof","mandate"],"description":"Document type. Workspace types (`registry_extract`,\n`representative_id`, `authority_proof`) must omit `company_id` and\n`mandate_grantor_name`; `mandate` requires both.\n"},"content":{"type":"string","format":"byte","description":"Base64-encoded file bytes (max 10 MB decoded). Required."},"filename":{"type":"string","description":"Original filename. Optional; defaults to `document`."},"mime_type":{"type":"string","description":"Declared MIME type. Optional — if present it must match the type\nsniffed from the bytes, otherwise the upload is rejected.\n"},"company_id":{"type":"string","description":"Company the mandate covers (mandate docs only; must belong to the workspace)."},"mandate_grantor_name":{"type":"string","description":"Name of the authorised representative who granted the mandate (mandate docs only)."}}},"KycSubmit":{"type":"object","description":"Request body for `POST /v1/kyc/submit`.","required":["company_id","kyc_legal_name","kyc_enterprise_number","attestation_name","attestation_role","attested","attestation_version"],"properties":{"company_id":{"type":"string","description":"The own/signing company (must be a company of the workspace)."},"kyc_legal_name":{"type":"string","description":"Legal entity name."},"kyc_enterprise_number":{"type":"string","description":"Enterprise/company number. Stored verbatim — no format validation."},"attestation_name":{"type":"string","description":"Name of the person accepting the attestation."},"attestation_role":{"type":"string","description":"Role of the person accepting the attestation."},"attested":{"type":"boolean","description":"Must be exactly `true` to accept the attestation."},"attestation_version":{"type":"string","description":"Must equal the current attestation version (`attestation.current_version`).","example":"2026-08-26"}}},"DocumentCreate":{"type":"object","description":"Request payload for creating and sending a Peppol document.","required":["number","issue_date","from","to","lines"],"properties":{"type":{"type":"string","enum":["invoice","credit_note"],"default":"invoice"},"number":{"type":"string","description":"Your document number (e.g., \"INV-2026-001\")"},"issue_date":{"type":"string","format":"date"},"due_date":{"type":"string","format":"date"},"currency":{"type":"string","default":"EUR","description":"ISO 4217 currency code"},"from":{"$ref":"#/components/schemas/Party"},"to":{"$ref":"#/components/schemas/Party"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"},"minItems":1},"note":{"type":"string","description":"Optional note to include on the document"},"payment_means":{"$ref":"#/components/schemas/PaymentMeans"},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/Attachment"}},"idempotency_key":{"type":"string","description":"Unique key to prevent duplicate sends.\nIf provided, a second request with the same key\nreturns the original result.\n"},"buyer_reference":{"type":"string","description":"Buyer reference (BT-10) — the buyer's identifier for the invoice,\ne.g. a purchase-order number.\n\nNote: this value is emitted both as `cbc:BuyerReference` (BT-10)\n**and** as `cac:OrderReference/cbc:ID` (BT-13). A dedicated BT-10\nfield decoupled from BT-13 is not yet available.\n"},"allowances":{"type":"array","description":"Document-level allowances (BG-20) — deductions applied before tax.","items":{"$ref":"#/components/schemas/AllowanceCharge"}},"charges":{"type":"array","description":"Document-level charges (BG-21) — additions applied before tax.","items":{"$ref":"#/components/schemas/AllowanceCharge"}},"tax_code":{"type":"string","description":"Document VAT category code (BT-118), e.g. `S` (standard), `Z` (zero),\n`E` (exempt), `AE` (reverse charge). Defaults to `S`, or `Z` when\nevery line is zero-rated.\n"},"vatex":{"type":"string","description":"VAT exemption reason code (BT-121) from the VATEX code list, e.g.\n`VATEX-EU-132`. Applies at document level only.\n"},"vatex_note":{"type":"string","description":"VAT exemption reason text (BT-120). Applies at document level only."},"invoice_period":{"$ref":"#/components/schemas/InvoicePeriod"}}},"AllowanceCharge":{"type":"object","description":"A document- or line-level allowance or charge. The sign is fixed by\nwhich array carries it (`allowances` deducts, `charges` adds), so\n`amount` is always a positive value.\n","required":["amount","reason","tax_rate"],"properties":{"amount":{"type":"number","description":"Positive amount, in the document currency, max 2 decimals."},"reason":{"type":"string","description":"Human-readable reason for the allowance or charge."},"tax_rate":{"type":"number","description":"VAT percentage (e.g. `21.00`) selecting the VAT breakdown this\nadjustment belongs to. At line level the line's own rate applies.\n"}}},"InvoicePeriod":{"type":"object","description":"Invoicing period (BG-14) — the service or billing date range.","properties":{"start_date":{"type":"string","format":"date"},"end_date":{"type":"string","format":"date"}}},"Party":{"type":"object","description":"A sender or recipient party on a document.","required":["name","tax_id"],"properties":{"name":{"type":"string"},"tax_id":{"type":"string","description":"Tax ID. Can be in Peppol format (`0208:0123456789`)\nor plain (`BE0123456789`) — we resolve the scheme.\n"},"address":{"$ref":"#/components/schemas/Address"},"email":{"type":"string","format":"email"},"peppol_id":{"type":"string","pattern":"^\\d{4}:\\S+$","description":"Explicit Peppol participant ID (overrides tax_id lookup).\nFormat: `scheme:id` — a 4-digit scheme, a colon, then a non-empty\nid (e.g., `0208:0785666049`). Validated at ingest: a malformed id\nis rejected synchronously with `invalid_peppol_id` (422) instead of\nfailing later at the provider. Scheme `0208` (BE enterprise number)\nmust be a 10-digit CBE number with a valid mod-97 checksum; a\nleading `BE` is normalized away.\n"}}},"Address":{"type":"object","description":"A postal address.","properties":{"street":{"type":"string"},"city":{"type":"string"},"postal_code":{"type":"string"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code"}}},"LineItem":{"type":"object","description":"A single invoice or credit-note line.","required":["description","quantity","unit_price","tax_rate"],"properties":{"description":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string","default":"C62","description":"UN/CEFACT unit code (C62 = unit/piece)"},"unit_price":{"type":"number","description":"Price per unit (excl. tax)"},"tax_rate":{"type":"number","description":"VAT percentage (e.g., 21.00)"},"discount":{"type":"number","deprecated":true,"description":"Absolute discount amount on this line, in the document currency\n(not a percentage). Deprecated — use line-level `allowances[]`.\n"},"allowances":{"type":"array","description":"Line-level allowances (BG-27) — deductions from the line net.","items":{"$ref":"#/components/schemas/AllowanceCharge"}},"charges":{"type":"array","description":"Line-level charges (BG-28) — additions to the line net.","items":{"$ref":"#/components/schemas/AllowanceCharge"}}}},"PaymentMeans":{"type":"object","description":"How the document should be paid (method and bank details).","properties":{"method":{"type":"string","enum":["bank_transfer","card","direct_debit","other"],"default":"bank_transfer"},"iban":{"type":"string"},"bic":{"type":"string"},"reference":{"type":"string","description":"Payment reference / structured communication"}}},"Attachment":{"type":"object","description":"A file to embed with the document, as base64 content.","required":["filename","content"],"properties":{"filename":{"type":"string"},"content":{"type":"string","format":"byte","description":"Base64-encoded file content"},"mime_type":{"type":"string","default":"application/pdf"}}},"Document":{"type":"object","description":"A sent document with computed totals and delivery status.","properties":{"id":{"type":"string","description":"peppol.sh document ID (prefixed `doc_`)"},"type":{"type":"string","enum":["invoice","credit_note"]},"number":{"type":"string"},"status":{"$ref":"#/components/schemas/DocumentStatus"},"from":{"$ref":"#/components/schemas/PartyInfo"},"to":{"$ref":"#/components/schemas/PartyInfo"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"}},"subtotal":{"type":"number"},"tax_total":{"type":"number"},"total":{"type":"number"},"currency":{"type":"string"},"issue_date":{"type":"string","format":"date"},"due_date":{"type":"string","format":"date"},"created_at":{"type":"string","format":"date-time"},"sent_at":{"type":"string","format":"date-time","nullable":true},"delivered_at":{"type":"string","format":"date-time","nullable":true}}},"DocumentStatus":{"type":"string","description":"Delivery status of a document.","enum":["queued","sending","delivered","failed"]},"PartyInfo":{"type":"object","description":"Resolved party identity as stored on a sent document.","properties":{"name":{"type":"string"},"tax_id":{"type":"string"},"peppol_id":{"type":"string"}}},"DocumentEvent":{"type":"object","description":"A single entry in a document's delivery-history timeline.","properties":{"event":{"type":"string","enum":["created","validated","queued","sending","delivered","failed"]},"timestamp":{"type":"string","format":"date-time"},"detail":{"type":"string","nullable":true}}},"DocumentAttachment":{"type":"object","description":"Metadata for one attachment stored with a document.","required":["id","filename","mime_type","size_bytes","sha256","created_at"],"properties":{"id":{"type":"string","example":"att_a1b2c3d4"},"filename":{"type":"string"},"mime_type":{"type":"string","description":"Magic-byte-verified MIME type."},"size_bytes":{"type":"integer"},"sha256":{"type":"string","description":"Lowercase hex SHA-256 of the stored bytes."},"created_at":{"type":"string","format":"date-time"}}},"EventWithContext":{"type":"object","description":"A document event enriched with its document and company context.","required":["id","document_id","document_number","recipient","company_id","company_name","type","to_status","created_at"],"properties":{"id":{"type":"string"},"document_id":{"type":"string"},"document_number":{"type":"string"},"recipient":{"type":"string","nullable":true},"company_id":{"type":"string"},"company_name":{"type":"string"},"type":{"type":"string","enum":["queued","sending","delivered","failed","retry"]},"from_status":{"type":"string","nullable":true},"to_status":{"type":"string"},"message":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"DocumentList":{"type":"object","description":"A cursor-paginated page of documents.","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Document"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}},"SmpLookupResult":{"type":"object","description":"Result of a Peppol participant lookup via SMP.\nIncludes NAPTR resolution details, supported document types, and business card information.\n","properties":{"participant_id":{"type":"object","properties":{"scheme":{"type":"string","description":"EAS scheme code (e.g., \"0208\" for Belgium)"},"id":{"type":"string","description":"Participant identifier value"}}},"domain":{"type":"string","enum":["sml","smk"],"description":"SML domain used for lookup"},"naptr_domain":{"type":"string","description":"The DNS NAPTR domain queried (SHA-256 hash + scheme + SML domain)"},"smp_url":{"type":"string","format":"uri","description":"The resolved SMP base URL"},"services":{"type":"array","description":"List of supported document types and their endpoints","items":{"type":"object","properties":{"document_type_id":{"type":"string","description":"Full document type identifier (e.g., busdox-docid-qns::urn:oasis:...)"},"document_type_name":{"type":"string","nullable":true,"description":"Human-readable document type name (e.g., \"Peppol BIS Billing UBL Invoice V3\")"},"process_id":{"type":"string","description":"Process identifier (e.g., cenbii-procid-ubl::urn:fdc:peppol.eu:...)"},"process_name":{"type":"string","nullable":true,"description":"Human-readable process name (e.g., \"Peppol BIS Billing 3.0\")"},"endpoints":{"type":"array","items":{"type":"object","properties":{"transport_profile":{"type":"string","description":"Transport protocol (e.g., \"peppol-transport-as4-v2_0\")"},"url":{"type":"string","format":"uri","description":"Access Point endpoint URL"},"certificate":{"type":"string","description":"Base64-encoded X.509 certificate (raw PEM)"},"cert_subject":{"type":"string","nullable":true,"description":"Parsed certificate subject (e.g., \"CN=Access Point, O=Corp, C=BE\")"},"cert_issuer":{"type":"string","nullable":true,"description":"Parsed certificate issuer"},"cert_valid_from":{"type":"string","nullable":true,"description":"Certificate validity start (ISO 8601)"},"cert_valid_to":{"type":"string","nullable":true,"description":"Certificate validity end (ISO 8601)"},"cert_fingerprint_sha256":{"type":"string","nullable":true,"description":"SHA-256 fingerprint of the DER-encoded certificate (lowercase hex)"},"activation_date":{"type":"string","nullable":true},"expiration_date":{"type":"string","nullable":true},"technical_contact":{"type":"string","nullable":true,"description":"Technical contact URL"},"technical_info":{"type":"string","nullable":true,"description":"Technical information URL"}}}}}}},"business_card":{"type":"object","nullable":true,"description":"Business card information for the participant (if available)","properties":{"participant_id":{"type":"object","properties":{"scheme":{"type":"string"},"id":{"type":"string"}}},"entities":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","nullable":true,"description":"Business entity name"},"country_code":{"type":"string","nullable":true,"description":"ISO 3166-1 alpha-2 country code"},"geographic_info":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"type":"object","properties":{"scheme":{"type":"string","description":"Identifier scheme (e.g., \"VAT\")"},"value":{"type":"string","description":"Identifier value"}}}},"website_urls":{"type":"array","items":{"type":"string","format":"uri"}},"contacts":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"email":{"type":"string","format":"email","nullable":true}}}}}}}}}}},"DnsLookupResult":{"type":"object","description":"DNS-only resolution result for a Peppol participant.\nReturns the NAPTR hostname and resolved SMP URL without fetching service metadata.\n","properties":{"peppol_id":{"type":"string","description":"The Peppol participant ID that was resolved"},"naptr_hostname":{"type":"string","description":"The computed NAPTR DNS hostname"},"smp_url":{"type":"string","format":"uri","description":"The resolved SMP base URL from the NAPTR record"},"domain":{"type":"string","enum":["sml","smk"],"description":"The SML domain used for resolution"}}},"ValidationResult":{"type":"object","description":"Outcome of validating a document payload, with errors and warnings.","properties":{"valid":{"type":"boolean"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ValidationIssue"}},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ValidationIssue"}}}},"ValidationIssue":{"type":"object","required":["message"],"description":"A single validation issue attached to a field within a document payload.\n","properties":{"path":{"type":"string","description":"Dot-path to the offending field, e.g. `buyer.tax_id` or\n`lines[0].unit_price`. Omitted when the issue is document-wide.\n"},"code":{"type":"string","description":"Machine-readable issue code from the validator (e.g. `invalid_format`)."},"message":{"type":"string","description":"Human-readable description of the issue."}}},"Webhook":{"type":"object","description":"A webhook subscription for real-time document event notifications.","properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["document.queued","document.sending","document.delivered","document.failed","credits.low"]}},"active":{"type":"boolean"},"secret":{"type":"string","description":"HMAC signing secret (`whsec_…`). Returned only on creation and on\nsecret rotation; omitted from all other responses. Used to verify\nthe `X-Peppol-Signature-V2` / `X-Peppol-Signature` delivery headers.\n"},"created_at":{"type":"string","format":"date-time"}}},"WebhookCreate":{"type":"object","description":"Request body for creating a webhook subscription.","required":["url","events"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["document.queued","document.sending","document.delivered","document.failed","credits.low"]}},"secret":{"type":"string","description":"Signing secret for webhook payloads.\nIf not provided, one will be generated.\n"}}},"WebhookDelivery":{"type":"object","description":"A single delivery attempt for a webhook event.","properties":{"id":{"type":"string","description":"Delivery id (e.g. `whd_abc123`)."},"webhook_id":{"type":"string"},"document_id":{"type":"string","nullable":true,"description":"The document that triggered the event, or null for non-document events (e.g. `credits.low`, `webhook.test`)."},"event":{"type":"string","description":"Event type (e.g. `document.queued`, `credits.low`, `webhook.test`)."},"status":{"type":"string","enum":["pending","delivered","failed"]},"status_code":{"type":"integer","nullable":true,"description":"Last HTTP status code returned by the receiver, if the request completed."},"attempts":{"type":"integer","description":"Number of delivery attempts made so far."},"last_error":{"type":"string","nullable":true},"next_retry_at":{"type":"string","nullable":true},"delivered_at":{"type":"string","nullable":true},"created_at":{"type":"string"}}},"WebhookDeliveryList":{"type":"object","description":"A cursor-paginated page of webhook delivery attempts.","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true,"description":"Pass this back as the `cursor` query param to fetch the next page."}}},"WebhookTestResponse":{"type":"object","description":"Result of dispatching a synthetic test event to a webhook.","properties":{"delivery_id":{"type":"string","description":"Recorded delivery id for this test attempt — look up in the deliveries list."},"success":{"type":"boolean","description":"True when the receiver returned a 2xx status."},"status_code":{"type":"integer","nullable":true,"description":"HTTP status code from the receiver, or null if the request never completed."},"error":{"type":"string","nullable":true,"description":"Error message when the request failed to reach the receiver or returned non-2xx."}}},"WebhookRotateSecretResponse":{"type":"object","description":"Result of rotating a webhook's signing secret.","properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"secret":{"type":"string","description":"The new signing secret. Returned ONCE — store it now; it cannot be retrieved again."},"secret_overlap_expires_at":{"type":"integer","description":"Unix seconds when the previous (overlap) secret stops signing deliveries (24h after rotation)."}}},"ErrorObject":{"type":"object","required":["error"],"description":"Canonical error envelope returned by every non-2xx response.\n\n**Shape:** `{ error: { type, code, message, param?, details? } }`\n\n### Error codes\n\n| `type` | `code` | Meaning |\n|---|---|---|\n| validation_error | `missing_field` | A required field was omitted. |\n| validation_error | `invalid_field` | A field is present but has an invalid value or format. |\n| validation_error | `email_taken` | 409 — an account with this email already exists. |\n| validation_error | `already_member` | 409 — account is already a member of this company. |\n| validation_error | `no_fields_to_update` | PATCH with an empty update body. |\n| validation_error | `last_owner` | Cannot remove/demote the last owner of a company. |\n| validation_error | `invalid_account_status` | Operation not allowed in the account's current status. |\n| validation_error | `invalid_peppol_id` | Peppol participant ID has an invalid format. |\n| validation_error | `invalid_scheme` | Peppol scheme (EAS) is unknown. |\n| validation_error | `batch_too_large` | Batch contains more than 100 documents. |\n| validation_error | `mixed_company_ids` | Batch mixes multiple `company_id`s. |\n| validation_error | `ubl_schema_invalid` | UBL payload failed Zod schema validation (see `details.issues`). |\n| validation_error | `ubl_business_rule` | UBL payload failed business rule validation (see `details`). |\n| validation_error | `ubl_missing_document_type` | UBL payload has neither `invoiceLine` nor `creditNoteLine`. |\n| validation_error | `unsupported_content_type` | Request Content-Type is not supported by this endpoint. |\n| authentication_error | `missing_api_key` | `Authorization` header missing. |\n| authentication_error | `invalid_api_key` | API key format is wrong or key is unknown. |\n| authentication_error | `disabled_account` | 403 — account is disabled. |\n| authorization_error | `not_a_member` | Caller is not a member of the specified company. |\n| authorization_error | `insufficient_role` | Caller's role is too low for this operation. |\n| validation_error | `invalid_country` | 400 — country is unknown or not Peppol-active. |\n| validation_error | `kyc_missing_content` | 400 — KYC upload `content` is absent or empty. |\n| validation_error | `kyc_invalid_encoding` | 400 — KYC upload `content` is not valid base64. |\n| validation_error | `kyc_invalid_doc_type` | 400 — `doc_type` is not a recognised KYC document type. |\n| validation_error | `kyc_unsupported_type` | 400 — sniffed magic bytes match no accepted type. |\n| validation_error | `kyc_mime_mismatch` | 400 — declared `mime_type` disagrees with the sniffed type. |\n| validation_error | `kyc_mandate_requires_company` | 400 — a `mandate` upload is missing `company_id`/`mandate_grantor_name`. |\n| validation_error | `kyc_doc_pairing_invalid` | 400 — a workspace document carries `company_id`/`mandate_grantor_name`. |\n| validation_error | `kyc_document_too_large` | 413 — KYC document exceeds the 10 MB cap. |\n| validation_error | `kyc_locked` | 409 — KYC uploads are locked (status is `pending`/`approved`). |\n| validation_error | `kyc_not_submittable` | 409 — workspace is not in a submittable state. |\n| validation_error | `kyc_no_companies` | 422 — workspace has no companies. |\n| validation_error | `kyc_no_own_company` | 422 — submit `company_id` was blank. |\n| validation_error | `kyc_invalid_company` | 422 — submit `company_id` is not a company of this workspace. |\n| validation_error | `kyc_missing_documents` | 422 — a required workspace doc type is missing (`details.missing`). |\n| validation_error | `kyc_missing_legal_name` | 422 — `kyc_legal_name` was blank. |\n| validation_error | `kyc_missing_enterprise_number` | 422 — `kyc_enterprise_number` was blank. |\n| validation_error | `kyc_attestation_incomplete` | 422 — `attestation_name` or `attestation_role` was blank. |\n| validation_error | `kyc_attestation_not_accepted` | 422 — `attested` was not `true`. |\n| validation_error | `kyc_attestation_version_mismatch` | 422 — `attestation_version` is not the current version. |\n| not_found | `company_not_found` | Company does not exist or caller cannot see it. |\n| not_found | `workspace_not_found` | Workspace row not found. |\n| not_found | `webhook_not_found` | Webhook not found in the caller's workspace. |\n| not_found | `account_not_found` | Authenticated account was not found. |\n| not_found | `member_not_found` | Target account is not a member of the company. |\n| not_found | `document_not_found` | Document does not exist. |\n| not_found | `api_key_not_found` | API key not found or already revoked. |\n| not_found | `participant_not_found` | Peppol participant not found in SML/SMP. |\n| not_found | `route_not_found` | HTTP method/path does not match any route. |\n| provider_error | `provider_error` | Upstream provider returned an unexpected error. |\n| provider_error | `provider_validation_failed` | Upstream provider rejected the payload (see `details`). |\n| provider_error | `smp_error` | SMP or DNS lookup failed. |\n| rate_limit_error | `rate_limited` | Too many requests. |\n| billing_error | `insufficient_credits` | Company does not have enough credits to send the document(s). Purchase more via `POST /v1/billing/checkout`. |\n| billing_error | `sandbox_not_allowed` | Billing endpoints (e.g. `POST /v1/billing/checkout`) reject sandbox API keys. |\n| internal_error | `internal_error` | Unexpected server error. |\n","properties":{"error":{"type":"object","required":["type","code","message"],"properties":{"type":{"type":"string","enum":["validation_error","authentication_error","authorization_error","not_found","rate_limit_error","provider_error","billing_error","internal_error"],"description":"Error category:\n- `validation_error` — invalid input\n- `authentication_error` — missing or invalid API key\n- `authorization_error` — authenticated but not authorized (e.g., wrong role)\n- `not_found` — resource does not exist\n- `rate_limit_error` — too many requests\n- `provider_error` — upstream provider failure\n- `billing_error` — credit/billing problem (e.g. insufficient credits). Returned as HTTP 402.\n- `internal_error` — unexpected server error\n"},"code":{"type":"string","description":"Machine-readable error code. See the Error codes table above for the\nfull taxonomy. SDK clients should switch on `code` (not `message`).\n"},"message":{"type":"string","description":"Human-readable error message."},"param":{"type":"string","description":"Dot-path to the field that caused the error, e.g. `buyer.tax_id`\nor `lines[0].unit_price`. Omitted for non-field errors.\n"},"details":{"description":"Optional structured payload with additional context (e.g. the list\nof issues for UBL schema/business-rule failures, or the provider's\nraw validation errors for `provider_validation_failed`).\n"}}}}}},"responses":{"BadRequest":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"Unauthenticated":{"description":"Missing or invalid API key (`missing_api_key` or `invalid_api_key`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"},"example":{"error":{"type":"authentication_error","code":"missing_api_key","message":"Missing API key","retry":{"retryable":false}}}}}},"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"Forbidden":{"description":"Authenticated but not authorized (e.g. not a member of the target company, or insufficient role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"PaymentRequired":{"description":"Insufficient credits — purchase more via `POST /v1/billing/checkout`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"ValidationError":{"description":"Payload validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"RateLimited":{"description":"Too many requests","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"InternalError":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}}}}}