{
  "openapi": "3.1.0",
  "info": {
    "title": "ttdHUB API",
    "version": "1.0.0",
    "summary": "REST API tanda tangan elektronik ttdHUB: dokumen, penerima, field, permintaan tanda tangan, jejak audit, webhook, penandatanganan tersemat, dan API mitra.",
    "description": "Semua endpoint berada di bawah `/api/v1` pada domain ttdHUB Anda dan memakai JSON (kecuali unggah berkas: `multipart/form-data`).\n\n**Autentikasi organisasi:** header `X-API-Key: ttd_…` (buat di halaman Integrasi). Kunci mewarisi peran penggunanya; peran Peninjau hanya dapat membaca. Operasi tulis lewat API key membutuhkan fitur paket `api_webhooks` (paket Bisnis); membaca, mengunduh, audit, dan ekspor selalu boleh.\n\n**Autentikasi mitra:** header `X-Partner-Key: ttdp_…` di bawah `/partner-api`. Untuk bertindak atas organisasi pelanggan, sertakan juga `X-Organization-Id`. Semua endpoint Dokumen, Penerima, Field, Permintaan tanda tangan, Audit, dan Embed tersedia di bawah `/partner-api/…` dengan jalur yang sama (mis. `POST /partner-api/documents`) dan dijalankan sebagai akun layanan organisasi tersebut. Mitra tidak pernah memegang kunci atau sesi pemilik organisasi.\n\n**Galat:** `{error, code?}`. Batas paket: 402 `{code:'plan_required', feature, required_plan, required_plan_name}`; kuota: 402 `{code:'quota_exceeded', key, limit, used, period}`. Urutan pemeriksaan: 401 → 403 → 404 → 400 → 402.\n\n**Batas laju:** 600 permintaan/menit per IP untuk `/api/v1`, dan 600/menit per kunci mitra.\n\nUang selalu dalam rupiah bulat (integer)."
  },
  "servers": [{ "url": "/api/v1", "description": "Server ttdHUB yang sama dengan halaman ini" }],
  "tags": [
    { "name": "Dokumen", "description": "Unggah PDF, lihat, unduh, dan batalkan dokumen. Dokumen baru berstatus `draft`." },
    { "name": "Penerima", "description": "Peran penerima dokumen (Penanda tangan, Penyetuju, Peninjau/tembusan). Field ditautkan ke peran." },
    { "name": "Field", "description": "Field formulir pada halaman PDF. Koordinat `x, y, w, h` adalah pecahan 0–1 dari lebar/tinggi halaman, titik asal kiri atas." },
    { "name": "Permintaan tanda tangan", "description": "Kirim dokumen ke penerima. Setiap pengiriman memakai 1 kuota dokumen bulanan organisasi." },
    { "name": "Audit", "description": "Jejak audit lengkap dokumen. Token penanda tangan, hash kode, dan berkas identitas tidak pernah dikembalikan." },
    { "name": "Integrasi", "description": "API key dan webhook organisasi. Webhook dikelola Pemilik/Admin; membuat API key hanya lewat sesi browser." },
    { "name": "Embed", "description": "Penandatanganan tersemat (iframe) untuk organisasi pelanggan mitra. URL berlaku 10 menit, sekali pakai, dan hanya dapat dibingkai oleh origin mitra yang disetujui." },
    { "name": "Mitra", "description": "API OEM untuk mitra: buat organisasi pelanggan, pantau pemakaian, dan jalankan API dokumen atas nama organisasi pelanggan (header `X-Organization-Id`)." }
  ],
  "security": [{ "ApiKey": [] }],
  "paths": {
    "/documents": {
      "post": {
        "tags": ["Dokumen"], "operationId": "uploadDocument", "summary": "Unggah dokumen PDF",
        "description": "PDF maksimal 25 MB. Dokumen baru berstatus `draft`.",
        "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "required": ["file"], "properties": {
          "file": { "type": "string", "format": "binary", "description": "Berkas PDF (application/pdf)" },
          "title": { "type": "string", "maxLength": 200, "description": "Judul; bawaan: nama berkas tanpa .pdf" } } } } } },
        "responses": {
          "201": { "description": "Dokumen dibuat", "content": { "application/json": { "schema": { "type": "object", "properties": { "document": { "$ref": "#/components/schemas/Document" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" }, "403": { "$ref": "#/components/responses/Forbidden" }, "413": { "description": "Berkas terlalu besar" }
        }
      },
      "get": {
        "tags": ["Dokumen"], "operationId": "listDocuments", "summary": "Daftar dokumen",
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string", "maxLength": 200 }, "description": "Cari judul" },
          { "name": "status", "in": "query", "schema": { "$ref": "#/components/schemas/DocumentStatus" } },
          { "name": "folder_id", "in": "query", "schema": { "type": "string", "format": "uuid" } },
          { "name": "scope", "in": "query", "schema": { "type": "string", "enum": ["mine", "team"] }, "description": "`mine` = hanya dokumen milik pemanggil" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 100 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "default": 0 } }
        ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "documents": { "type": "array", "items": { "$ref": "#/components/schemas/Document" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/documents/import": {
      "post": {
        "tags": ["Dokumen"], "operationId": "importDocument", "summary": "Ambil PDF dari URL publik",
        "description": "URL diambil server (dilindungi SSRF: hanya alamat publik, tanpa pengalihan), maksimal 25 MB.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "format": "uri", "maxLength": 2048 }, "title": { "type": "string", "maxLength": 200 } } } } } },
        "responses": { "201": { "description": "Dokumen dibuat", "content": { "application/json": { "schema": { "type": "object", "properties": { "document": { "$ref": "#/components/schemas/Document" } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" } }
      }
    },
    "/documents/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "get": {
        "tags": ["Dokumen"], "operationId": "getDocument", "summary": "Detail dokumen beserta halaman, penerima, dan field",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": {
          "document": { "$ref": "#/components/schemas/Document" },
          "pages": { "type": "array", "items": { "$ref": "#/components/schemas/Page" } },
          "roles": { "type": "array", "items": { "$ref": "#/components/schemas/Role" } },
          "fields": { "type": "array", "items": { "$ref": "#/components/schemas/Field" } } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" } }
      },
      "patch": {
        "tags": ["Dokumen"], "operationId": "updateDocument", "summary": "Ubah folder atau judul (judul hanya untuk draf)",
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "folder_id": { "type": ["string", "null"], "format": "uuid" }, "title": { "type": "string", "maxLength": 200 } } } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "document": { "$ref": "#/components/schemas/Document" } } } } } }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/documents/{id}/file.pdf": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "get": {
        "tags": ["Dokumen"], "operationId": "downloadDocument", "summary": "Unduh PDF",
        "description": "Salinan yang belum selesai diberi watermark DRAF. `signed=1` mengunduh salinan final bersegel (PAdES) setelah selesai.",
        "parameters": [
          { "name": "signed", "in": "query", "schema": { "type": "string", "enum": ["1"] } },
          { "name": "raw", "in": "query", "schema": { "type": "string", "enum": ["1"] }, "description": "Tanpa watermark; hanya untuk pengguna yang dapat mengubah dokumen" }
        ],
        "responses": { "200": { "description": "PDF", "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } } }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/documents/{id}/void": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "post": {
        "tags": ["Dokumen"], "operationId": "voidDocument", "summary": "Batalkan dokumen yang sedang berjalan",
        "description": "Selalu tersedia lewat API key, apa pun paketnya. Memicu webhook `voided`.",
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "reason": { "type": "string", "maxLength": 500 } } } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Ok" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/document-roles": {
      "post": {
        "tags": ["Penerima"], "operationId": "createRole", "summary": "Tambah penerima ke dokumen",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "allOf": [{ "type": "object", "required": ["document_id"], "properties": { "document_id": { "type": "string", "format": "uuid" } } }, { "$ref": "#/components/schemas/RoleInput" }] } } } },
        "responses": { "201": { "description": "Dibuat", "content": { "application/json": { "schema": { "type": "object", "properties": { "role": { "$ref": "#/components/schemas/Role" } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/document-roles/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "put": {
        "tags": ["Penerima"], "operationId": "updateRole", "summary": "Ubah penerima",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RoleInput" } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "role": { "$ref": "#/components/schemas/Role" } } } } } }, "404": { "$ref": "#/components/responses/NotFound" } }
      },
      "delete": { "tags": ["Penerima"], "operationId": "deleteRole", "summary": "Hapus penerima", "responses": { "204": { "description": "Dihapus" } } }
    },
    "/form-fields": {
      "post": {
        "tags": ["Field"], "operationId": "createField", "summary": "Tambah satu field",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "allOf": [{ "type": "object", "required": ["document_id"], "properties": { "document_id": { "type": "string", "format": "uuid" } } }, { "$ref": "#/components/schemas/FieldInput" }] } } } },
        "responses": { "201": { "description": "Dibuat", "content": { "application/json": { "schema": { "type": "object", "properties": { "field": { "$ref": "#/components/schemas/Field" } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "402": { "$ref": "#/components/responses/PaymentRequired" } }
      }
    },
    "/form-fields/update-many": {
      "post": {
        "tags": ["Field"], "operationId": "saveFields", "summary": "Simpan banyak field sekaligus (buat & ubah)",
        "description": "Entri dengan `id` mengubah field yang ada; tanpa `id` membuat field baru. `ref` (≤ 64 karakter) dapat dirujuk oleh `condition.field_id` field lain dalam batch yang sama. Maksimal 1000 field.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["document_id", "fields"], "properties": {
          "document_id": { "type": "string", "format": "uuid" },
          "fields": { "type": "array", "maxItems": 1000, "items": { "allOf": [{ "$ref": "#/components/schemas/FieldInput" }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "ref": { "type": "string", "maxLength": 64 } } }] } } } } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "fields": { "type": "array", "items": { "$ref": "#/components/schemas/Field" } } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "402": { "$ref": "#/components/responses/PaymentRequired" } }
      }
    },
    "/form-fields/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "delete": { "tags": ["Field"], "operationId": "deleteField", "summary": "Hapus field", "responses": { "204": { "description": "Dihapus" } } }
    },
    "/sign-requests/create-with-document-roles": {
      "post": {
        "tags": ["Permintaan tanda tangan"], "operationId": "sendDocument", "summary": "Kirim dokumen untuk ditandatangani",
        "description": "Membuat/memperbarui peran penerima lalu mengirim undangan ke tahap pertama. `routing_stage` > 1, Penyetuju, atau `allow_delegation` membutuhkan fitur `routing_stages`; field kondisional/angka/rumus `logic_fields`; stempel/lampiran `stamp_attachments`; OTP SMS `sms_otp`. Memakai 1 kuota dokumen (402 `quota_exceeded` bila habis). Memicu webhook `sent`.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } } },
        "responses": {
          "201": { "description": "Terkirim", "content": { "application/json": { "schema": { "type": "object", "properties": {
            "sign_request": { "$ref": "#/components/schemas/SignRequest" },
            "signers": { "type": "array", "items": { "$ref": "#/components/schemas/SignerLink" } } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "402": { "$ref": "#/components/responses/PaymentRequired" }, "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/sign-requests/{id}/void": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "post": {
        "tags": ["Permintaan tanda tangan"], "operationId": "voidSignRequest", "summary": "Batalkan permintaan tanda tangan",
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "reason": { "type": "string" } } } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Ok" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" } }
      }
    },
    "/sign-requests/signers/{signerId}/reassign": {
      "parameters": [{ "name": "signerId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
      "post": {
        "tags": ["Permintaan tanda tangan"], "operationId": "reassignSigner", "summary": "Ganti penanda tangan yang belum menandatangani",
        "description": "Tautan lama berhenti berlaku; penerima baru menerima undangan.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["email"], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254 }, "name": { "type": "string", "maxLength": 200 } } } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "sign_url": { "type": "string", "format": "uri" } } } } } }, "409": { "$ref": "#/components/responses/Conflict" } }
      }
    },
    "/audit/{documentId}": {
      "parameters": [{ "name": "documentId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
      "get": {
        "tags": ["Audit"], "operationId": "getAudit", "summary": "Jejak audit dokumen",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": {
          "document": { "$ref": "#/components/schemas/Document" },
          "activities": { "type": "array", "items": { "$ref": "#/components/schemas/Activity" } },
          "signers": { "type": "array", "items": { "$ref": "#/components/schemas/AuditSigner" } },
          "emails": { "type": "array", "items": { "type": "object", "properties": { "to_email": { "type": "string" }, "subject": { "type": "string" }, "status": { "type": "string", "enum": ["sent", "failed"] }, "sent_at": { "type": "string", "format": "date-time" } } } },
          "attachments": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "original_filename": { "type": "string" }, "mime": { "type": "string" }, "size_bytes": { "type": "integer" }, "signer_email": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" } } } } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/integrations/api-keys": {
      "get": { "tags": ["Integrasi"], "operationId": "listApiKeys", "summary": "Daftar API key (Anggota: miliknya; Admin: seluruh organisasi)",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "api_keys": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "label": { "type": ["string", "null"] }, "prefix": { "type": "string" }, "revoked": { "type": "boolean" }, "last_used_at": { "type": ["string", "null"], "format": "date-time" }, "created_at": { "type": "string", "format": "date-time" } } } } } } } } } } }
    },
    "/integrations/api-keys/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "delete": { "tags": ["Integrasi"], "operationId": "revokeApiKey", "summary": "Cabut API key", "responses": { "204": { "description": "Dicabut" } } }
    },
    "/integrations/webhooks": {
      "get": { "tags": ["Integrasi"], "operationId": "listWebhooks", "summary": "Daftar webhook (Pemilik/Admin)",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "webhooks": { "type": "array", "items": { "$ref": "#/components/schemas/Webhook" } } } } } } } } },
      "post": { "tags": ["Integrasi"], "operationId": "createWebhook", "summary": "Tambah webhook (Pemilik/Admin)",
        "description": "URL harus alamat publik (diperiksa ulang saat pengiriman). Maksimal 25 webhook. `secret` HMAC hanya ditampilkan sekali.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "format": "uri", "maxLength": 2048 }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEvent" }, "description": "Kosong = semua event" } } } } } },
        "responses": { "201": { "description": "Dibuat", "content": { "application/json": { "schema": { "type": "object", "properties": { "webhook": { "$ref": "#/components/schemas/Webhook" }, "secret": { "type": "string" } } } } } }, "402": { "$ref": "#/components/responses/PaymentRequired" } } }
    },
    "/integrations/webhooks/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "delete": { "tags": ["Integrasi"], "operationId": "deleteWebhook", "summary": "Hapus webhook", "responses": { "204": { "description": "Dihapus" } } }
    },
    "/integrations/webhooks/{id}/deliveries": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "get": { "tags": ["Integrasi"], "operationId": "listWebhookDeliveries", "summary": "50 pengiriman terakhir sebuah webhook",
        "description": "`status`: `sent`, `failed`, atau `skipped_plan` (organisasi tidak lagi memiliki fitur API & webhook; webhook tidak dihapus).",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "deliveries": { "type": "array", "items": { "type": "object", "properties": { "event": { "type": "string" }, "status": { "type": "string" }, "response_code": { "type": ["integer", "null"] }, "created_at": { "type": "string", "format": "date-time" } } } } } } } } } } }
    },
    "/embed/sign-url": {
      "post": {
        "tags": ["Embed"], "operationId": "createEmbedUrl", "summary": "Buat URL penandatanganan tersemat",
        "description": "Hanya untuk organisasi pelanggan mitra (atau lewat `/partner-api/embed/sign-url` dengan `X-Organization-Id`). `origin` harus persis salah satu origin embed mitra yang disetujui (`https://host[:port]`); bawaan: origin dari `return_url`, atau satu-satunya origin yang disetujui. `return_url` harus memakai origin yang sama. URL berlaku 10 menit dan hanya dapat ditukar sekali; buat URL baru untuk setiap pemuatan iframe.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["sign_request_id", "signer_email"], "properties": {
          "sign_request_id": { "type": "string", "format": "uuid" },
          "signer_email": { "type": "string", "format": "email" },
          "origin": { "type": "string", "examples": ["https://app.mitra.co.id"] },
          "return_url": { "type": "string", "format": "uri", "examples": ["https://app.mitra.co.id/kontrak/selesai"] } } } } } },
        "responses": {
          "201": { "description": "URL dibuat", "content": { "application/json": { "schema": { "type": "object", "properties": { "url": { "type": "string", "format": "uri", "examples": ["https://ttd.example.co.id/sign.html?embed=XXXX"] }, "expires_at": { "type": "string", "format": "date-time" }, "origin": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/embed/exchange": {
      "post": {
        "tags": ["Embed"], "operationId": "exchangeEmbedToken", "summary": "Tukar token embed (dipakai halaman tanda tangan)",
        "description": "Dipanggil oleh `sign.html?embed=…`, bukan oleh server mitra. Sekali pakai; pemanggilan berikutnya, token kedaluwarsa, mitra ditangguhkan, atau origin yang dicabut menghasilkan 410 `embed_unavailable`.",
        "security": [],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["token"], "properties": { "token": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "sign_token": { "type": "string" }, "origin": { "type": "string" }, "return_url": { "type": ["string", "null"] }, "sign_request_id": { "type": "string", "format": "uuid" }, "expires_at": { "type": "string", "format": "date-time" }, "headerless": { "type": "boolean" } } } } } },
          "410": { "$ref": "#/components/responses/Gone" }
        }
      }
    },
    "/partner-api/me": {
      "get": { "tags": ["Mitra"], "operationId": "partnerMe", "summary": "Profil mitra", "security": [{ "PartnerKey": [] }],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "partner": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "status": { "type": "string", "enum": ["active", "suspended"] }, "plan_code": { "type": "string" }, "doc_price": { "type": "integer" }, "allowed_embed_origins": { "type": "array", "items": { "type": "string" } } } } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } }
    },
    "/partner-api/organizations": {
      "post": {
        "tags": ["Mitra"], "operationId": "createOrganization", "summary": "Buat organisasi pelanggan",
        "description": "Membuat organisasi dengan paket mitra (tanpa uji coba), akun layanan untuk API, dan undangan Pemilik yang dikirim langsung ke email pemilik (tautan undangan tidak pernah dikembalikan ke mitra). Maksimal 100 organisasi per 24 jam per mitra.",
        "security": [{ "PartnerKey": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["name", "owner_email"], "properties": {
          "name": { "type": "string", "maxLength": 120 },
          "owner_email": { "type": "string", "format": "email", "maxLength": 254 },
          "external_id": { "type": "string", "pattern": "^[A-Za-z0-9._:-]{1,100}$", "description": "Referensi Anda sendiri; unik per mitra" } } } } } },
        "responses": {
          "201": { "description": "Dibuat", "content": { "application/json": { "schema": { "type": "object", "properties": { "organization": { "$ref": "#/components/schemas/Organization" }, "owner_invite": { "$ref": "#/components/schemas/OwnerInvite" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" }, "429": { "description": "Batas 100 organisasi per 24 jam (`org_daily_limit`)" }
        }
      },
      "get": {
        "tags": ["Mitra"], "operationId": "listOrganizations", "summary": "Daftar organisasi pelanggan", "security": [{ "PartnerKey": [] }],
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string", "maxLength": 120 }, "description": "Cari nama atau external_id" },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "page_size", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } },
          { "$ref": "#/components/parameters/Period" }
        ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "total": { "type": "integer" }, "page": { "type": "integer" }, "page_size": { "type": "integer" }, "period": { "type": "string" }, "organizations": { "type": "array", "items": { "$ref": "#/components/schemas/Organization" } } } } } } } }
      }
    },
    "/partner-api/organizations/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "get": { "tags": ["Mitra"], "operationId": "getOrganization", "summary": "Detail organisasi pelanggan", "security": [{ "PartnerKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/Period" }],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "period": { "type": "string" }, "organization": { "$ref": "#/components/schemas/Organization" } } } } } }, "404": { "$ref": "#/components/responses/NotFound" } } }
    },
    "/partner-api/organizations/{id}/owner-invite": {
      "parameters": [{ "$ref": "#/components/parameters/Id" }],
      "post": { "tags": ["Mitra"], "operationId": "resendOwnerInvite", "summary": "Kirim ulang undangan Pemilik", "security": [{ "PartnerKey": [] }],
        "description": "Hanya selama organisasi belum memiliki Pemilik (409 `owner_exists`). Undangan lama dicabut.",
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "owner_email": { "type": "string", "format": "email" } } } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "owner_invite": { "$ref": "#/components/schemas/OwnerInvite" } } } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" } } }
    },
    "/partner-api/usage": {
      "get": { "tags": ["Mitra"], "operationId": "partnerUsage", "summary": "Pemakaian dokumen per organisasi pelanggan", "security": [{ "PartnerKey": [] }],
        "description": "Dokumen terkirim dalam periode (Asia/Jakarta), dasar tagihan `partner_usage` bulanan = dokumen × `doc_price`. Sertakan `X-Organization-Id` untuk satu organisasi.",
        "parameters": [{ "$ref": "#/components/parameters/Period" }, { "$ref": "#/components/parameters/OrganizationIdOptional" }],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Usage" } } } } } }
    },
    "/partner-api/sign-requests/create-with-document-roles": {
      "post": {
        "tags": ["Mitra"], "operationId": "partnerSendDocument", "summary": "Contoh: kirim dokumen atas nama organisasi pelanggan",
        "description": "Sama dengan `POST /sign-requests/create-with-document-roles`, dengan header `X-Partner-Key` + `X-Organization-Id`. Pola yang sama berlaku untuk `/partner-api/documents`, `/partner-api/document-roles`, `/partner-api/form-fields`, `/partner-api/sign-requests`, `/partner-api/audit`, dan `/partner-api/embed`.",
        "security": [{ "PartnerKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/OrganizationId" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } } },
        "responses": { "201": { "description": "Terkirim (lihat sendDocument)" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    }
  },
  "webhooks": {
    "sent": { "post": { "summary": "Dokumen dikirim", "description": "Dikirim ke URL webhook yang berlangganan event `sent`. Lihat header tanda tangan di WebhookEnvelope.",
      "parameters": [{ "$ref": "#/components/parameters/WhEvent" }, { "$ref": "#/components/parameters/WhTimestamp" }, { "$ref": "#/components/parameters/WhSignature" }],
      "requestBody": { "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/WebhookEnvelope" }, { "type": "object", "properties": { "event": { "const": "sent" }, "data": { "type": "object", "properties": { "document_id": { "type": "string", "format": "uuid" }, "sign_request_id": { "type": "string", "format": "uuid" }, "title": { "type": "string" }, "signers": { "type": "array", "items": { "type": "string", "format": "email" } } } } } }] } } } },
      "responses": { "2XX": { "description": "Diterima" } } } },
    "completed": { "post": { "summary": "Semua pihak selesai; salinan final bersegel tersedia",
      "parameters": [{ "$ref": "#/components/parameters/WhEvent" }, { "$ref": "#/components/parameters/WhTimestamp" }, { "$ref": "#/components/parameters/WhSignature" }],
      "requestBody": { "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/WebhookEnvelope" }, { "type": "object", "properties": { "event": { "const": "completed" }, "data": { "type": "object", "properties": { "document_id": { "type": "string", "format": "uuid" }, "signed_hash": { "type": "string", "description": "SHA-256 PDF final" } } } } }] } } } },
      "responses": { "2XX": { "description": "Diterima" } } } },
    "declined": { "post": { "summary": "Penerima menolak",
      "parameters": [{ "$ref": "#/components/parameters/WhEvent" }, { "$ref": "#/components/parameters/WhTimestamp" }, { "$ref": "#/components/parameters/WhSignature" }],
      "requestBody": { "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/WebhookEnvelope" }, { "type": "object", "properties": { "event": { "const": "declined" }, "data": { "type": "object", "properties": { "document_id": { "type": "string", "format": "uuid" }, "by": { "type": "string", "format": "email" }, "reason": { "type": ["string", "null"] } } } } }] } } } },
      "responses": { "2XX": { "description": "Diterima" } } } },
    "voided": { "post": { "summary": "Pengirim membatalkan dokumen",
      "parameters": [{ "$ref": "#/components/parameters/WhEvent" }, { "$ref": "#/components/parameters/WhTimestamp" }, { "$ref": "#/components/parameters/WhSignature" }],
      "requestBody": { "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/WebhookEnvelope" }, { "type": "object", "properties": { "event": { "const": "voided" }, "data": { "type": "object", "properties": { "document_id": { "type": "string", "format": "uuid" }, "reason": { "type": ["string", "null"] } } } } }] } } } },
      "responses": { "2XX": { "description": "Diterima" } } } }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": { "type": "apiKey", "in": "header", "name": "X-API-Key", "description": "API key organisasi (`ttd_…`), dibuat di halaman Integrasi." },
      "PartnerKey": { "type": "apiKey", "in": "header", "name": "X-Partner-Key", "description": "Kunci mitra (`ttdp_…`), dibuat di Portal mitra. Hanya berlaku di bawah `/partner-api`." }
    },
    "parameters": {
      "Id": { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
      "Period": { "name": "period", "in": "query", "schema": { "type": "string", "pattern": "^\\d{4}-(0[1-9]|1[0-2])$" }, "description": "Bulan `YYYY-MM` (Asia/Jakarta); bawaan bulan berjalan" },
      "OrganizationId": { "name": "X-Organization-Id", "in": "header", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "Id organisasi pelanggan milik mitra" },
      "OrganizationIdOptional": { "name": "X-Organization-Id", "in": "header", "required": false, "schema": { "type": "string", "format": "uuid" } },
      "WhEvent": { "name": "X-ttdHUB-Event", "in": "header", "required": true, "schema": { "$ref": "#/components/schemas/WebhookEvent" } },
      "WhTimestamp": { "name": "X-ttdHUB-Timestamp", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Detik Unix. Tolak cap waktu yang lebih tua dari beberapa menit." },
      "WhSignature": { "name": "X-ttdHUB-Signature", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Hex HMAC-SHA256(secret, `${timestamp}.${isi mentah}`). Bandingkan dengan perbandingan waktu-konstan." }
    },
    "responses": {
      "BadRequest": { "description": "Input tidak valid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "Kredensial tidak ada atau tidak valid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "Peran tidak mengizinkan, sesi browser diperlukan, atau mitra ditangguhkan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Tidak ditemukan (juga untuk data milik organisasi lain)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Conflict": { "description": "Status tidak mengizinkan aksi ini", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Gone": { "description": "Tidak lagi tersedia", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "PaymentRequired": { "description": "Fitur di luar paket atau kuota habis", "content": { "application/json": { "schema": { "oneOf": [{ "$ref": "#/components/schemas/PlanRequired" }, { "$ref": "#/components/schemas/QuotaExceeded" }] } } } }
    },
    "schemas": {
      "Error": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" }, "code": { "type": "string" } } },
      "PlanRequired": { "type": "object", "properties": { "error": { "type": "string" }, "code": { "const": "plan_required" }, "feature": { "type": "string" }, "required_plan": { "type": "string" }, "required_plan_name": { "type": "string" } } },
      "QuotaExceeded": { "type": "object", "properties": { "error": { "type": "string" }, "code": { "const": "quota_exceeded" }, "key": { "type": "string", "examples": ["docs", "seats", "templates"] }, "limit": { "type": "integer" }, "used": { "type": "integer" }, "period": { "type": ["string", "null"] } } },
      "Ok": { "type": "object", "properties": { "ok": { "const": true } } },
      "DocumentStatus": { "type": "string", "enum": ["draft", "sent", "completed", "voided", "declined", "expired"] },
      "Document": { "type": "object", "properties": {
        "id": { "type": "string", "format": "uuid" }, "entity_id": { "type": "string", "format": "uuid" }, "owner_user_id": { "type": "string", "format": "uuid" },
        "title": { "type": "string" }, "original_filename": { "type": ["string", "null"] }, "status": { "$ref": "#/components/schemas/DocumentStatus" },
        "page_count": { "type": "integer" }, "hash_id": { "type": "string", "description": "SHA-256 PDF asli" }, "folder_id": { "type": ["string", "null"], "format": "uuid" },
        "signed_hash_id": { "type": ["string", "null"], "description": "SHA-256 PDF final setelah selesai" }, "can_edit": { "type": "boolean" }, "created_at": { "type": "string", "format": "date-time" }, "completed_at": { "type": ["string", "null"], "format": "date-time" } } },
      "Page": { "type": "object", "properties": { "page_number": { "type": "integer" }, "width": { "type": "number" }, "height": { "type": "number" } } },
      "SignerType": { "type": "string", "enum": ["signer", "approver", "viewer", "cc"], "description": "signer = Penanda tangan, approver = Penyetuju, viewer/cc = Peninjau (tembusan)" },
      "RoleInput": { "type": "object", "properties": {
        "name": { "type": "string", "maxLength": 254 }, "email": { "type": "string", "format": "email", "maxLength": 254 },
        "signer_type": { "$ref": "#/components/schemas/SignerType" }, "queue_position": { "type": "integer" },
        "routing_stage": { "type": "integer", "minimum": 1, "maximum": 50 }, "is_enabled": { "type": "boolean" },
        "require_verification": { "type": "boolean", "description": "Wajib OTP" }, "otp_channel": { "type": "string", "enum": ["email", "sms"] },
        "phone": { "type": "string", "description": "Untuk OTP SMS (E.164, mis. +6281234567890)" }, "require_id": { "type": "boolean", "description": "Wajib verifikasi identitas" },
        "access_code": { "type": ["string", "null"], "maxLength": 100, "description": "Kode akses; tidak pernah dikembalikan" },
        "receives_final_copy": { "type": "boolean" }, "badge_color": { "type": "string" } } },
      "Role": { "allOf": [{ "$ref": "#/components/schemas/RoleInput" }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "document_id": { "type": "string", "format": "uuid" }, "has_access_code": { "type": "boolean" } } }] },
      "FieldType": { "type": "string", "enum": ["signature", "initials", "date", "text", "checkbox", "dropdown", "radio", "stamp", "attachment", "number", "formula"] },
      "Condition": { "type": ["object", "null"], "description": "Tampilkan field hanya bila kondisi terpenuhi", "properties": { "field_id": { "type": "string", "description": "Id field pengendali (atau `ref` dalam batch)" }, "op": { "type": "string", "enum": ["checked", "unchecked", "equals", "not_equals", "not_empty"] }, "value": { "type": ["string", "number"] } }, "required": ["field_id", "op"] },
      "FieldInput": { "type": "object", "required": ["ff_type", "x", "y", "w", "h"], "properties": {
        "document_role_id": { "type": ["string", "null"], "format": "uuid" }, "page_number": { "type": "integer", "minimum": 1, "default": 1 },
        "ff_type": { "$ref": "#/components/schemas/FieldType" },
        "x": { "type": "number", "minimum": 0, "maximum": 1 }, "y": { "type": "number", "minimum": 0, "maximum": 1 }, "w": { "type": "number", "minimum": 0, "maximum": 1 }, "h": { "type": "number", "minimum": 0, "maximum": 1 },
        "is_required": { "type": "boolean", "default": true }, "font_size": { "type": "integer", "default": 12 },
        "validation": { "type": "string", "enum": ["none", "email", "number", "date"] }, "auto_fill": { "type": "string", "enum": ["none", "name", "email", "date"] },
        "locked": { "type": "boolean", "description": "Nilai diisi pengirim dan dikunci" }, "value": { "type": ["string", "null"], "maxLength": 2000 },
        "choices": { "type": ["array", "null"], "items": { "type": "string" }, "description": "Pilihan dropdown/radio; untuk lampiran: `[teks permintaan]`" },
        "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,31}$", "description": "Wajib untuk angka/rumus" },
        "formula": { "type": "string", "description": "Untuk ff_type=formula, mis. `harga * jumlah`" },
        "min": { "type": "number" }, "max": { "type": "number" }, "condition": { "$ref": "#/components/schemas/Condition" } } },
      "Field": { "allOf": [{ "$ref": "#/components/schemas/FieldInput" }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "document_id": { "type": "string", "format": "uuid" } } }] },
      "SendRequest": { "type": "object", "required": ["document_id", "document_roles"], "properties": {
        "document_id": { "type": "string", "format": "uuid" }, "title": { "type": "string" },
        "email_subject": { "type": "string" }, "email_body": { "type": "string" },
        "has_queue": { "type": "boolean", "description": "Urutan lama: tahap 1..n mengikuti queue_position" },
        "allow_delegation": { "type": "boolean" }, "reminders_enabled": { "type": "boolean" }, "reminder_every_hours": { "type": "integer", "minimum": 0 },
        "sender_receives_final_copy": { "type": "boolean", "default": true }, "expires_in_days": { "type": "number", "exclusiveMinimum": 0, "maximum": 3650 },
        "document_roles": { "type": "array", "minItems": 1, "maxItems": 50, "items": { "allOf": [{ "$ref": "#/components/schemas/RoleInput" }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Peran yang sudah ada (field tetap tertaut)" } } }] } } } },
      "SignRequest": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "document_id": { "type": "string", "format": "uuid" }, "title": { "type": "string" }, "status": { "type": "string", "enum": ["active", "finalizing", "completed", "declined", "voided", "expired"] }, "has_queue": { "type": "boolean" }, "allow_delegation": { "type": "boolean" }, "expires_at": { "type": ["string", "null"], "format": "date-time" }, "created_at": { "type": "string", "format": "date-time" } } },
      "SignerLink": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "email": { "type": "string" }, "order_position": { "type": "integer" }, "routing_stage": { "type": "integer" }, "signer_type": { "type": "string", "enum": ["signer", "approver"] }, "status": { "type": "string" }, "sign_url": { "type": "string", "format": "uri", "description": "Tautan rahasia penanda tangan — jangan dibagikan selain ke penerimanya" }, "has_access_code": { "type": "boolean" } } },
      "AuditSigner": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "email": { "type": "string" }, "name": { "type": ["string", "null"] }, "status": { "type": "string", "enum": ["pending", "viewed", "completed", "declined", "delegated"] }, "signer_type": { "type": "string" }, "routing_stage": { "type": "integer" }, "viewed_at": { "type": ["string", "null"], "format": "date-time" }, "completed_at": { "type": ["string", "null"], "format": "date-time" }, "declined_reason": { "type": ["string", "null"] }, "has_id_file": { "type": "boolean" } } },
      "Activity": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "event_type": { "type": "string", "examples": ["created", "sent", "viewed", "signed", "approved", "declined", "delegated", "voided", "completed"] }, "actor_email": { "type": ["string", "null"] }, "ip_address": { "type": ["string", "null"] }, "metadata": { "type": ["object", "null"] }, "created_at": { "type": "string", "format": "date-time" } } },
      "WebhookEvent": { "type": "string", "enum": ["sent", "completed", "declined", "voided", "*"] },
      "Webhook": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "url": { "type": "string", "format": "uri" }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEvent" } }, "is_active": { "type": "boolean" }, "created_at": { "type": "string", "format": "date-time" } } },
      "WebhookEnvelope": { "type": "object", "required": ["event", "at", "data"], "properties": { "event": { "type": "string" }, "at": { "type": "string", "format": "date-time" }, "data": { "type": "object" } } },
      "Organization": { "type": "object", "properties": {
        "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "external_id": { "type": ["string", "null"] }, "created_at": { "type": "string", "format": "date-time" },
        "plan_code": { "type": "string" },
        "owner": { "type": ["object", "null"], "properties": { "email": { "type": "string" }, "name": { "type": ["string", "null"] } } },
        "pending_owner_invite": { "type": ["object", "null"], "properties": { "email": { "type": "string" }, "expires_at": { "type": "string", "format": "date-time" } } },
        "docs": { "type": "integer", "description": "Dokumen terkirim pada periode" },
        "service_account": { "type": "string", "enum": ["active", "revoked", "none"], "description": "`revoked`: Pemilik organisasi mencabut akses mitra" } } },
      "OwnerInvite": { "type": "object", "properties": { "email": { "type": "string" }, "expires_at": { "type": "string", "format": "date-time" }, "email_sent": { "type": "boolean" } } },
      "Usage": { "type": "object", "properties": { "period": { "type": "string" }, "doc_price": { "type": "integer" }, "total_docs": { "type": "integer" }, "estimated_amount": { "type": "integer", "description": "Rupiah, sebelum pajak" }, "organizations": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": ["string", "null"] }, "docs": { "type": "integer" }, "amount": { "type": "integer" } } } } } }
    }
  }
}
