{
  "openapi": "3.1.0",
  "info": {
    "title": "Riggle brief handoff API",
    "version": "1.1.0",
    "summary": "Turn a shoot description into a riggle.kr link that opens Riggle with the brief in the message box.",
    "description": "Riggle (리글) prepares photo and video shoots in Korea: gear list, lighting setup, rough rental budget and rental-shop candidates. Its one action, POST /api/v1/brief-link, returns a riggle.kr handoff link (GET /api/v1 lists the endpoints); the person opens it, reviews the brief, presses send and signs in (free). The API gives no answers and no data, stores and logs nothing, and needs no authentication. Limits: briefs up to 4,000 characters, and briefs with an email address or phone number are refused; send the brief in the JSON body, never in a URL query string. Rate limits: about 30 requests a minute per client (best effort, per running instance), reported on POST responses in RateLimit-Policy/RateLimit and RateLimit-Limit/-Remaining/-Reset headers; a 429 carries Retry-After. Versioning: the API is versioned in the path (/api/v1). Changes within v1 are additive only (new optional fields or error codes). A breaking change ships as /api/v2; v1 keeps working for at least 90 days with Deprecation and Sunset headers, announced on https://riggle.kr/developers.",
    "contact": {
      "name": "Riggle",
      "url": "https://riggle.kr/contact",
      "email": "contact@riggle.kr"
    },
    "termsOfService": "https://riggle.kr/legal/terms",
    "x-versioning-policy": {
      "scheme": "path",
      "current": "v1",
      "breakingChanges": "new major path (/api/v2)",
      "deprecationNoticeDays": 90,
      "signals": [
        "Deprecation",
        "Sunset"
      ],
      "announcements": "https://riggle.kr/developers"
    }
  },
  "externalDocs": {
    "description": "Developer guide",
    "url": "https://riggle.kr/developers"
  },
  "servers": [
    {
      "url": "https://riggle.kr",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "handoff",
      "description": "Hand a shoot to Riggle"
    }
  ],
  "paths": {
    "/api/v1/brief-link": {
      "post": {
        "operationId": "createBriefLink",
        "tags": [
          "handoff"
        ],
        "summary": "Create a Riggle brief handoff link",
        "description": "Validates the brief and returns a riggle.kr link whose fragment carries it. Opening the link fills Riggle's message box; nothing is sent until the person sends it. Briefs with email addresses or phone numbers are refused. A plain HTML form post (application/x-www-form-urlencoded) gets a 303 redirect to the link instead of JSON. Form posts receive errors as a short HTML page with the same message and hint, and a link longer than 7,000 characters as a one-click page instead of a redirect.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BriefLinkRequest"
              },
              "examples": {
                "english": {
                  "summary": "English brief",
                  "value": {
                    "brief": "Two-day product video for a coffee brand in a Seoul studio, Sony FX3, budget around 2,000,000 KRW"
                  }
                },
                "korean": {
                  "summary": "Korean brief",
                  "value": {
                    "brief": "서울 실내 웨딩 영상, 카메라 2대, FX3 + 24-70, 조명 추천해줘"
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/BriefLinkRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The handoff link (JSON). For a form post whose link is longer than 7,000 characters, an HTML page with a link to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BriefLinkResponse"
                },
                "example": {
                  "url": "https://riggle.kr/#brief=Two-day%20product%20video%20for%20a%20coffee%20brand",
                  "brief_characters": 40,
                  "note": "Give this link to the person. Opening it puts the brief in Riggle's message box; nothing is sent until they review it, press send and sign in (free)."
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "Idempotency-Key": {
                "$ref": "#/components/headers/Idempotency-Key"
              }
            }
          },
          "400": {
            "description": "Missing brief or invalid JSON (codes brief_missing, invalid_json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "brief_missing",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "405": {
            "description": "Only POST and OPTIONS are supported (code method_not_allowed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "413": {
            "description": "Body larger than 64 KB (code payload_too_large).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "payload_too_large",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "422": {
            "description": "Empty, too long, or contains contact details (codes brief_empty, brief_too_long, contact_details).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "contact_details",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests (code rate_limited).",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "303": {
            "description": "Form post accepted: redirect to the handoff link.",
            "headers": {
              "Location": {
                "description": "The riggle.kr/#brief= link.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional client key (1–255 visible ASCII characters). The operation has no side effects, so a retry with the same key and body returns the same link; the key is echoed back.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ]
      }
    },
    "/api/v1": {
      "get": {
        "operationId": "getApiIndex",
        "tags": [
          "handoff"
        ],
        "summary": "List the API's endpoints and documentation",
        "description": "Returns a JSON index of the endpoints, the OpenAPI description, the documentation page and the MCP servers. /api returns the same index.",
        "security": [],
        "responses": {
          "200": {
            "description": "API index.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "BriefLinkRequest": {
        "type": "object",
        "required": [
          "brief"
        ],
        "properties": {
          "brief": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000,
            "description": "The shoot in the person's own words (Korean or English): shoot type, dates, location, budget, gear they already have. No names, emails or phone numbers."
          }
        }
      },
      "BriefLinkResponse": {
        "type": "object",
        "required": [
          "url",
          "brief_characters",
          "note"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "riggle.kr link; the brief is in the URL fragment, which browsers do not send to servers."
          },
          "brief_characters": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4000,
            "description": "Length of the normalised brief in characters."
          },
          "note": {
            "type": "string",
            "description": "What to tell the person about the link."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "hint"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "brief_missing",
                  "brief_empty",
                  "brief_too_long",
                  "contact_details",
                  "invalid_json",
                  "payload_too_large",
                  "method_not_allowed",
                  "rate_limited",
                  "not_found"
                ],
                "description": "Stable error code."
              },
              "message": {
                "type": "string",
                "description": "What went wrong."
              },
              "hint": {
                "type": "string",
                "description": "How to fix it."
              }
            }
          }
        },
        "description": "Every error from the API, including 404 for unknown /api/v1 paths, has this shape."
      },
      "ApiIndex": {
        "type": "object",
        "required": [
          "name",
          "version",
          "endpoints",
          "openapi",
          "documentation"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "method": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                },
                "operationId": {
                  "type": "string"
                }
              }
            }
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "mcp": {
            "type": "object",
            "properties": {
              "product": {
                "type": "string",
                "format": "uri"
              },
              "docs": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "IETF draft rate-limit policy, e.g. \"burst\";q=30;w=60.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit": {
        "description": "IETF draft remaining quota, e.g. \"burst\";r=29;t=60.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit-Limit": {
        "description": "Requests allowed per window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Remaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Reset": {
        "description": "Seconds until the window resets.",
        "schema": {
          "type": "integer"
        }
      },
      "Idempotency-Key": {
        "description": "Echo of the request's Idempotency-Key.",
        "schema": {
          "type": "string"
        }
      }
    }
  }
}
