{
  "openapi": "3.1.0",
  "info": {
    "title": "RecetteClic public recipes API",
    "version": "1.0.0",
    "description": "Public, read-only access to the RecetteClic recipe directory: search community recipes structured by AI from TikTok, YouTube, Instagram videos and websites, and read their ingredients, steps, timings and estimated nutrition. No account or API key required. Recipe pages: https://recetteclic.app/<locale>/r/<slug>.\n\nVersioning: the major version is in the path (`/v1`). Within a version, changes are additive only (new fields or endpoints); a breaking change ships as `/v2`. A deprecated endpoint or version answers with `Deprecation` and `Sunset` headers at least 6 months before removal.\n\nRate limits: 120 requests per 60 s per IP, announced on every response by the `RateLimit` and `RateLimit-Policy` headers (plus `RateLimit-Limit`, `-Remaining`, `-Reset`); a 429 carries `Retry-After`.\n\nErrors: every 4xx/5xx body is the `Error` object `{ error, message }`; switch on `error`, never on `message`.",
    "contact": {
      "email": "hello@recetteclic.app",
      "url": "https://recetteclic.app"
    }
  },
  "servers": [
    {
      "url": "https://api.recetteclic.app/v1",
      "description": "Version 1"
    }
  ],
  "externalDocs": {
    "description": "Developer documentation",
    "url": "https://recetteclic.app/developers"
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable code. Never renamed within a version.",
            "enum": [
              "not_found",
              "rate_limited",
              "internal_error"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation, in English."
          },
          "retryAfterSec": {
            "type": "integer",
            "description": "On 429 only: seconds until the window resets."
          }
        },
        "example": {
          "error": "not_found",
          "message": "No recipe with this slug."
        }
      }
    },
    "headers": {
      "RateLimit": {
        "description": "Remaining quota and seconds to reset (IETF draft-ietf-httpapi-ratelimit-headers).",
        "schema": {
          "type": "string",
          "example": "\"public-v1\";r=119;t=60"
        }
      },
      "RateLimit-Policy": {
        "description": "Quota policy: q = requests, w = window in seconds.",
        "schema": {
          "type": "string",
          "example": "\"public-v1\";q=120;w=60"
        }
      },
      "RateLimit-Limit": {
        "description": "Requests allowed per window.",
        "schema": {
          "type": "integer",
          "example": 120
        }
      },
      "RateLimit-Remaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Reset": {
        "description": "Seconds until the window resets.",
        "schema": {
          "type": "integer"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "NotFound": {
        "description": "Not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          },
          "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"
            },
            "example": {
              "error": "rate_limited",
              "message": "Too many requests. Retry in 42 s.",
              "retryAfterSec": 42
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/recipes/public/list": {
      "get": {
        "operationId": "listPublicRecipes",
        "summary": "Search or browse public recipes",
        "description": "Newest first. `q` matches the original title, description and tags (mostly French); ingredients are not searched.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Keyword matched against the original title, description and tags.",
            "schema": {
              "type": "string",
              "maxLength": 80
            }
          },
          {
            "name": "tag",
            "in": "query",
            "description": "Category slug from /recipes/public/tags.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number, from 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Results per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 60,
              "default": 30
            }
          },
          {
            "name": "x-rc-locale",
            "in": "header",
            "required": false,
            "description": "Language of the returned recipe text.",
            "schema": {
              "type": "string",
              "enum": [
                "fr",
                "en",
                "es"
              ],
              "default": "fr"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of public recipes.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "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": {
                  "type": "object",
                  "properties": {
                    "recipes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "coverUrl": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "source": {
                            "type": "string",
                            "description": "tiktok, youtube, instagram, web, photo, manual…"
                          },
                          "prepTimeMin": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "cookTimeMin": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "totalTimeMin": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "difficulty": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "very_easy",
                              "easy",
                              "medium",
                              "hard",
                              "expert",
                              null
                            ]
                          },
                          "likesCount": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "totalCount": {
                      "type": "integer"
                    },
                    "pageCount": {
                      "type": "integer"
                    },
                    "hasNextPage": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/recipes/public/tags": {
      "get": {
        "operationId": "listPublicCategories",
        "summary": "Public recipe categories with their volume",
        "responses": {
          "200": {
            "description": "Categories.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "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": {
                  "type": "object",
                  "properties": {
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/recipes/by-slug/{slug}": {
      "get": {
        "operationId": "getRecipeBySlug",
        "summary": "Full recipe by slug",
        "description": "Ingredients, steps, servings, timings, estimated nutrition per serving, source and author. Also serves recipes shared by link only (`isPublic: false`); do not list or republish those.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-rc-locale",
            "in": "header",
            "required": false,
            "description": "Language of the returned recipe text.",
            "schema": {
              "type": "string",
              "enum": [
                "fr",
                "en",
                "es"
              ],
              "default": "fr"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The recipe.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "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": {
                  "type": "object",
                  "properties": {
                    "recipe": {
                      "allOf": [
                        {
                          "type": "object",
                          "properties": {
                            "slug": {
                              "type": "string"
                            },
                            "title": {
                              "type": "string"
                            },
                            "description": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "coverUrl": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "source": {
                              "type": "string",
                              "description": "tiktok, youtube, instagram, web, photo, manual…"
                            },
                            "prepTimeMin": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "cookTimeMin": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "totalTimeMin": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "difficulty": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "enum": [
                                "very_easy",
                                "easy",
                                "medium",
                                "hard",
                                "expert",
                                null
                              ]
                            },
                            "likesCount": {
                              "type": "integer"
                            }
                          }
                        },
                        {
                          "type": "object",
                          "properties": {
                            "isPublic": {
                              "type": "boolean"
                            },
                            "servings": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "tags": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "sourceUrl": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "originalAuthor": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "ingredients": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "name": {
                                    "type": "string"
                                  },
                                  "quantity": {
                                    "type": [
                                      "string",
                                      "null"
                                    ]
                                  },
                                  "unit": {
                                    "type": [
                                      "string",
                                      "null"
                                    ]
                                  }
                                }
                              }
                            },
                            "steps": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "index": {
                                    "type": "integer"
                                  },
                                  "text": {
                                    "type": "string"
                                  },
                                  "durationSec": {
                                    "type": [
                                      "integer",
                                      "null"
                                    ]
                                  }
                                }
                              }
                            },
                            "nutrition": {
                              "type": [
                                "object",
                                "null"
                              ],
                              "description": "Per serving, estimated (USDA)."
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "health",
        "summary": "Service status",
        "responses": {
          "200": {
            "description": "The API is up.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "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": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    },
                    "time": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  }
}
