{
  "openapi": "3.1.0",
  "info": {
    "title": "TM7 Ready Public API",
    "version": "1.0.0",
    "description": "Public HTTP APIs for TM7 Ready (https://tm7ready.com), a service that prepares print-ready Thai immigration TM.7 tourist extension forms.\n\nAuthentication: Customer flows use per-order HttpOnly cookies set after POST /api/orders or via /api/order-access bootstrap links. There is no OAuth for these endpoints.\n\nRate limits apply per client IP. In production, POST /api/orders and POST /api/checkout require a same-origin Origin or Referer header matching NEXT_PUBLIC_APP_URL.\n\nAdmin, cron, and webhook routes are not documented here."
  },
  "servers": [
    {
      "url": "https://tm7ready.com",
      "description": "Production"
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Service liveness",
        "description": "Returns `{ \"status\": \"ok\" }` or `{ \"status\": \"degraded\" }`. A detailed check including database, Redis, and worker status is available only with `Authorization: Bearer $HEALTH_CHECK_SECRET`.",
        "responses": {
          "200": {
            "description": "Health status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status"],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": ["ok", "degraded"]
                    },
                    "checks": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string",
                        "enum": ["ok", "error"]
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited"
          }
        }
      }
    },
    "/api/offices": {
      "get": {
        "operationId": "listOffices",
        "summary": "Rank immigration offices",
        "description": "Returns up to five immigration offices ranked by province name and optional coordinates.",
        "parameters": [
          {
            "name": "province",
            "in": "query",
            "schema": { "type": "string", "default": "Bangkok", "maxLength": 120 }
          },
          {
            "name": "lat",
            "in": "query",
            "schema": { "type": "number", "minimum": -90, "maximum": 90 }
          },
          {
            "name": "lng",
            "in": "query",
            "schema": { "type": "number", "minimum": -180, "maximum": 180 }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked offices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["offices"],
                  "properties": {
                    "offices": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "name": { "type": "string" },
                          "province": { "type": "string" },
                          "address": { "type": "string" },
                          "lat": { "type": "number" },
                          "lng": { "type": "number" },
                          "distanceKm": { "type": "number" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid query parameters" },
          "429": { "description": "Rate limited" },
          "503": { "description": "Service temporarily unavailable" }
        }
      }
    },
    "/api/orders": {
      "post": {
        "operationId": "createOrder",
        "summary": "Create a draft order from wizard answers",
        "description": "Creates a draft order and sets an HttpOnly order-access cookie. Use the returned orderId with POST /api/checkout after the cookie is present.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["answers"],
                "properties": {
                  "locale": {
                    "type": "string",
                    "description": "BCP 47 locale code supported by the wizard"
                  },
                  "answers": {
                    "type": "object",
                    "description": "TM.7 wizard answers (see @tm7/validation tm7AnswersSchema)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draft order created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["orderId"],
                  "properties": {
                    "orderId": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request body" },
          "403": { "description": "Origin check failed in production" },
          "429": { "description": "Rate limited" },
          "503": { "description": "Service temporarily unavailable" }
        }
      }
    },
    "/api/checkout": {
      "post": {
        "operationId": "createCheckoutSession",
        "summary": "Create a Stripe Checkout session",
        "description": "Requires an HttpOnly order-access cookie from POST /api/orders. Returns a Stripe Checkout URL for payment.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["orderId"],
                "properties": {
                  "orderId": { "type": "string" },
                  "locale": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout session created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["url"],
                  "properties": {
                    "url": { "type": "string", "format": "uri" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request" },
          "403": { "description": "Forbidden or origin check failed" },
          "404": { "description": "Order not found" },
          "409": { "description": "Order not payable" },
          "429": { "description": "Rate limited" },
          "503": { "description": "Purchases disabled or Stripe unavailable" }
        }
      }
    },
    "/api/orders/{orderId}/status": {
      "get": {
        "operationId": "getOrderStatus",
        "summary": "Poll order fulfillment status",
        "description": "Requires the HttpOnly order-access cookie. Used by the success page to poll without full SSR refresh. Stripe Checkout success URLs also pass through GET /api/order-access with a one-time bootstrap token to restore the cookie after payment.",
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Order status snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status", "pdfReady", "emailSent"],
                  "properties": {
                    "status": { "type": "string" },
                    "pdfReady": { "type": "boolean" },
                    "emailSent": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "403": { "description": "Missing or invalid order cookie" },
          "429": { "description": "Rate limited" }
        }
      }
    }
  }
}
