{
  "openapi": "3.1.0",
  "info": {
    "title": "RightOS Right API",
    "version": "0.9.0",
    "summary": "Privacy-preserving rights verification API — digital QR tickets for queues, reservations, EV charging, and package pickup.",
    "description": "RightOS is a verification infrastructure for places, time slots, queue positions, and usage rights.\n\nCore concept: a **Right Token** is a digital right (e.g. a queue position) identified by a `tokenId` and proven by a `verificationCode` that is handed to the holder exactly once at issuance. The server stores only the SHA-256 hash of the code (`secretHash`), never the code itself. Verifiers confirm that *a valid right is present*, not *who the person is* — no names, phone numbers, or birthdates are ever required from end users.\n\nTransfers use re-keying: a new code is issued and the old one is invalidated immediately, so authority is always proven by holding the current code.\n\n**What RightOS is NOT**: RightOS is not a taxi or ride-hailing service. It does not arrange vehicles, set fares, assign drivers, or broker dispatch.\n\n日本語: RightOSは場所・時間・順番・利用権のプライバシー保護型検証基盤です。タクシー営業・配車サービスではありません。",
    "termsOfService": "https://rightos.i-s3.com/software/rightos/legal/terms",
    "contact": {
      "name": "I-S3 Software Division",
      "url": "https://rightos.i-s3.com/software/rightos"
    }
  },
  "servers": [
    {
      "url": "https://rightos.i-s3.com",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "tokens",
      "description": "Issue, verify, use, cancel, and transfer Right Tokens"
    },
    {
      "name": "locations",
      "description": "Locations (shops, EV chargers, pickup points) belonging to an organization"
    },
    {
      "name": "organizations",
      "description": "Organization registration and account operations"
    },
    {
      "name": "plans",
      "description": "Pricing plans (globally uniform pricing)"
    },
    {
      "name": "billing",
      "description": "Subscription billing via Stripe hosted Checkout / Billing Portal (card data never touches RightOS servers)"
    },
    {
      "name": "console",
      "description": "Dashboard data for the Right Console"
    },
    {
      "name": "webhooks",
      "description": "Outbound webhooks: signed event notifications to your system"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key issued exactly once at organization registration (format: rk_live_...). The server stores only its SHA-256 hash. Alternative header: x-rightos-key."
      },
      "headerApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-rightos-key",
        "description": "Same key as bearerApiKey, sent via a dedicated header."
      }
    },
    "schemas": {
      "PlanId": {
        "type": "string",
        "enum": [
          "free",
          "starter",
          "business",
          "pro",
          "enterprise"
        ]
      },
      "Plan": {
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/PlanId"
          },
          "name": {
            "type": "string"
          },
          "nameEn": {
            "type": "string"
          },
          "monthlyPrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "Monthly price in JPY. null = custom quote. Pricing is globally uniform."
          },
          "tokenLimit": {
            "type": [
              "number",
              "null"
            ],
            "description": "Monthly Right Token issuance limit. null = unlimited/custom. free=50, starter=1000, business=10000, pro=100000."
          },
          "locationLimit": {
            "type": [
              "number",
              "null"
            ],
            "description": "Location limit. free=1, starter=3, business=20, pro/enterprise=unlimited."
          },
          "apiEnabled": {
            "type": "boolean"
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "featuresEn": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Organization": {
        "type": "object",
        "description": "Public organization record. The API key hash is never exposed.",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "org_2f8a1c9d3e4b"
            ]
          },
          "name": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "description": "ISO country code, default JP"
          },
          "contactEmail": {
            "type": "string",
            "format": "email"
          },
          "planId": {
            "$ref": "#/components/schemas/PlanId"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Subscription": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "organizationId": {
            "type": "string"
          },
          "planId": {
            "$ref": "#/components/schemas/PlanId"
          },
          "status": {
            "type": "string",
            "enum": [
              "trialing",
              "active",
              "past_due",
              "cancelled"
            ]
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "currentPeriodEnd": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LocationType": {
        "type": "string",
        "enum": [
          "shop",
          "clinic",
          "ev_charging",
          "event",
          "logistics",
          "pickup",
          "other"
        ]
      },
      "RightPolicy": {
        "type": "object",
        "description": "Effective policy for a location (ADR-0010/0014, Policy Engine Phase 2). Resolution order: industry preset for the location's type -> country overlay (organization country x location type; conservative operational defaults for JP/US/GB/KR/TW/FR/DE/IT/ES/AU, not legal advice; see GET /api/rightos/policies) -> per-location override. Presets: shop (transferable, max 3, 12h, self-cancel), clinic (non-transferable, 12h, self-cancel), ev_charging (non-transferable, 4h, self-cancel), event (transferable once, 24h, no self-cancel), logistics (transferable, max 3, 24h, no self-cancel), pickup (transferable once, 6h, no self-cancel), other (transferable, max 3, 12h, self-cancel).",
        "properties": {
          "transferable": {
            "type": "boolean",
            "description": "Whether the right can be transferred (re-keying)"
          },
          "maxTransfers": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Maximum number of transfers. null = unlimited"
          },
          "defaultValidityMinutes": {
            "type": "integer",
            "description": "Default validity applied when a token is issued without endTime"
          },
          "verificationRequirement": {
            "type": "string",
            "enum": [
              "none",
              "external_id"
            ],
            "description": "external_id declares that identity checks are delegated to external partners/ID systems — RightOS itself never collects personal data"
          },
          "holderCancellable": {
            "type": "boolean",
            "description": "Whether the current holder (who knows the verificationCode) can self-cancel the token. Operator cancellation via the authenticated cancel endpoint is always available"
          }
        }
      },
      "PolicyChange": {
        "type": "object",
        "description": "Policy change audit record (ADR-0014). Contains no personal data. No delete/update API is provided.",
        "properties": {
          "id": {
            "type": "string"
          },
          "locationId": {
            "type": "string"
          },
          "organizationId": {
            "type": "string"
          },
          "before": {
            "type": [
              "object",
              "null"
            ],
            "description": "Override in effect before the change (null = preset/overlay only)"
          },
          "after": {
            "type": [
              "object",
              "null"
            ],
            "description": "Override in effect after the change (null = reset to preset)"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Location": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "loc_shop"
            ]
          },
          "organizationId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "type": {
            "$ref": "#/components/schemas/LocationType"
          },
          "timezone": {
            "type": "string",
            "examples": [
              "Asia/Tokyo"
            ]
          },
          "active": {
            "type": "boolean"
          }
        }
      },
      "RightTokenStatus": {
        "type": "string",
        "enum": [
          "issued",
          "verified",
          "used",
          "cancelled",
          "expired"
        ]
      },
      "RightToken": {
        "type": "object",
        "description": "Public Right Token. The secretHash is never exposed by any endpoint.",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "tok_2f8a1c9d3e4b5a6f7081"
            ]
          },
          "organizationId": {
            "type": "string"
          },
          "locationId": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "examples": [
              "順番待ち / Queue ticket"
            ]
          },
          "description": {
            "type": "string"
          },
          "startTime": {
            "type": "string",
            "format": "date-time"
          },
          "endTime": {
            "type": "string",
            "format": "date-time"
          },
          "priorityNumber": {
            "type": "integer",
            "description": "Queue position within the location"
          },
          "status": {
            "$ref": "#/components/schemas/RightTokenStatus"
          },
          "transferCount": {
            "type": "integer",
            "description": "Number of times transferred. 0 = original holder."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "usedAt": {
            "type": "string",
            "format": "date-time"
          },
          "cancelledAt": {
            "type": "string",
            "format": "date-time"
          },
          "transferredAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VerificationResult": {
        "type": "string",
        "enum": [
          "success",
          "failed",
          "expired",
          "cancelled",
          "already_used"
        ]
      },
      "IssuedTokenResponse": {
        "type": "object",
        "properties": {
          "token": {
            "$ref": "#/components/schemas/RightToken"
          },
          "verificationCode": {
            "type": "string",
            "description": "Returned exactly once. The server stores only its SHA-256 hash and cannot re-issue it.",
            "examples": [
              "K7MP-Q2XT9"
            ]
          },
          "walletUrl": {
            "type": "string",
            "description": "Right Wallet URL (QR display) to hand to the end user.",
            "examples": [
              "/software/rightos/wallet/tok_2f8a.../?code=K7MP-Q2XT9"
            ]
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "examples": [
              "missing_api_key",
              "invalid_api_key",
              "rate_limited",
              "confirmation_required",
              "not_found",
              "demo_org"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "enum": [
          "token.verified",
          "token.used",
          "token.cancelled",
          "token.transferred"
        ]
      },
      "WebhookDeliveryResult": {
        "type": "object",
        "description": "Most recent webhook delivery attempt.",
        "properties": {
          "at": { "type": "string", "format": "date-time" },
          "event": { "$ref": "#/components/schemas/WebhookEventType" },
          "ok": { "type": "boolean" },
          "status": { "type": "integer", "description": "HTTP status (absent on connection failure or timeout)" },
          "error": { "type": "string", "description": "Short error description (no secrets)" }
        },
        "required": ["at", "event", "ok"]
      },
      "Webhook": {
        "type": "object",
        "description": "Outbound webhook. The signing secret (whsec_...) is NOT stored server-side; it is derived from a master key and returned exactly once at registration.",
        "properties": {
          "id": {
            "type": "string"
          },
          "organizationId": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastDelivery": {
            "$ref": "#/components/schemas/WebhookDeliveryResult"
          },
          "deliveredCount": {
            "type": "integer",
            "minimum": 0
          },
          "failedCount": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": ["id", "organizationId", "url", "events", "createdAt"]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key (error: missing_api_key | invalid_api_key)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PlanLimit": {
        "description": "Plan limit exceeded (HTTP 402). Upgrade the plan to continue.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (HTTP 429). See Retry-After header. Limits: verify/transfer = 10/min per IP+token and 60/min per IP; signup = 5/hour per IP.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds until the window resets"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/rightos/billing/config": {
      "get": {
        "tags": [
          "billing"
        ],
        "operationId": "getBillingConfig",
        "summary": "Whether billing (Stripe) is enabled (no auth)",
        "responses": {
          "200": {
            "description": "Billing availability flag, used by UIs to show or hide upgrade actions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/rightos/billing/checkout": {
      "post": {
        "tags": [
          "billing"
        ],
        "operationId": "createCheckoutSession",
        "summary": "Create a Stripe Checkout session for a paid plan (hosted page; card data never touches RightOS servers)",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "planId"
                ],
                "properties": {
                  "planId": {
                    "type": "string",
                    "enum": [
                      "starter",
                      "business",
                      "pro"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout URL to redirect the user to",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_plan"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "demo_org — the demo organization cannot change plans"
          },
          "503": {
            "description": "billing_not_configured — billing is disabled in this environment (MVP mode)"
          }
        }
      }
    },
    "/api/rightos/billing/portal": {
      "post": {
        "tags": [
          "billing"
        ],
        "operationId": "createBillingPortalSession",
        "summary": "Create a Stripe Billing Portal session (plan changes, payment methods, cancellation)",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal URL to redirect the user to",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "demo_org"
          },
          "404": {
            "description": "no_billing_account — the organization has no Stripe customer yet"
          },
          "503": {
            "description": "billing_not_configured"
          }
        }
      }
    },
    "/api/rightos/billing/webhook": {
      "post": {
        "tags": [
          "billing"
        ],
        "operationId": "stripeWebhook",
        "summary": "Stripe webhook endpoint (signature-verified; not for direct use)",
        "description": "Verifies the stripe-signature header against the webhook secret. Handles checkout.session.completed, customer.subscription.updated, and customer.subscription.deleted (downgrade to free; data is never deleted).",
        "responses": {
          "200": {
            "description": "Event received"
          },
          "400": {
            "description": "missing_signature / invalid_signature"
          },
          "503": {
            "description": "billing_not_configured"
          }
        }
      }
    },
    "/api/rightos/plans": {
      "get": {
        "tags": [
          "plans"
        ],
        "operationId": "listPlans",
        "summary": "List pricing plans (no auth)",
        "responses": {
          "200": {
            "description": "All plans. Pricing is globally uniform (same price worldwide).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plans": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Plan"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/rightos/organizations": {
      "post": {
        "tags": [
          "organizations"
        ],
        "operationId": "registerOrganization",
        "summary": "Register an organization (returns the API key exactly once)",
        "description": "Creates an organization with a trialing subscription (no credit card during MVP). The response contains the API key (rk_live_...) exactly once — store it securely; the server keeps only its hash. Rate limit: 5/hour per IP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "contactEmail",
                  "planId"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "contactEmail": {
                    "type": "string",
                    "format": "email"
                  },
                  "planId": {
                    "$ref": "#/components/schemas/PlanId"
                  },
                  "country": {
                    "type": "string",
                    "default": "JP"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Organization created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    },
                    "subscription": {
                      "$ref": "#/components/schemas/Subscription"
                    },
                    "apiKey": {
                      "type": "string",
                      "description": "Shown exactly once."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/rightos/organizations/rotate-key": {
      "post": {
        "tags": [
          "organizations"
        ],
        "operationId": "rotateApiKey",
        "summary": "Re-issue the API key (old key invalidated immediately)",
        "description": "For suspected leaks or periodic rotation. Requires the current key. The new key is returned exactly once. Lost keys (cannot authenticate) are re-issued manually via I-S3 support after verifying the registered email.",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "New key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiKey": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Demo organization keys cannot be rotated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/rightos/organizations/delete": {
      "post": {
        "tags": [
          "organizations"
        ],
        "operationId": "deleteOrganization",
        "summary": "Permanently delete the organization and all its data (irreversible)",
        "description": "Deletes the organization, its locations, tokens, and verification logs. body.confirm must exactly match the organization name. Export your data first via GET /api/rightos/export if needed.",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirm"
                ],
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Exact organization name"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "confirmation_required (name mismatch — nothing deleted)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Demo organization cannot be deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/rightos/export": {
      "get": {
        "tags": [
          "organizations"
        ],
        "operationId": "exportOrganizationData",
        "summary": "Export all organization data as JSON (no lock-in)",
        "description": "Returns organization, subscriptions, locations, tokens, and verification logs. Contains no secret values (no key hashes, no code hashes).",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Full export (Content-Disposition: attachment)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exportedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    },
                    "subscriptions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Subscription"
                      }
                    },
                    "locations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Location"
                      }
                    },
                    "tokens": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RightToken"
                      }
                    },
                    "logs": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/rightos/locations": {
      "get": {
        "tags": [
          "locations"
        ],
        "operationId": "listLocations",
        "summary": "List your organization's locations",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Locations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "locations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Location"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "locations"
        ],
        "operationId": "createLocation",
        "summary": "Create a location (402 when plan limit exceeded)",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  },
                  "type": {
                    "$ref": "#/components/schemas/LocationType"
                  },
                  "timezone": {
                    "type": "string",
                    "default": "Asia/Tokyo"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "location": {
                      "$ref": "#/components/schemas/Location"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PlanLimit"
          }
        }
      }
    },
    "/api/rightos/locations/{locationId}/policy": {
      "get": {
        "tags": [
          "locations"
        ],
        "operationId": "getLocationPolicy",
        "summary": "Get the location's effective policy (public, for transparency)",
        "parameters": [
          {
            "name": "locationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Effective policy (override merged over industry preset)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "locationId": {
                      "type": "string"
                    },
                    "locationType": {
                      "$ref": "#/components/schemas/LocationType"
                    },
                    "policy": {
                      "$ref": "#/components/schemas/RightPolicy"
                    },
                    "hasOverride": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Location not found"
          }
        }
      },
      "put": {
        "tags": [
          "locations"
        ],
        "operationId": "setLocationPolicy",
        "summary": "Override the location policy (own organization only; partial update; null resets to preset)",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "locationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "transferable": {
                        "type": "boolean"
                      },
                      "maxTransfers": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "minimum": 0,
                        "maximum": 1000
                      },
                      "defaultValidityMinutes": {
                        "type": "integer",
                        "minimum": 5,
                        "maximum": 43200
                      },
                      "verificationRequirement": {
                        "type": "string",
                        "enum": [
                          "none",
                          "external_id"
                        ]
                      },
                      "holderCancellable": {
                        "type": "boolean"
                      }
                    }
                  }
                ],
                "description": "Partial policy override, or null to reset to the industry preset"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated effective policy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "locationId": {
                      "type": "string"
                    },
                    "locationType": {
                      "$ref": "#/components/schemas/LocationType"
                    },
                    "policy": {
                      "$ref": "#/components/schemas/RightPolicy"
                    },
                    "hasOverride": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid policy field or value"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Location belongs to another organization"
          },
          "404": {
            "description": "Location not found"
          }
        }
      }
    },
    "/api/rightos/status": {
      "get": {
        "tags": [
          "meta"
        ],
        "operationId": "getStatus",
        "summary": "Service status: self-checks and measured uptime (public)",
        "description": "Returns overall status (operational/degraded), component self-checks (API, data store latency, billing/email configuration), and measured uptime fractions (24h/7d/30d) from external multi-region uptime checks via Google Cloud Monitoring. The uptime field is null when monitoring data is unavailable. Human-readable status page: https://rightos.i-s3.com/software/rightos/status",
        "responses": {
          "200": {
            "description": "Current status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "operational",
                        "degraded"
                      ]
                    },
                    "time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "revision": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "checks": {
                      "type": "object"
                    },
                    "uptime": {
                      "type": [
                        "array",
                        "null"
                      ],
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string",
                            "enum": [
                              "24h",
                              "7d",
                              "30d"
                            ]
                          },
                          "fraction": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/rightos/policies": {
      "get": {
        "tags": [
          "locations"
        ],
        "operationId": "listPolicyDefinitions",
        "summary": "All industry presets and country overlays (public, for transparency)",
        "description": "Returns the full policy knowledge base: industry presets per location type and country overlays (organization country x location type). Resolution order for a location's effective policy: preset -> country overlay -> location override. Overlays are conservative operational defaults informed by local laws (e.g. Japan's Ticket Resale Prohibition Act, South Korea's 2026 Performance Act amendment, France's Penal Code art. 313-6-2) — not legal advice. Currently covers JP, US, GB, KR, TW, FR, DE, IT, ES, AU.",
        "responses": {
          "200": {
            "description": "Policy definitions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "resolutionOrder": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "presets": {
                      "type": "object",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/RightPolicy"
                      }
                    },
                    "countryOverlays": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "object"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/rightos/locations/{locationId}/policy/history": {
      "get": {
        "tags": [
          "locations"
        ],
        "operationId": "getLocationPolicyHistory",
        "summary": "Policy change audit log for the location (own organization only)",
        "description": "Returns policy override changes (before/after, timestamp) in reverse chronological order. Provides accountability for which rules were in effect at verification time (ADR-0014). Records are append-only; no delete/update API exists.",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "locationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Audit records, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "locationId": {
                      "type": "string"
                    },
                    "changes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PolicyChange"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Location belongs to another organization"
          },
          "404": {
            "description": "Location not found"
          }
        }
      }
    },
    "/api/rightos/tokens/{tokenId}/holder-cancel": {
      "post": {
        "tags": [
          "tokens"
        ],
        "operationId": "holderCancelToken",
        "summary": "Self-cancel by the current holder (requires the verificationCode; no auth header)",
        "description": "The current holder proves possession with the verificationCode and cancels the token. Rejected with 409 policy_cancel_disabled when the location's policy sets holderCancellable=false. Operator cancellation via the authenticated cancel endpoint is always available regardless of this policy. Rate limited like verify (10/min per token per IP).",
        "parameters": [
          {
            "name": "tokenId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "verificationCode"
                ],
                "properties": {
                  "verificationCode": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "$ref": "#/components/schemas/RightToken"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "verificationCode is required"
          },
          "403": {
            "description": "unauthorized — wrong verificationCode"
          },
          "404": {
            "description": "Token not found"
          },
          "409": {
            "description": "not_cancellable (already used/cancelled/expired) or policy_cancel_disabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/rightos/tokens/issue": {
      "post": {
        "tags": [
          "tokens"
        ],
        "operationId": "issueToken",
        "summary": "Issue a Right Token (the verificationCode is returned exactly once)",
        "description": "Issues a digital QR ticket at one of your locations. The response's verificationCode and walletUrl are shown exactly once — hand the walletUrl (QR) to the end user. 402 when the plan's monthly token limit is exceeded.",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "locationId",
                  "title"
                ],
                "properties": {
                  "locationId": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "startTime": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "endTime": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Issued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssuedTokenResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PlanLimit"
          },
          "403": {
            "description": "Location belongs to another organization"
          }
        }
      }
    },
    "/api/rightos/tokens/{tokenId}": {
      "get": {
        "tags": [
          "tokens"
        ],
        "operationId": "getToken",
        "summary": "Get Right Token details (never includes secretHash; no auth)",
        "parameters": [
          {
            "name": "tokenId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "$ref": "#/components/schemas/RightToken"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/rightos/tokens/{tokenId}/verify": {
      "post": {
        "tags": [
          "tokens"
        ],
        "operationId": "verifyToken",
        "summary": "Verify a Right Token by hash-matching its verificationCode (no auth)",
        "description": "Hash-compares the presented verificationCode. The result is recorded in the verification log. Rate limited against brute force: 10/min per IP+token, 60/min per IP.",
        "parameters": [
          {
            "name": "tokenId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "verificationCode"
                ],
                "properties": {
                  "verificationCode": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verification outcome",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "$ref": "#/components/schemas/VerificationResult"
                    },
                    "token": {
                      "$ref": "#/components/schemas/RightToken"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/rightos/tokens/{tokenId}/use": {
      "post": {
        "tags": [
          "tokens"
        ],
        "operationId": "useToken",
        "summary": "Mark a Right Token as used (own organization only)",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "tokenId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated token"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Token belongs to another organization"
          }
        }
      }
    },
    "/api/rightos/tokens/{tokenId}/cancel": {
      "post": {
        "tags": [
          "tokens"
        ],
        "operationId": "cancelToken",
        "summary": "Cancel a Right Token (own organization only)",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "tokenId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated token"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Token belongs to another organization"
          }
        }
      }
    },
    "/api/rightos/tokens/{tokenId}/transfer": {
      "post": {
        "tags": [
          "tokens"
        ],
        "operationId": "transferToken",
        "summary": "Transfer a right (re-keying: new code issued, old code invalidated; no auth)",
        "description": "Only the current holder (who knows the current verificationCode) can transfer. A new code is issued and the old one is invalidated immediately. Paid transfers/auctions are not offered. Rate limited like verify. Subject to the location's policy: returns 409 with error policy_transfer_disabled when the location forbids transfers, or transfer_limit_reached when maxTransfers is exhausted.",
        "parameters": [
          {
            "name": "tokenId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "verificationCode"
                ],
                "properties": {
                  "verificationCode": {
                    "type": "string",
                    "description": "Current code, proving transfer authority"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New code and wallet URL for the new holder (returned exactly once)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssuedTokenResponse"
                }
              }
            }
          },
          "409": {
            "description": "Wrong code or token not transferable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/rightos/console": {
      "get": {
        "tags": [
          "console"
        ],
        "operationId": "getConsoleData",
        "summary": "Dashboard data for your organization (stats, locations, tokens)",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "headerApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard data scoped to your organization"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/rightos/webhooks": {
      "get": {
        "tags": [
          "webhooks"
        ],
        "operationId": "listWebhooks",
        "summary": "List your organization's webhooks (auth required; never includes signing secrets)",
        "security": [
          {
            "apiKey": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "webhooks"
        ],
        "operationId": "createWebhook",
        "summary": "Register a webhook (auth required; up to 3 per organization; https only)",
        "description": "Events token.verified / token.used / token.cancelled / token.transferred are delivered as signed HTTPS POSTs (header x-rightos-signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, `${t}.${rawBody}`)>). The signing secret (whsec_...) is returned only once in this response and is never stored server-side. Delivery is best-effort: 3s timeout, no retries — respond 2xx immediately and process asynchronously. Private/internal hosts are rejected (SSRF protection).",
        "security": [
          {
            "apiKey": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https URL (http allowed for localhost only)"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/WebhookEventType"
                    },
                    "description": "Defaults to all four events"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. The secret is returned only once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/Webhook"
                    },
                    "secret": {
                      "type": "string",
                      "description": "Signing secret (whsec_...). Shown only once; not retrievable later."
                    },
                    "signatureHeader": {
                      "type": "string"
                    },
                    "signatureFormat": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid URL (non-https or internal host) or invalid events",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "webhook_limit_reached (3 per organization)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "demo_org (demo organization cannot register webhooks)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/rightos/webhooks/{webhookId}": {
      "delete": {
        "tags": [
          "webhooks"
        ],
        "operationId": "deleteWebhook",
        "summary": "Delete a webhook (auth required; own organization only)",
        "security": [
          {
            "apiKey": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "webhookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}
