{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "FASTIK API",
    "version": "1.0.0",
    "description": "La API v1 de FASTIK. Errores en `application/problem+json` (RFC 9457) con un `code` estable; una ruta que no existe da 404 `not_found` y otro método, 405 `method_not_allowed` con `Allow`. Ignora los campos que no conozcas: una versión menor puede añadir alguno."
  },
  "servers": [{ "url": "https://api.fastik.es" }],
  "security": [{ "clave": [] }],
  "paths": {
    "/v1/me": {
      "get": {
        "operationId": "getMe",
        "summary": "La clave y la organización: para «Probar la conexión».",
        "x-fastik-scope": "read",
        "description": "Permiso: `read`.",
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/Me" },
                "example": {
                  "key": {
                    "id": "6a0f3b7e-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
                    "name": "ERP",
                    "prefix": "fk_live_3f9a1c07",
                    "scopes": ["read"],
                    "expires_at": "2027-10-28T09:12:00.000Z"
                  },
                  "organization": { "name": "Tu organización" }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    },
    "/v1/families": {
      "get": {
        "operationId": "listFamilies",
        "summary": "Las familias del catálogo, en una sola página.",
        "x-fastik-scope": "read",
        "description": "Permiso: `read`.",
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/FamilyPage" },
                "example": {
                  "data": [{ "key": "fontaneria", "name": "Fontanería" }],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    },
    "/v1/products": {
      "get": {
        "operationId": "listProducts",
        "summary": "Los productos, por `updated_at`. Sin `updated_since`, sin los archivados.",
        "x-fastik-scope": "read",
        "description": "Permiso: `read`.",
        "parameters": [
          {
            "name": "updated_since",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "sku",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "minLength": 1, "maxLength": 100 }
          },
          {
            "name": "family",
            "in": "query",
            "required": false,
            "schema": { "$ref": "#/components/schemas/FamilyKey" }
          },
          {
            "name": "include_archived",
            "in": "query",
            "required": false,
            "schema": { "enum": ["true", "false"] }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "El `next_cursor` de la página anterior, con los mismos filtros.",
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/ProductPage" },
                "example": {
                  "data": [
                    {
                      "id": "0d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6",
                      "sku": "FIL-0042",
                      "name": "Filtro de agua",
                      "family": "fontaneria",
                      "unit": "uds",
                      "current_stock": 12,
                      "min_stock": 4,
                      "reorder_qty": 10,
                      "archived_at": null,
                      "created_at": "2026-10-28T09:12:00.000Z",
                      "updated_at": "2026-10-28T09:12:00.000Z"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`, `invalid_cursor`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "upsertProducts",
        "summary": "Un lote de 1 a 100 altas o actualizaciones por `sku`. Nunca cambia el stock.",
        "x-fastik-scope": "catalog:write",
        "description": "Permiso: `catalog:write`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ProductBatch" },
              "example": {
                "products": [
                  {
                    "sku": "FIL-0042",
                    "name": "Filtro de agua",
                    "family": "fontaneria",
                    "unit": "uds",
                    "min_stock": 4,
                    "reorder_qty": 10,
                    "unit_cost": 3.5
                  },
                  { "sku": "LED-E27", "name": "Bombilla LED E27", "unit_cost": 2.1 }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/ProductResults" },
                "example": {
                  "results": [
                    {
                      "sku": "FIL-0042",
                      "status": "updated",
                      "product_id": "0d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6"
                    },
                    {
                      "sku": "LED-E27",
                      "status": "created",
                      "product_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`, `invalid_body`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`, `insufficient_scope`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "413": {
            "description": "`payload_too_large`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "415": {
            "description": "`unsupported_media_type`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    },
    "/v1/products/{id}": {
      "get": {
        "operationId": "getProduct",
        "summary": "Un producto; 404 si no es de la organización.",
        "x-fastik-scope": "read",
        "description": "Permiso: `read`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/Product" },
                "example": {
                  "id": "0d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6",
                  "sku": null,
                  "name": "Bombilla LED E27",
                  "family": null,
                  "unit": "uds",
                  "current_stock": 0,
                  "min_stock": 0,
                  "reorder_qty": 0,
                  "archived_at": null,
                  "created_at": "2026-10-28T09:12:00.000Z",
                  "updated_at": "2026-10-28T09:12:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "404": {
            "description": "`not_found`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    },
    "/v1/movements": {
      "get": {
        "operationId": "listMovements",
        "summary": "Los movimientos, por `recorded_at`, sin los de los 2 últimos segundos. Solapa 5 minutos y deduplica por `id`.",
        "x-fastik-scope": "read",
        "description": "Permiso: `read`.",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "product_id",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "uuid" }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": { "enum": ["withdraw", "restock", "adjust"] }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "El `next_cursor` de la página anterior, con los mismos filtros.",
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/MovementPage" },
                "example": {
                  "data": [
                    {
                      "id": "6a0f3b7e-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
                      "product_id": "0d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6",
                      "type": "withdraw",
                      "quantity": 2,
                      "delta": -2,
                      "stock_after": 10,
                      "reason": null,
                      "occurred_at": "2026-10-28T09:12:00.000Z",
                      "recorded_at": "2026-10-28T09:12:00.000Z",
                      "source": "api",
                      "api_key_id": "1b1b1b1b-1b1b-4b1b-8b1b-1b1b1b1b1b1b"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`, `invalid_cursor`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createMovements",
        "summary": "Un lote de 1 a 100 retiradas o reposiciones. Repetir un `id` no aplica nada dos veces.",
        "x-fastik-scope": "movements:write",
        "description": "Permiso: `movements:write`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/MovementBatch" },
              "example": {
                "movements": [
                  {
                    "id": "6a0f3b7e-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
                    "product_id": "0d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6",
                    "type": "withdraw",
                    "quantity": 2,
                    "occurred_at": "2026-10-28T09:12:00.000Z"
                  },
                  {
                    "id": "7b1a4c8f-2d3e-4f50-9a6b-1c2d3e4f5a6b",
                    "sku": "FIL-0042",
                    "type": "restock",
                    "quantity": 10
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/MovementResults" },
                "example": {
                  "results": [
                    {
                      "id": "6a0f3b7e-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
                      "status": "applied",
                      "stock": 10
                    },
                    {
                      "id": "7b1a4c8f-2d3e-4f50-9a6b-1c2d3e4f5a6b",
                      "status": "rejected",
                      "stock": null,
                      "reason": "product_not_found"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`, `invalid_body`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`, `insufficient_scope`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "413": {
            "description": "`payload_too_large`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "415": {
            "description": "`unsupported_media_type`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    },
    "/v1/alerts": {
      "get": {
        "operationId": "listAlerts",
        "summary": "Los avisos de stock abiertos; con `include_resolved_since`, también los cerrados.",
        "x-fastik-scope": "read",
        "description": "Permiso: `read`.",
        "parameters": [
          {
            "name": "include_resolved_since",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "El `next_cursor` de la página anterior, con los mismos filtros.",
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/AlertPage" },
                "example": {
                  "data": [
                    {
                      "id": "6a0f3b7e-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
                      "kind": "below_minimum",
                      "product": {
                        "id": "0d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6",
                        "sku": "FIL-0042",
                        "name": "Filtro de agua",
                        "unit": "uds",
                        "current_stock": 2,
                        "min_stock": 4
                      },
                      "created_at": "2026-10-28T09:12:00.000Z",
                      "resolved_at": null
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`, `invalid_cursor`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    },
    "/v1/costs": {
      "get": {
        "operationId": "listCosts",
        "summary": "El coste unitario actual de cada producto, sin impuestos, por `updated_at`.",
        "x-fastik-scope": "costs:read",
        "description": "Permiso: `costs:read`.",
        "parameters": [
          {
            "name": "updated_since",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "El `next_cursor` de la página anterior, con los mismos filtros.",
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/CostPage" },
                "example": {
                  "data": [
                    {
                      "product_id": "0d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6",
                      "unit_cost": 3.5,
                      "tax_rate": null,
                      "updated_at": "2026-10-28T09:12:00.000Z"
                    }
                  ],
                  "currency": "EUR",
                  "tax_label": "IGIC",
                  "default_tax_rate": 0.07,
                  "prices_include_tax": false,
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`, `invalid_cursor`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`, `insufficient_scope`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    },
    "/v1/purchases": {
      "get": {
        "operationId": "listPurchases",
        "summary": "Las compras, por `registered_at`; las anuladas, con `voided_at`.",
        "x-fastik-scope": "costs:read",
        "description": "Permiso: `costs:read`.",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "El `next_cursor` de la página anterior, con los mismos filtros.",
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "Bien.",
            "headers": {
              "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/PurchasePage" },
                "example": {
                  "data": [
                    {
                      "id": "6a0f3b7e-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
                      "movement_id": "7b1a4c8f-2d3e-4f50-9a6b-1c2d3e4f5a6b",
                      "product_id": "0d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6",
                      "quantity": 10,
                      "unit_cost": 3.5,
                      "urgent": false,
                      "registered_at": "2026-10-28T09:12:00.000Z",
                      "voided_at": null
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`, `invalid_cursor`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "401": {
            "description": "`unauthorized`",
            "headers": {
              "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "403": {
            "description": "`organization_suspended`, `integrations_not_enabled`, `insufficient_scope`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable`",
            "headers": { "Retry-After": { "$ref": "#/components/headers/Retry-After" } },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    },
    "/v1/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "summary": "Este documento. Público, sin clave.",
        "security": [],
        "responses": {
          "200": {
            "description": "Bien.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OpenApiDocument" },
                "example": { "openapi": "3.1.0" }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter`",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "clave": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "fk_live_ o fk_test_ y 64 hexadecimales"
      }
    },
    "headers": {
      "RateLimit-Limit": {
        "description": "Peticiones del minuto para este tipo (lectura o escritura).",
        "schema": { "type": "integer" }
      },
      "RateLimit-Remaining": {
        "description": "Las que quedan en el minuto.",
        "schema": { "type": "integer" }
      },
      "RateLimit-Reset": {
        "description": "Segundos hasta el minuto siguiente.",
        "schema": { "type": "integer" }
      },
      "Retry-After": {
        "description": "Segundos antes de volver a probar.",
        "schema": { "type": "integer" }
      },
      "WWW-Authenticate": {
        "description": "Siempre `Bearer`.",
        "schema": { "const": "Bearer" }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "format": "uri" },
          "title": { "type": "string" },
          "status": { "type": "integer", "minimum": 400, "maximum": 599 },
          "code": {
            "enum": [
              "unauthorized",
              "insufficient_scope",
              "organization_suspended",
              "integrations_not_enabled",
              "not_found",
              "method_not_allowed",
              "invalid_parameter",
              "invalid_body",
              "payload_too_large",
              "unsupported_media_type",
              "invalid_cursor",
              "rate_limited",
              "internal_error",
              "temporarily_unavailable"
            ]
          },
          "detail": { "type": "string" },
          "errors": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/BatchError" }
          }
        },
        "required": ["type", "title", "status", "code", "detail"]
      },
      "BatchError": {
        "type": "object",
        "properties": {
          "index": {
            "type": ["integer", "null"],
            "minimum": 0,
            "description": "`null` si es del lote entero."
          },
          "field": {
            "type": ["string", "null"],
            "description": "`null` si es del elemento entero."
          },
          "code": {
            "enum": [
              "required",
              "invalid_type",
              "invalid_value",
              "out_of_range",
              "not_allowed",
              "invalid_characters",
              "not_normalized"
            ]
          }
        },
        "required": ["index", "field", "code"]
      },
      "Me": {
        "type": "object",
        "properties": {
          "key": {
            "type": "object",
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "name": { "type": "string" },
              "prefix": { "type": "string", "pattern": "^fk_(live|test)_[0-9a-f]{8}$" },
              "scopes": {
                "type": "array",
                "items": {
                  "enum": ["read", "costs:read", "movements:write", "catalog:write"]
                },
                "uniqueItems": true
              },
              "expires_at": { "type": ["string", "null"], "format": "date-time" }
            },
            "required": ["id", "name", "prefix", "scopes", "expires_at"]
          },
          "organization": {
            "type": "object",
            "properties": { "name": { "type": "string" } },
            "required": ["name"]
          }
        },
        "required": ["key", "organization"]
      },
      "FamilyKey": {
        "enum": [
          "iluminacion",
          "material-electrico",
          "pilas-y-baterias",
          "fontaneria",
          "griferia-y-sanitarios",
          "climatizacion-y-ventilacion",
          "cerrajeria-y-herrajes",
          "fijacion-y-tornilleria",
          "adhesivos-sellantes-y-cintas",
          "pintura-y-acabados",
          "carpinteria-y-mobiliario",
          "piscina-y-tratamiento-de-agua",
          "jardineria-y-riego",
          "incendios-y-senalizacion",
          "epi-y-vestuario",
          "quimicos-y-lubricantes",
          "habitacion-y-aparatos",
          "cocina-y-lavanderia",
          "herramienta-y-consumibles",
          "limpieza-y-consumibles",
          "otros"
        ]
      },
      "Family": {
        "type": "object",
        "properties": {
          "key": { "$ref": "#/components/schemas/FamilyKey" },
          "name": { "type": "string" }
        },
        "required": ["key", "name"]
      },
      "FamilyPage": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Family" } },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "Para la siguiente página (`?cursor=`); `null` en la última."
          }
        },
        "required": ["data", "next_cursor"]
      },
      "Product": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "sku": { "type": ["string", "null"] },
          "name": { "type": "string" },
          "family": {
            "oneOf": [{ "$ref": "#/components/schemas/FamilyKey" }, { "type": "null" }]
          },
          "unit": {
            "enum": ["uds", "m", "L", "kg", "cajas", "rollos", "pares", "juegos"]
          },
          "current_stock": { "type": "integer" },
          "min_stock": { "type": "integer", "minimum": 0 },
          "reorder_qty": { "type": "integer" },
          "archived_at": { "type": ["string", "null"], "format": "date-time" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        },
        "required": [
          "id",
          "sku",
          "name",
          "family",
          "unit",
          "current_stock",
          "min_stock",
          "reorder_qty",
          "archived_at",
          "created_at",
          "updated_at"
        ]
      },
      "ProductPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Product" }
          },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "Para la siguiente página (`?cursor=`); `null` en la última."
          }
        },
        "required": ["data", "next_cursor"]
      },
      "Person": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" }
        },
        "required": ["id", "name"]
      },
      "Movement": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "product_id": { "type": "string", "format": "uuid" },
          "type": { "enum": ["withdraw", "restock", "adjust"] },
          "quantity": { "type": "integer", "minimum": 0 },
          "delta": { "type": "integer" },
          "stock_after": { "type": ["integer", "null"] },
          "reason": {
            "enum": [
              "count",
              "breakage",
              "loss",
              "entry_error",
              "return",
              "other",
              "initial_stock",
              null
            ]
          },
          "occurred_at": { "type": "string", "format": "date-time" },
          "recorded_at": { "type": "string", "format": "date-time" },
          "source": { "enum": ["app", "shared_device", "api", "system", null] },
          "api_key_id": {
            "type": "string",
            "format": "uuid",
            "description": "Solo en los de una clave (`source: api`)."
          },
          "person": {
            "oneOf": [{ "$ref": "#/components/schemas/Person" }, { "type": "null" }],
            "description": "Solo con «Datos de personas» encendido en la organización."
          }
        },
        "required": [
          "id",
          "product_id",
          "type",
          "quantity",
          "delta",
          "stock_after",
          "reason",
          "occurred_at",
          "recorded_at",
          "source"
        ]
      },
      "MovementPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Movement" }
          },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "Para la siguiente página (`?cursor=`); `null` en la última."
          }
        },
        "required": ["data", "next_cursor"]
      },
      "Alert": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "kind": { "enum": ["below_minimum", "negative"] },
          "product": {
            "type": "object",
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "sku": { "type": ["string", "null"] },
              "name": { "type": "string" },
              "unit": {
                "enum": ["uds", "m", "L", "kg", "cajas", "rollos", "pares", "juegos"]
              },
              "current_stock": { "type": "integer" },
              "min_stock": { "type": "integer", "minimum": 0 }
            },
            "required": ["id", "sku", "name", "unit", "current_stock", "min_stock"]
          },
          "created_at": { "type": "string", "format": "date-time" },
          "resolved_at": { "type": ["string", "null"], "format": "date-time" }
        },
        "required": ["id", "kind", "product", "created_at", "resolved_at"]
      },
      "AlertPage": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Alert" } },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "Para la siguiente página (`?cursor=`); `null` en la última."
          }
        },
        "required": ["data", "next_cursor"]
      },
      "Cost": {
        "type": "object",
        "properties": {
          "product_id": { "type": "string", "format": "uuid" },
          "unit_cost": { "type": "number", "minimum": 0 },
          "tax_rate": {
            "type": ["number", "null"],
            "minimum": 0,
            "description": "`null`: el general de la página."
          },
          "updated_at": { "type": "string", "format": "date-time" }
        },
        "required": ["product_id", "unit_cost", "tax_rate", "updated_at"]
      },
      "CostPage": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Cost" } },
          "currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
          "tax_label": { "type": "string" },
          "default_tax_rate": { "type": "number", "minimum": 0 },
          "prices_include_tax": { "const": false },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "Para la siguiente página (`?cursor=`); `null` en la última."
          }
        },
        "required": [
          "data",
          "currency",
          "tax_label",
          "default_tax_rate",
          "prices_include_tax",
          "next_cursor"
        ]
      },
      "Purchase": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "movement_id": { "type": "string", "format": "uuid" },
          "product_id": { "type": "string", "format": "uuid" },
          "quantity": { "type": "integer", "minimum": 1 },
          "unit_cost": { "type": "number", "minimum": 0 },
          "urgent": { "type": "boolean" },
          "registered_at": { "type": "string", "format": "date-time" },
          "voided_at": { "type": ["string", "null"], "format": "date-time" }
        },
        "required": [
          "id",
          "movement_id",
          "product_id",
          "quantity",
          "unit_cost",
          "urgent",
          "registered_at",
          "voided_at"
        ]
      },
      "PurchasePage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Purchase" }
          },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "Para la siguiente página (`?cursor=`); `null` en la última."
          }
        },
        "required": ["data", "next_cursor"]
      },
      "MovementInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Lo genera quien llama: es la idempotencia."
          },
          "product_id": { "type": "string", "format": "uuid" },
          "sku": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "Si llega `product_id`, se ignora."
          },
          "type": { "enum": ["withdraw", "restock"] },
          "quantity": { "type": "integer", "minimum": 1, "maximum": 100000 },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "De 30 días atrás a 5 minutos adelante; de serie, ahora."
          },
          "expected_stock": { "description": "Se admite y se ignora." },
          "destination_id": { "description": "Se admite y se ignora." }
        },
        "required": ["id", "type", "quantity"],
        "additionalProperties": false,
        "anyOf": [{ "required": ["product_id"] }, { "required": ["sku"] }]
      },
      "MovementBatch": {
        "type": "object",
        "properties": {
          "movements": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": { "$ref": "#/components/schemas/MovementInput" }
          }
        },
        "required": ["movements"],
        "additionalProperties": false
      },
      "MovementResult": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": { "enum": ["applied", "duplicate", "rejected"] },
          "stock": { "type": ["integer", "null"] },
          "reason": { "enum": ["product_not_found", "id_conflict"] }
        },
        "required": ["id", "status", "stock"],
        "if": { "properties": { "status": { "const": "rejected" } } },
        "then": { "required": ["reason"] },
        "else": { "not": { "required": ["reason"] } }
      },
      "MovementResults": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MovementResult" }
          }
        },
        "required": ["results"]
      },
      "ProductInput": {
        "type": "object",
        "properties": {
          "sku": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "La referencia de tu ERP: la llave, exacta, una vez por lote. Sin espacios en los bordes; sin espacios que no sean U+0020, caracteres de formato ni de control (`invalid_characters`), y en NFC (`not_normalized`)."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Obligatorio en el alta (sin él, `rejected` con `name_required`); sin él, un existente conserva el suyo. Las mismas reglas de texto que `sku`."
          },
          "family": {
            "oneOf": [{ "$ref": "#/components/schemas/FamilyKey" }, { "type": "null" }]
          },
          "unit": {
            "enum": ["uds", "m", "L", "kg", "cajas", "rollos", "pares", "juegos"]
          },
          "min_stock": { "type": "integer", "minimum": 0, "maximum": 1000000 },
          "reorder_qty": { "type": "integer", "minimum": 1, "maximum": 1000000 },
          "unit_cost": {
            "type": "number",
            "minimum": 0,
            "maximum": 99999999.9999,
            "description": "Sin impuestos, con como mucho 4 decimales. Obligatorio en el alta. El ruido de la coma flotante (`0.30000000000000004`) se redondea a 4 decimales; `1e-5` o `3.14159` son `invalid_value`."
          }
        },
        "required": ["sku"],
        "additionalProperties": false
      },
      "ProductBatch": {
        "type": "object",
        "properties": {
          "products": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": { "$ref": "#/components/schemas/ProductInput" }
          }
        },
        "required": ["products"],
        "additionalProperties": false
      },
      "ProductResult": {
        "type": "object",
        "properties": {
          "sku": { "type": "string" },
          "status": { "enum": ["created", "updated", "unchanged", "rejected"] },
          "product_id": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "`null` solo en `name_required` y `unit_cost_required`."
          },
          "reason": {
            "enum": ["name_required", "unit_cost_required", "product_archived"]
          }
        },
        "required": ["sku", "status", "product_id"],
        "if": { "properties": { "status": { "const": "rejected" } } },
        "then": { "required": ["reason"] },
        "else": { "not": { "required": ["reason"] } }
      },
      "ProductResults": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ProductResult" }
          }
        },
        "required": ["results"]
      },
      "OpenApiDocument": { "type": "object", "description": "Este documento." }
    }
  }
}
