{
  "openapi": "3.1.0",
  "info": {
    "title": "Digibite Public API",
    "version": "1.0.0",
    "summary": "API pública de Digibite — versión 1 (congelada)",
    "description": "API de lectura para integradores.\n\n> **La v1 está CONGELADA y marcada como obsoleta.** Todas las respuestas incluyen\n> las cabeceras `Deprecation: true` y `Warning: 299`. No se le añadirán endpoints.\n> Está en preparación una **v2** con ventas, catálogo completo, horarios y\n> sincronización incremental para integradores que hacen polling.\n>\n> Si estás integrando **ventas** (consumo teórico, escandallos, food cost), la v1\n> **no** es lo que necesitas: escribe a integrations@digibite.app y te damos el\n> contrato de la v2.\n\n## Entornos\n\nDos servidores con el mismo contrato: **producción** (`api-digibite.web.app`, datos reales) y **pruebas** (`ptw-api.web.app`, datos de desarrollo). Integra contra pruebas y cambia la URL al pasar a producción.\n\n**Las credenciales no son intercambiables**: una clave de pruebas no vale en producción ni al revés. Si te responde `401` tras cambiar de entorno, es esto.\n\n## Autenticación\n\nCabecera `x-api-key` en todas las peticiones. La credencial queda **acotada a una\norganización**: solo verás los restaurantes de la tuya. Una clave sin ámbito, de\nentorno distinto o caducada recibe `401`.\n\nEs una API **servidor a servidor**. No pongas la clave en un frontend: sería\npública. No hay CORS habilitado, precisamente por eso.\n\n## Límites de uso\n\n120 peticiones por minuto y por IP. Cada respuesta trae `RateLimit-Limit`,\n`RateLimit-Remaining` y `RateLimit-Reset`. Al superarlo recibes `429` con\n`Retry-After` en segundos.\n\n## Convenciones\n\n- Toda respuesta tiene la forma `{ status, data, message }`.\n- `status` vale `\"success\"` o `\"error\"`; en error, `errorCode` identifica el caso.\n- Los textos multiidioma son objetos con la lengua como clave (`{ \"es\": \"…\" }`).\n- Nada se cachea: las respuestas van con `Cache-Control: private, no-store`.\n",
    "contact": {
      "name": "Integraciones Digibite",
      "email": "integrations@digibite.app",
      "url": "https://digibite.app"
    }
  },
  "servers": [
    {
      "url": "https://api-digibite.web.app",
      "description": "Producción. Datos reales de los locales en explotación."
    },
    {
      "url": "https://ptw-api.web.app",
      "description": "Pruebas. Mismo contrato, datos de desarrollo: úsalo para integrar sin tocar locales en servicio. Las credenciales NO son intercambiables entre entornos."
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Restaurantes",
      "description": "Locales de tu organización: datos de escaparate y horarios."
    },
    {
      "name": "Catálogo",
      "description": "Carta publicada de un local, por modo de venta."
    },
    {
      "name": "Contacto",
      "description": "Formulario de contacto."
    }
  ],
  "paths": {
    "/restaurants/list": {
      "get": {
        "tags": [
          "Restaurantes"
        ],
        "summary": "Listar los restaurantes de tu organización",
        "description": "Devuelve los locales de la organización a la que está acotada tu credencial, ordenados por nombre.\n\n**Comprueba `meta.truncated`.** Si viene `true`, hay más locales de los que caben en el `limit` pedido y debes subirlo. No hay paginación por cursor en la v1.",
        "operationId": "listRestaurants",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Máximo de elementos. Por defecto y máximo: 200. Un valor no válido cae al valor por defecto.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 200
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Código de idioma para validar los textos. No traduce la respuesta.",
            "schema": {
              "type": "string",
              "example": "es"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Listado de restaurantes.",
            "headers": {
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "data",
                    "meta",
                    "message"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "success"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RestaurantListItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ListMeta"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                },
                "examples": {
                  "dosLocales": {
                    "summary": "Dos locales, sin truncar",
                    "value": {
                      "status": "success",
                      "data": [
                        {
                          "id": "UcncSuSSCw0RvvLzU7an",
                          "name": "Padthaiwok Malasaña",
                          "description": {
                            "es": "Cocina tailandesa"
                          },
                          "descriptionShort": {
                            "es": "Tailandés"
                          },
                          "phone": "+34611502712",
                          "address": {
                            "street": "Corredera Baja de San Pablo, 41",
                            "city": "Madrid",
                            "postalCode": "28004"
                          },
                          "coordinates": {
                            "lat": 40.4234,
                            "lng": -3.7038
                          },
                          "images": {},
                          "slug": "madrid-malasana",
                          "seo": {},
                          "schedule": null,
                          "scheduleRealtime": [],
                          "timezone": "Europe/Madrid"
                        }
                      ],
                      "meta": {
                        "count": 1,
                        "limit": 200,
                        "truncated": false
                      },
                      "message": ""
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/restaurants/{idRestaurant}": {
      "get": {
        "tags": [
          "Restaurantes"
        ],
        "summary": "Obtener un restaurante",
        "description": "Devuelve un local **de tu organización**. Si el identificador no existe, o existe pero pertenece a otra organización, la respuesta es `404` en ambos casos: el endpoint no revela qué identificadores existen.",
        "operationId": "getRestaurant",
        "parameters": [
          {
            "name": "idRestaurant",
            "in": "path",
            "required": true,
            "description": "Identificador del local, tal y como lo devuelve `/restaurants/list`.",
            "schema": {
              "type": "string",
              "example": "UcncSuSSCw0RvvLzU7an"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "es"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El restaurante.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "success"
                    },
                    "data": {
                      "$ref": "#/components/schemas/RestaurantDetail"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No existe, o no pertenece a tu organización.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "errorCode": "restaurants/not-found",
                  "message": "It is not possible to process the request"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/products/{idRestaurant}/{mode}": {
      "get": {
        "tags": [
          "Catálogo"
        ],
        "summary": "Carta publicada de un local",
        "description": "Devuelve el catálogo publicado para un modo de venta.\n\n> **Acotado a tu organización.** Un local que no pertenezca a la organización de tu apikey responde `404`, igual que uno inexistente: no se distinguen a propósito, para que el endpoint no sirva de oráculo de identificadores ajenos. Si `data` viene `null`, el local no tiene tarifa asignada para ese modo.\n\n> **v1 congelada.** Su contrato no cambiará; lo nuevo va en la v2.",
        "operationId": "getProducts",
        "parameters": [
          {
            "name": "idRestaurant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mode",
            "in": "path",
            "required": true,
            "description": "Modo de venta.",
            "schema": {
              "type": "string",
              "enum": [
                "delivery",
                "takeaway",
                "restaurant"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Catálogo, o `null` si el local no tiene tarifa para ese modo."
          },
          "401": {
            "description": "Clave no válida, local inexistente o modo no admitido. La v1 usa 401 para todos estos casos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "modoInvalido": {
                    "value": {
                      "status": "error",
                      "errorCode": "Restaurant/mode-not-found",
                      "message": "Avaibles modes are delivery, takeaway, restaurant"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/contact/sendContact": {
      "post": {
        "tags": [
          "Contacto"
        ],
        "summary": "Enviar un mensaje de contacto",
        "operationId": "sendContact",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mensaje aceptado."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Credencial acotada a una organización. Se solicita a integrations@digibite.app."
      }
    },
    "headers": {
      "RateLimitRemaining": {
        "description": "Peticiones restantes en la ventana actual.",
        "schema": {
          "type": "integer"
        }
      },
      "Deprecation": {
        "description": "Siempre `true` en la v1.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Falta la clave, no existe, está inactiva, ha caducado, es de otro entorno o no tiene organización asignada. No se distingue el motivo a propósito.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "sinClave": {
                "value": {
                  "status": "error",
                  "errorCode": "Auth/apikey-not-found",
                  "message": "It is not possible to process the request"
                }
              },
              "claveNoValida": {
                "value": {
                  "status": "error",
                  "errorCode": "Auth/apikey-not-valid",
                  "message": "It is not possible to process the request"
                }
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Se ha superado el límite de 120 peticiones por minuto.",
        "headers": {
          "Retry-After": {
            "description": "Segundos que faltan para que se reabra la ventana.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "status": "error",
              "errorCode": "RateLimit",
              "message": "Too many requests. Contact with administrator"
            }
          }
        }
      }
    },
    "schemas": {
      "LangText": {
        "type": "object",
        "description": "Texto multiidioma. La clave es el código de lengua.",
        "additionalProperties": {
          "type": "string"
        },
        "example": {
          "es": "Cocina tailandesa",
          "en": "Thai food"
        }
      },
      "ListMeta": {
        "type": "object",
        "required": [
          "count",
          "limit",
          "truncated"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Elementos devueltos en esta respuesta."
          },
          "limit": {
            "type": "integer",
            "description": "Límite efectivo aplicado."
          },
          "truncated": {
            "type": "boolean",
            "description": "`true` si se alcanzó el límite y puede haber más elementos sin devolver."
          }
        }
      },
      "RestaurantListItem": {
        "type": "object",
        "description": "Datos de escaparate de un local.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "$ref": "#/components/schemas/LangText"
          },
          "descriptionShort": {
            "$ref": "#/components/schemas/LangText"
          },
          "phone": {
            "type": "string"
          },
          "address": {
            "type": "object",
            "additionalProperties": true
          },
          "coordinates": {
            "type": "object",
            "properties": {
              "lat": {
                "type": "number"
              },
              "lng": {
                "type": "number"
              }
            }
          },
          "images": {
            "type": "object",
            "additionalProperties": true
          },
          "slug": {
            "type": "string"
          },
          "seo": {
            "type": "object",
            "additionalProperties": true
          },
          "schedule": {
            "type": [
              "object",
              "null"
            ],
            "description": "Horario publicado; `null` si no hay.",
            "additionalProperties": true
          },
          "scheduleRealtime": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "timezone": {
            "type": "string",
            "example": "Europe/Madrid"
          }
        }
      },
      "RestaurantDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RestaurantListItem"
          },
          {
            "type": "object",
            "properties": {
              "image": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": true
              },
              "onlineConfig": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        ],
        "description": "Como el elemento de listado, más la imagen principal y la configuración del canal online."
      },
      "Error": {
        "type": "object",
        "required": [
          "status",
          "errorCode",
          "message"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "error"
          },
          "errorCode": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      }
    }
  }
}
