{
  "openapi": "3.0.3",
  "info": {
    "title": "Kawenter Partner API",
    "version": "1.1.0",
    "description": "Kawenter eVisa / travel Partner API for approved travel agents (URL major /v1, release 1.1.0). Catalog, quote, create, pay-wallet, auto top-up, reports, files, webhooks. Authenticate with ka_live_… keys (scopes: evisa, wallet, files, webhooks). Versioning & deprecation: https://kawenter.com/docs/partner#versioning — Changelog: https://kawenter.com/docs/partner-changelog.md — GET /version, GET /changelog. Human docs: https://kawenter.com/docs/partner — Agent JSON: https://kawenter.com/docs/partner?mode=agent — Public read-only catalog (no auth): /api/evisa.php",
    "x-kawenter-api-version": "v1",
    "x-kawenter-release": "1.1.0",
    "x-kawenter-changelog": "https://kawenter.com/docs/partner-changelog.md",
    "x-kawenter-deprecation-policy": "https://kawenter.com/docs/partner#versioning"
  },
  "servers": [
    {
      "url": "https://kawenter.com/api/partner/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    },
    {
      "BearerApiKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Partner key: ka_live_…"
      },
      "BearerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ka_live_",
        "description": "Same key as Authorization: Bearer ka_live_…"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Structured Partner API error. Always includes error_code + message; details explains limits, missing fields, wallet shortfalls, etc. Echo request_id to support.",
        "required": ["ok", "error_code", "message", "status"],
        "properties": {
          "ok": { "type": "boolean", "example": false },
          "error_code": { "type": "string", "example": "MISSING_NATIONALITY" },
          "message": { "type": "string", "example": "nationality is required." },
          "status": { "type": "integer", "example": 400 },
          "retry_after": { "type": "integer", "nullable": true, "description": "Seconds to wait before retry (also sent as Retry-After header when set)" },
          "request_id": { "type": "string", "example": "prt_a1b2c3d4e5f60718", "description": "Also returned as X-Request-Id header" },
          "method": { "type": "string", "example": "GET" },
          "path": { "type": "string", "example": "catalog/evisas" },
          "docs_url": { "type": "string", "example": "https://kawenter.com/docs/partner#errors" },
          "details": {
            "type": "object",
            "additionalProperties": true,
            "description": "Machine-readable context: missing fields, balances, scopes, file limits, hints, action codes, etc.",
            "properties": {
              "hint": { "type": "string" },
              "action": { "type": "string", "description": "Suggested recovery action code (e.g. retry_after, topup_wallet, add_nationality)" },
              "missing": { "type": "array", "items": { "type": "string" } },
              "balance_cents": { "type": "integer" },
              "required_cents": { "type": "integer" },
              "shortfall_cents": { "type": "integer" },
              "required_scope": { "type": "string" },
              "scopes": { "type": "array", "items": { "type": "string" } },
              "limit": { "type": "integer" },
              "limit_scope": { "type": "string", "enum": ["key", "agency", "client"] },
              "window_name": { "type": "string", "enum": ["minute", "hour", "day"] }
            }
          }
        }
      },
      "RateLimitExceeded": {
        "type": "object",
        "description": "Returned when a per-key or agency minute/daily quota is exceeded. See also X-RateLimit-* response headers.",
        "allOf": [
          { "$ref": "#/components/schemas/Error" },
          {
            "type": "object",
            "properties": {
              "error_code": { "type": "string", "example": "RATE_LIMIT_EXCEEDED" },
              "status": { "type": "integer", "example": 429 },
              "retry_after": { "type": "integer", "description": "Seconds until the active window resets" },
              "limit": { "type": "integer", "description": "Quota for the exceeded window" },
              "window": { "type": "integer", "description": "Window length in seconds (60 or 86400)" },
              "remaining": { "type": "integer", "example": 0 },
              "reset": { "type": "integer", "description": "Unix timestamp when the window resets" },
              "limit_scope": { "type": "string", "enum": ["key", "agency"], "example": "key" },
              "window_name": { "type": "string", "enum": ["minute", "day"], "example": "minute" }
            }
          }
        ]
      },
      "PartnerIdentity": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "api": { "type": "string", "example": "partner" },
          "version": { "type": "string", "example": "v1", "description": "URL major (also API-Version response header)" },
          "release": { "type": "string", "example": "1.1.0", "description": "Semver release (also X-Kawenter-API-Release header)" },
          "agency_uid": { "type": "string" },
          "discount_percent": { "type": "number" },
          "scopes": {
            "type": "array",
            "items": { "type": "string", "enum": ["evisa", "wallet", "files", "webhooks"] }
          },
          "rate_limit_per_minute": { "type": "integer", "example": 30, "description": "Configured per-key minute quota" },
          "rate_limit_per_day": { "type": "integer", "example": 1000, "description": "Configured per-key daily quota" },
          "docs_url": { "type": "string", "example": "https://kawenter.com/docs/partner" },
          "openapi_url": { "type": "string", "example": "https://kawenter.com/.well-known/partner-openapi.json" },
          "changelog_url": { "type": "string", "example": "https://kawenter.com/docs/partner-changelog.md" },
          "versioning_url": { "type": "string", "example": "https://kawenter.com/docs/partner#versioning" }
        }
      },
      "PartnerOrder": {
        "type": "object",
        "properties": {
          "order_id": { "type": "string" },
          "number": { "type": "string" },
          "status": {
            "type": "string",
            "description": "WooCommerce status. Incomplete docs use on-hold or not_completed_doc (same as check-status)."
          },
          "status_label": {
            "type": "string",
            "description": "Present when needs_documents: \"Documents uncompleted\""
          },
          "currency": { "type": "string" },
          "total": { "type": "string" },
          "needs_payment": { "type": "boolean" },
          "needs_documents": {
            "type": "boolean",
            "description": "True when status is on-hold or not_completed_doc"
          },
          "has_document": { "type": "boolean" },
          "documents_url": { "type": "string", "description": "Path to authenticated document download when has_document is true" },
          "extra_docs": { "type": "string", "description": "Staff note / what is missing when needs_documents is true" },
          "submit_documents_url": {
            "type": "string",
            "description": "Path for POST /orders/{id}/documents when needs_documents is true"
          },
          "cancelable": {
            "type": "boolean",
            "description": "True when status is processing, wallet-paid, and within 1 hour of payment"
          },
          "cancel_until": {
            "type": "string",
            "format": "date-time",
            "description": "UTC deadline for POST /orders/{id}/cancel when cancelable"
          },
          "cancel_seconds_remaining": { "type": "integer" },
          "paid_at": { "type": "string", "format": "date-time" },
          "tracking": {
            "type": "string",
            "description": "Visa / order tracking text when set (same as check-status _wc_acof_4 / tracking_info)"
          },
          "check_status_url": {
            "type": "string",
            "format": "uri",
            "description": "Public Kawenter check-status page for this order"
          }
        }
      },
      "EvisaDetail": {
        "type": "object",
        "description": "Catalog eVisa details returned on quote and GET /catalog/evisas/{id}",
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "image": { "type": "string" },
          "destination": { "type": "string" },
          "visa_type": { "type": "string" },
          "validity": { "type": "string" },
          "validity_days": { "type": "integer", "nullable": true },
          "duration_of_stay": { "type": "string" },
          "duration_of_stay_days": { "type": "integer", "nullable": true },
          "number_of_entry": { "type": "string" },
          "processing_time": { "type": "string", "example": "5-7" },
          "processing_time_label": { "type": "string", "example": "5-7 working days" },
          "requirements": { "type": "array", "items": { "type": "string" } },
          "important_notes": { "type": "array", "items": { "type": "string" } },
          "notes": { "type": "array", "items": { "type": "string" } },
          "instruction": { "type": "string" },
          "needs_extra_documents": { "type": "boolean" },
          "product_type": { "type": "string", "example": "evisa" },
          "required_fields": {
            "type": "object",
            "description": "Which applicant/contact fields the create-order endpoint enforces for this catalog item",
            "properties": {
              "full_name": { "type": "boolean" },
              "passport": { "type": "boolean" },
              "photo": { "type": "boolean" },
              "job": { "type": "boolean" },
              "mother_name": { "type": "boolean" },
              "entry_date": { "type": "boolean" },
              "return_date": { "type": "boolean" },
              "email": { "type": "boolean" },
              "phone": { "type": "boolean" },
              "addon": { "type": "boolean" }
            }
          },
          "addons": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string", "example": "addon_0" },
                "title": { "type": "string" },
                "description": { "type": "string" },
                "price": { "type": "number", "description": "Per-applicant catalog price" },
                "currency": { "type": "string" }
              }
            }
          },
          "insurance": {
            "type": "object",
            "properties": {
              "available": { "type": "boolean" },
              "fee": { "type": "number", "description": "Per-order catalog fee (not × applicants)" },
              "unit_fee": { "type": "number" },
              "refund_amount": { "type": "number" },
              "currency": { "type": "string" }
            }
          }
        }
      },
      "Quote": {
        "type": "object",
        "properties": {
          "evisa_id": { "type": "string" },
          "title": { "type": "string" },
          "nationality": { "type": "string" },
          "destination": { "type": "string" },
          "applicant_count": { "type": "integer" },
          "government_fee": { "type": "number" },
          "processing_fee_base": { "type": "number" },
          "processing_fee": { "type": "number" },
          "agency_discount_percent": { "type": "number" },
          "agency_discount_amount": { "type": "number" },
          "retail_markup_percent": { "type": "number", "description": "Agent white-label markup on wholesale processing (suggested sell price)" },
          "processing_fee_retail": { "type": "number", "description": "Wholesale processing × (1 + retail_markup_percent/100)" },
          "unit_processing_fee_retail": { "type": "number" },
          "wholesale_total": { "type": "number", "description": "Same as total — Partner create/pay amount" },
          "retail_total": { "type": "number", "description": "Suggested traveler price when markup is configured" },
          "agency_margin": { "type": "number", "description": "retail_total − wholesale_total" },
          "unit_addon_fee": { "type": "number", "description": "Per-applicant add-on total from catalog" },
          "addon_fee": { "type": "number", "description": "unit_addon_fee × applicant_count (server-calculated)" },
          "selected_addons": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "title": { "type": "string" },
                "unit_price": { "type": "number" },
                "price": { "type": "number" },
                "currency": { "type": "string" }
              }
            }
          },
          "insurance_included": { "type": "boolean" },
          "unit_insurance_fee": { "type": "number", "description": "Catalog insurance fee (once per order)" },
          "insurance_fee": { "type": "number", "description": "Same as unit_insurance_fee when included (not × applicants)" },
          "insurance_refund_amount": { "type": "number" },
          "visa_type": { "type": "string" },
          "validity": { "type": "string" },
          "duration_of_stay": { "type": "string" },
          "number_of_entry": { "type": "string" },
          "processing_time": { "type": "string" },
          "processing_time_label": { "type": "string" },
          "requirements": { "type": "array", "items": { "type": "string" } },
          "important_notes": { "type": "array", "items": { "type": "string" } },
          "evisa": { "$ref": "#/components/schemas/EvisaDetail" },
          "total": { "type": "number" },
          "currency": { "type": "string", "example": "USD" }
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": { "type": "string", "maxLength": 128 },
        "description": "Required on create; recommended on pay-wallet. Retries with the same key return the original result."
      }
    }
  },
  "paths": {
    "/": {
      "get": {
        "summary": "Partner API health / identity",
        "description": "Returns agency context, API version/release, and this key's rate_limit_per_minute / rate_limit_per_day. Default quotas are 30/min and 1,000/day per API key; see docs at /docs/partner#rate-limits. Versioning: /docs/partner#versioning.",
        "responses": {
          "200": {
            "description": "Authenticated agency context",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PartnerIdentity" }
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RateLimitExceeded" }
              }
            }
          }
        }
      }
    },
    "/version": {
      "get": {
        "summary": "API version + deprecation policy",
        "description": "Machine-readable versioning and deprecation policy (min 90-day notice for removals). Any valid API key.",
        "responses": {
          "200": { "description": "version, release, deprecation policy, changelog links" }
        }
      }
    },
    "/changelog": {
      "get": {
        "summary": "API changelog (JSON)",
        "description": "Release notes newest-first. Markdown mirror: /docs/partner-changelog.md.",
        "responses": {
          "200": { "description": "entries[] with version, date, summary, changes" }
        }
      }
    },
    "/wallet": {
      "get": {
        "summary": "Agency wallet balance (USD)",
        "description": "Requires scope wallet.",
        "responses": {
          "200": {
            "description": "balance_cents and balance"
          }
        }
      }
    },
    "/wallet/transactions": {
      "get": {
        "summary": "Wallet transaction ledger",
        "description": "Debits, top-ups, reversals, refunds. Requires scope wallet.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 25, "maximum": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": { "type": "string" },
            "description": "created_at cursor from previous next_cursor"
          }
        ],
        "responses": {
          "200": { "description": "items[], has_more, next_cursor" }
        }
      }
    },
    "/wallet/topup-packages": {
      "get": {
        "summary": "Allowed wallet top-up amounts",
        "description": "Min/max and quick presets ($1–$2,000). Scope wallet.",
        "responses": {
          "200": { "description": "packages, quick_cents, min_cents, max_cents" }
        }
      }
    },
    "/wallet/topups": {
      "post": {
        "summary": "Start a card top-up (Stripe Checkout → agency wallet)",
        "description": "Orders are settled only via pay-wallet. Partners fund the wallet with a corporate card here. Open checkout_url; after payment succeeds, GET /wallet shows the new balance. Scope wallet. Prefer Idempotency-Key.",
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount_cents": { "type": "integer", "minimum": 100, "maximum": 200000, "example": 10000 },
                  "amount": { "type": "number", "description": "USD dollars alternative to amount_cents" },
                  "success_url": { "type": "string", "format": "uri", "description": "Optional HTTPS return URL (may include {CHECKOUT_SESSION_ID})" },
                  "cancel_url": { "type": "string", "format": "uri" }
                }
              },
              "example": { "amount_cents": 10000 }
            }
          }
        },
        "responses": {
          "201": {
            "description": "checkout_url + order_id — open URL to pay by card (3D Secure via Stripe)"
          },
          "400": { "description": "INVALID_AMOUNT / AGENCY_EMAIL_REQUIRED", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/wallet/auto-topup": {
      "get": {
        "summary": "Get auto top-up settings",
        "description": "Linked card status, threshold_percent (default 20), target_balance_cents, and ready flag. When balance falls below threshold_percent of target after pay-wallet, Stripe charges the saved card off-session and credits the wallet. Scope wallet.",
        "responses": {
          "200": { "description": "settings object (card last4, threshold, target, enabled, ready)" }
        }
      },
      "put": {
        "summary": "Update auto top-up settings",
        "description": "Enable/disable and set target_balance_cents, threshold_percent (1–90, default 20), optional amount_cents (0 = refill up to target). Card must be linked first. Scope wallet.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": { "type": "boolean" },
                  "target_balance_cents": { "type": "integer", "minimum": 100, "example": 50000 },
                  "threshold_percent": { "type": "integer", "minimum": 1, "maximum": 90, "default": 20 },
                  "amount_cents": { "type": "integer", "description": "Fixed charge amount; 0 or omit = refill to target" }
                }
              },
              "example": { "enabled": true, "target_balance_cents": 50000, "threshold_percent": 20 }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated settings" },
          "400": { "description": "CARD_REQUIRED / TARGET_REQUIRED / INVALID_AMOUNT" }
        }
      },
      "delete": {
        "summary": "Unlink card and disable auto top-up",
        "description": "Detaches the Stripe payment method and sets enabled=false. Scope wallet.",
        "responses": {
          "200": { "description": "settings with card_linked=false" }
        }
      }
    },
    "/wallet/auto-topup/setup": {
      "post": {
        "summary": "Link a card for auto top-up (Stripe Checkout setup mode)",
        "description": "No charge — saves a card for future off-session top-ups. Open checkout_url, then POST /wallet/auto-topup/confirm with session_id. Scope wallet.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "success_url": { "type": "string", "format": "uri" },
                  "cancel_url": { "type": "string", "format": "uri" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "checkout_url + session_id" }
        }
      }
    },
    "/wallet/auto-topup/confirm": {
      "post": {
        "summary": "Confirm linked card after Checkout setup",
        "description": "Pass session_id from Stripe Checkout success. Then PUT /wallet/auto-topup to enable. Scope wallet.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["session_id"],
                "properties": {
                  "session_id": { "type": "string", "example": "cs_…" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Card linked; settings.card_linked=true" },
          "400": { "description": "SETUP_INCOMPLETE / NO_PAYMENT_METHOD" }
        }
      }
    },
    "/wallet/auto-topup/card": {
      "delete": {
        "summary": "Unlink saved auto top-up card",
        "description": "Same effect as DELETE /wallet/auto-topup. Scope wallet.",
        "responses": {
          "200": { "description": "Card unlinked; auto top-up disabled" }
        }
      }
    },
    "/files": {
      "get": {
        "summary": "List uploaded file metadata",
        "description": "Metadata only (no public URLs). Scope files. Optional include_deleted=1.",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 50 } },
          { "name": "include_deleted", "in": "query", "schema": { "type": "string", "enum": ["1", "true"] } }
        ],
        "responses": {
          "200": { "description": "files[] with file_id, content_type, size, sha256, original_name, created_at, deleted" }
        }
      },
      "post": {
        "summary": "Upload passport or photo",
        "description": "multipart/form-data field file; optional purpose=passport|photo|extra. Returns file_id + metadata (no public URL). Requires scope files.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": { "type": "string", "format": "binary" },
                  "purpose": { "type": "string", "enum": ["passport", "photo", "extra", "document", "missing"] }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "file_id, content_type, size, original_name, sha256, created_at" },
          "413": { "description": "File too large" }
        }
      }
    },
    "/files/{id}": {
      "get": {
        "summary": "Get file metadata",
        "description": "Metadata only — never returns a public URL. Scope files.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "file object" },
          "404": { "description": "FILE_NOT_FOUND" }
        }
      },
      "delete": {
        "summary": "Securely delete a file",
        "description": "Removes remote storage when possible, clears the stored URL, and marks the registry entry deleted. Scope files.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "file deleted; remote_deleted flag" },
          "404": { "description": "FILE_NOT_FOUND" }
        }
      }
    },
    "/reports/monthly": {
      "get": {
        "summary": "Monthly wallet + order statement",
        "description": "Aggregates wallet credits/debits by reason and indexed orders for a calendar month. Scope wallet.",
        "parameters": [
          { "name": "year", "in": "query", "schema": { "type": "integer", "example": 2026 } },
          { "name": "month", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 12, "example": 10 } }
        ],
        "responses": {
          "200": { "description": "period, wallet totals, orders by status, transactions[]" }
        }
      }
    },
    "/reports/financial": {
      "get": {
        "summary": "Financial report for a date range",
        "description": "Same shape as monthly statement. Pass year+month or from+to (YYYY-MM-DD). Scope wallet. Alias: /reports/summary.",
        "parameters": [
          { "name": "year", "in": "query", "schema": { "type": "integer" } },
          { "name": "month", "in": "query", "schema": { "type": "integer" } },
          { "name": "from", "in": "query", "schema": { "type": "string", "example": "2026-10-01" } },
          { "name": "to", "in": "query", "schema": { "type": "string", "example": "2026-10-31" } }
        ],
        "responses": {
          "200": { "description": "period, wallet, orders, transactions" }
        }
      }
    },
    "/webhooks": {
      "get": {
        "summary": "List webhook endpoints",
        "description": "Requires scope webhooks.",
        "responses": {
          "200": { "description": "webhooks[] (no signing secrets)" }
        }
      },
      "post": {
        "summary": "Register webhook endpoint",
        "description": "Returns signing_secret once. Requires scope webhooks.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": { "type": "string", "format": "uri", "description": "https callback URL" },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "order.processing",
                        "order.completed",
                        "order.documents_required",
                        "order.rejected"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "webhook_id + signing_secret" }
        }
      }
    },
    "/webhooks/{id}": {
      "delete": {
        "summary": "Deactivate a webhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Deactivated" },
          "404": { "description": "Not found" }
        }
      }
    },
    "/catalog/evisas": {
      "get": {
        "summary": "List eVisas for a nationality (agent-priced unit fees)",
        "parameters": [
          {
            "name": "nationality",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "example": "IQ" }
          },
          {
            "name": "destination",
            "in": "query",
            "schema": { "type": "string", "example": "TR" }
          },
          {
            "name": "lang",
            "in": "query",
            "schema": { "type": "string", "enum": ["en", "ar"] }
          }
        ],
        "responses": {
          "200": { "description": "evisas array" }
        }
      }
    },
    "/catalog/evisas/{id}": {
      "get": {
        "summary": "eVisa detail",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "nationality",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "eVisa detail with agent unit fees" },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/quotes/evisa": {
      "post": {
        "summary": "Agent-priced eVisa quote (server recomputes fees)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["evisa_id", "nationality"],
                "properties": {
                  "evisa_id": { "type": "string" },
                  "nationality": { "type": "string" },
                  "destination": { "type": "string" },
                  "applicant_count": { "type": "integer", "minimum": 1, "maximum": 9, "default": 1 },
                  "addon_id": {
                    "type": "string",
                    "description": "Catalog add-on id (e.g. addon_0). Price is looked up server-side; do not send addon_fee."
                  },
                  "addons": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Optional list of catalog add-on ids"
                  },
                  "include_insurance": {
                    "type": "boolean",
                    "description": "Opt in to catalog travel insurance. Fee is looked up server-side once per order; do not send insurance_fee."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Quote",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "quote": { "$ref": "#/components/schemas/Quote" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/orders/evisa": {
      "post": {
        "summary": "Create unpaid eVisa order (fees from catalog + agency %; never trust client totals)",
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["evisa_id", "nationality", "contact", "applicants"],
                "properties": {
                  "evisa_id": { "type": "string" },
                  "nationality": { "type": "string" },
                  "destination": { "type": "string" },
                  "applicant_count": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 9,
                    "description": "Must match applicants[] length when both are sent (max 9). Fees are × this count (same as website multi-traveler checkout)."
                  },
                  "addon_id": {
                    "type": "string",
                    "description": "Required when catalog lists add-ons. Server prices from catalog × applicants."
                  },
                  "addons": {
                    "type": "array",
                    "items": { "type": "string" }
                  },
                  "include_insurance": {
                    "type": "boolean",
                    "description": "Opt in to catalog insurance (once per order). Server prices only."
                  },
                  "contact": {
                    "type": "object",
                    "required": ["email", "phone"],
                    "properties": {
                      "email": { "type": "string", "format": "email" },
                      "phone": { "type": "string", "description": "E.164 with country code" },
                      "name": { "type": "string" }
                    }
                  },
                  "applicants": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 9,
                    "description": "One object per traveler. WooCommerce stores passport_url, photo_url, work, and mother as newline-joined values (same as the website).",
                    "items": {
                      "type": "object",
                      "required": ["full_name"],
                      "properties": {
                        "full_name": { "type": "string" },
                        "passport_file_id": { "type": "string", "description": "From POST /files (preferred)" },
                        "photo_file_id": { "type": "string", "description": "Required (same as website)" },
                        "passport_url": { "type": "string", "format": "uri", "description": "Legacy https URL" },
                        "photo_url": { "type": "string", "format": "uri" },
                        "job": { "type": "string", "description": "Required unless quote.required_fields.job is false" },
                        "mother_name": { "type": "string", "description": "Required unless quote.required_fields.mother_name is false" }
                      }
                    }
                  },
                  "entry_date": { "type": "string", "format": "date", "description": "YYYY-MM-DD; required unless catalog hides travel dates" },
                  "return_date": { "type": "string", "format": "date" },
                  "client_reference": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created unpaid order + quote breakdown" },
          "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/orders/{id}/pay-wallet": {
      "post": {
        "summary": "Pay an unpaid agency order from the agency wallet",
        "description": "Only settle path for Partner eVisa orders. Top up first via POST /wallet/topups if balance is low.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "responses": {
          "200": { "description": "Paid (or already paid / duplicate idempotent)" },
          "402": { "description": "Insufficient wallet — use POST /wallet/topups", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Not this agency's order" }
        }
      }
    },
    "/orders/{id}/cancel": {
      "post": {
        "summary": "Early cancel + full refund to agency wallet",
        "description": "Allowed only while status is processing (not sent_to_embassy) and within 1 hour of wallet payment. Credits the full wholesale total back to the wallet. Scopes: evisa + wallet.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "responses": {
          "200": { "description": "Cancelled; refunded_cents credited to wallet" },
          "409": {
            "description": "CANCEL_NOT_ALLOWED or CANCEL_WINDOW_EXPIRED",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/orders/{id}": {
      "get": {
        "summary": "Get order status (agency-owned only)",
        "description": "Includes has_document and documents_url when a visa PDF is ready. Prefer webhooks over polling.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "order": { "$ref": "#/components/schemas/PartnerOrder" }
                  }
                }
              }
            }
          },
          "403": { "description": "Forbidden" },
          "404": { "description": "Not found" }
        }
      }
    },
    "/orders/{id}/documents": {
      "get": {
        "summary": "Download completed visa document",
        "description": "Authenticated binary download (PDF/image). Requires scope evisa.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Document bytes",
            "content": {
              "application/pdf": { "schema": { "type": "string", "format": "binary" } },
              "image/jpeg": { "schema": { "type": "string", "format": "binary" } },
              "image/png": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "404": {
            "description": "DOCUMENT_NOT_READY — when needs_documents, upload via POST /files then POST this path with file_ids",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "post": {
        "summary": "Submit missing / replacement documents for an order",
        "description": "Same flow as check-status missing docs: upload with POST /files, then submit file_ids here. Allowed when status is on-hold, not_completed_doc, or processing. Moves on-hold / not_completed_doc to processing and emits order.processing. Requires scope evisa. Prefer Idempotency-Key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "file_ids": {
                    "type": "array",
                    "items": { "type": "string", "example": "pf_…" },
                    "description": "Preferred — ids from POST /files"
                  },
                  "file_id": { "type": "string", "description": "Single file_id shortcut" },
                  "files": {
                    "type": "array",
                    "items": {
                      "oneOf": [
                        { "type": "string" },
                        {
                          "type": "object",
                          "properties": {
                            "file_id": { "type": "string" },
                            "url": { "type": "string", "format": "uri" },
                            "file_url": { "type": "string", "format": "uri" }
                          }
                        }
                      ]
                    }
                  },
                  "urls": {
                    "type": "array",
                    "items": { "type": "string", "format": "uri" },
                    "description": "Fallback https URLs on allowed hosts; prefer file_ids"
                  }
                }
              },
              "example": {
                "file_ids": ["pf_0123456789abcdef0123456789abcdef"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Documents saved; order may move to processing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "order_id": { "type": "string" },
                    "order": { "$ref": "#/components/schemas/PartnerOrder" },
                    "documents_saved": { "type": "integer" },
                    "status_moved": { "type": "boolean" },
                    "previous_status": { "type": "string" },
                    "message": { "type": "string" },
                    "idempotency_key": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "FILES_REQUIRED / invalid file_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "ORDER_NOT_AWAITING_DOCUMENTS", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/orders": {
      "get": {
        "summary": "List / search agency partner orders",
        "description": "Default is a fast Firestore index (no WooCommerce N+1). Filter by status, date_from/date_to (YYYY-MM-DD), and client_reference. Pass expand=1 for parallel live WC order bodies.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 20, "maximum": 50 }
          },
          {
            "name": "status",
            "in": "query",
            "schema": { "type": "string", "example": "processing" },
            "description": "Exact WooCommerce/partner status match (e.g. pending, processing, completed, cancelled)."
          },
          {
            "name": "date_from",
            "in": "query",
            "schema": { "type": "string", "example": "2026-10-01" },
            "description": "Inclusive created_at lower bound (YYYY-MM-DD or ISO8601). Alias: from."
          },
          {
            "name": "date_to",
            "in": "query",
            "schema": { "type": "string", "example": "2026-10-31" },
            "description": "Inclusive created_at upper bound. Alias: to."
          },
          {
            "name": "client_reference",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Exact match on client_reference stored at create-order. Alias: client_ref."
          },
          {
            "name": "expand",
            "in": "query",
            "schema": { "type": "string", "enum": ["1", "true", "full", "wc"] },
            "description": "When set, return full PartnerOrder objects from WooCommerce (slower)."
          }
        ],
        "responses": {
          "200": { "description": "orders array (indexed or expanded) + filters echoed when set" }
        }
      }
    }
  }
}
