{
  "openapi": "3.1.0",
  "info": {
    "title": "Multas MX API",
    "version": "1.0.0",
    "summary": "Consulta vehicular de México por API REST: multas, tenencia, vigencia de placa, verificación y reporte de robo (REPUVE).",
    "description": "API REST asíncrona de Multas MX para consultar lo que debe y reporta un vehículo en México: multas e infracciones estatales y municipales, adeudo de tenencia, vigencia de placa, verificación vehicular y reporte de robo en REPUVE.\n\n**Flujo.** `POST /lookup` con la placa y los `stateCode` que quieras consultar (salen de `GET /states`) → respuesta inmediata `201` con un `requestId` → el resultado llega por webhook (`state_completed` por estado y `completed` al final) o con `GET /lookup/{requestId}`.\n\n**Autenticación.** Header `x-api-key` con la API key de tu cuenta (https://dashboard.multasmx.com).\n\n**Pruebas gratis.** Las placas `PRUEBA-LIMPIA`, `PRUEBA-ADEUDOS`, `PRUEBA-NOREG` y `PRUEBA-FALLA` (y los NIV `PRUEBA0000000001` a `PRUEBA0000000004` para REPUVE) devuelven datos ficticios con la misma forma que una consulta real, sin costo y con `sandbox: true`.\n\n**Créditos.** Cada estado o servicio de un `POST /lookup` cuesta créditos prepagados: 1 por multas, tenencia, vigencia o verificación y 3 por REPUVE. El costo vigente está en `GET /billing/pricing` y en el `creditCost` de `GET /states`. Lo que no se pudo consultar va en `skipped[]` y no se cobra.\n\n**Límite de peticiones.** 60 por minuto por API key, salvo acuerdo. Cada respuesta autenticada trae `RateLimit-Policy` y `RateLimit` (draft-ietf-httpapi-ratelimit-headers); un `429` trae además `Retry-After` en segundos.\n\n**Disponibilidad.** Servicio best-effort, sin SLA: depende de portales de gobierno que pueden fallar o cambiar sin aviso.",
    "termsOfService": "https://multasmx.com/terms",
    "contact": {
      "name": "Multas MX",
      "url": "https://multasmx.com/contact",
      "email": "contacto@multasmx.com"
    }
  },
  "externalDocs": {
    "description": "Documentación de la API",
    "url": "https://multasmx.com/docs"
  },
  "servers": [
    {
      "url": "https://api.multasmx.com",
      "description": "Producción"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Consultas",
      "description": "Pedir una consulta de placa y leer su resultado."
    },
    {
      "name": "Catálogo",
      "description": "Estados y servicios que se pueden consultar ahora."
    },
    {
      "name": "Créditos",
      "description": "Saldo, precios y costo por consulta."
    }
  ],
  "paths": {
    "/lookup": {
      "post": {
        "operationId": "createLookup",
        "tags": [
          "Consultas"
        ],
        "summary": "Consultar una placa en uno o varios estados y servicios",
        "description": "Pide la consulta de una placa (o de un NIV, sólo para `repuve`) en los estados y servicios de `states`. Responde de inmediato con un `requestId`; el resultado llega por webhook o con `GET /lookup/{requestId}`.\n\nEl cargo es la suma del costo de cada estado (ver `creditsBreakdown`) y se aplica antes de ejecutar la consulta. Si el saldo no alcanza, se rechaza completa con `402` y no se ejecuta nada. Los estados que no están dando servicio se devuelven en `skipped[]` y no se cobran.\n\nCon una placa de prueba (`PRUEBA-…`) la consulta es gratis, devuelve datos ficticios y trae `sandbox: true`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LookupRequest"
              },
              "example": {
                "plate": "PRUEBA-ADEUDOS",
                "states": [
                  "cdmx",
                  "edomex"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Consulta aceptada. Los estados quedan en `PENDING` y el resultado llega después.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupAccepted"
                },
                "example": {
                  "requestId": "9ce8ceb8-0b8f-4b1e-9d3a-6f0c2a7e5b11",
                  "status": "PROCESSING",
                  "plate": "PRUEBA-ADEUDOS",
                  "creditsCharged": 0,
                  "creditsBreakdown": {
                    "cdmx": 0,
                    "edomex": 0
                  },
                  "balance": {
                    "credits": 0,
                    "nextExpiry": null
                  },
                  "states": [
                    {
                      "stateCode": "cdmx",
                      "status": "PENDING"
                    },
                    {
                      "stateCode": "edomex",
                      "status": "PENDING"
                    }
                  ],
                  "skipped": [],
                  "sandbox": true
                }
              }
            }
          },
          "400": {
            "description": "Petición inválida. `error` dice cuál:\n- validación del body (`message` trae la lista de problemas);\n- `unknown_states`: algún `stateCode` no existe (`unknown` los lista);\n- `no_active_states`: ninguno de los estados pedidos está dando servicio (`skipped` dice por qué);\n- `plate_required`: `plate` es obligatoria para cualquier estado que no sea `repuve`;\n- `unknown_test_plate` / `test_identifier_mismatch`: placa o NIV de prueba que no existe, o mezclado con uno real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "unknown_states",
                  "unknown": [
                    "sonora"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "El saldo no cubre el costo de la consulta (`insufficient_credits`, con el desglose) o la prueba gratuita de una cuenta antigua expiró (`trial_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InsufficientCredits"
                    },
                    {
                      "$ref": "#/components/schemas/TrialExpired"
                    }
                  ]
                },
                "example": {
                  "error": "insufficient_credits",
                  "message": "Esta consulta cuesta 4 créditos y tu saldo disponible es de 2. Te faltan 2.",
                  "required": 4,
                  "available": 2,
                  "missing": 2,
                  "breakdown": {
                    "edomex": 1,
                    "repuve": 3
                  }
                }
              }
            }
          },
          "429": {
            "description": "Límite por minuto superado (`rate_limit_exceeded`, con `Retry-After`) o límite diario de consultas de prueba superado (`sandbox_limit_exceeded`).",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/RateLimitExceeded"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Las consultas de prueba no están disponibles en este momento (`sandbox_disabled`). Las consultas reales no se ven afectadas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "sandbox_disabled"
                }
              }
            }
          }
        }
      }
    },
    "/lookup/{requestId}": {
      "get": {
        "operationId": "getLookup",
        "tags": [
          "Consultas"
        ],
        "summary": "Leer el estado y el resultado de una consulta",
        "description": "Devuelve el estado de la consulta y el resultado de cada estado o servicio. Sirve para hacer polling si no usas webhooks, o para recuperar un resultado cuyo webhook no llegó. `status` pasa de `PROCESSING` a `COMPLETED` (todo salió bien), `PARTIAL` o `QUEUED_RETRY` (algún estado se reintenta solo). Las consultas de prueba se pueden leer durante 7 días.",
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "`requestId` que devolvió `POST /lookup`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Estado y resultados de la consulta.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "La consulta pertenece a otra cuenta (`not_owner`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 403,
                  "message": "not_owner",
                  "error": "Forbidden"
                }
              }
            }
          },
          "404": {
            "description": "No existe una consulta con ese id (`request_not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 404,
                  "message": "request_not_found",
                  "error": "Not Found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/states": {
      "get": {
        "operationId": "listStates",
        "tags": [
          "Catálogo"
        ],
        "summary": "Listar los estados y servicios que se pueden consultar ahora",
        "description": "Catálogo de `stateCode` que acepta `POST /lookup`, con su costo en créditos. Sólo aparecen los que están dando servicio en este momento: consúltalo antes de mandar una consulta en vez de guardar la lista. La respuesta puede traer campos adicionales que no forman parte del contrato.",
        "responses": {
          "200": {
            "description": "Catálogo vigente.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StateCatalog"
                },
                "example": {
                  "states": [
                    {
                      "stateCode": "cdmx",
                      "stateName": "Ciudad de México",
                      "type": "adeudos",
                      "creditCost": 1
                    },
                    {
                      "stateCode": "repuve",
                      "stateName": "REPUVE",
                      "type": "repuve",
                      "creditCost": 3
                    },
                    {
                      "stateCode": "tenencia-edomex",
                      "stateName": "Tenencia — Estado de México",
                      "type": "tenencia",
                      "creditCost": 1
                    }
                  ],
                  "count": 3
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/billing/balance": {
      "get": {
        "operationId": "getCreditBalance",
        "tags": [
          "Créditos"
        ],
        "summary": "Consultar el saldo de créditos y su caducidad",
        "description": "Saldo disponible y lotes vigentes. Los créditos vencen 12 meses después de la compra y se consumen primero los que vencen antes; `nextExpiry` es el próximo lote en vencer (`null` sin saldo).",
        "responses": {
          "200": {
            "description": "Saldo y lotes vigentes.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditBalance"
                },
                "example": {
                  "credits": 147,
                  "nextExpiry": {
                    "credits": 47,
                    "expiresAt": "2027-03-14T00:00:00.000Z"
                  },
                  "expiryMonths": 12,
                  "lots": [
                    {
                      "id": "b3f1c2a4-5d6e-4f70-8a9b-0c1d2e3f4a5b",
                      "credits": 100,
                      "remaining": 47,
                      "source": "purchase",
                      "grantedAt": "2026-03-14T00:00:00.000Z",
                      "expiresAt": "2027-03-14T00:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/billing/pricing": {
      "get": {
        "operationId": "getPricing",
        "tags": [
          "Créditos"
        ],
        "summary": "Consultar paquetes de créditos y el costo de cada consulta",
        "description": "Paquetes de créditos que se pueden comprar (precio en MXN) y cuántos créditos cuesta cada tipo de consulta. No requiere API key.",
        "security": [],
        "responses": {
          "200": {
            "description": "Precios vigentes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pricing"
                },
                "example": {
                  "packages": [
                    {
                      "key": "starter",
                      "credits": 100,
                      "priceMxn": 390,
                      "perCredit": 3.9
                    },
                    {
                      "key": "growth",
                      "credits": 500,
                      "priceMxn": 1500,
                      "perCredit": 3
                    },
                    {
                      "key": "business",
                      "credits": 2000,
                      "priceMxn": 5000,
                      "perCredit": 2.5
                    }
                  ],
                  "creditCosts": [
                    {
                      "query": "multa_estatal",
                      "label": "Multa estatal",
                      "credits": 1
                    },
                    {
                      "query": "repuve",
                      "label": "REPUVE",
                      "credits": 3
                    }
                  ],
                  "expiryMonths": 12
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "stateCompleted": {
      "post": {
        "operationId": "onStateCompleted",
        "summary": "Terminó un estado de una consulta con varios estados",
        "description": "Sólo en consultas de más de un estado: un aviso por estado conforme termina (con éxito o en reintento). Se envía a cada URL de webhook de la cuenta (hasta 3).",
        "parameters": [
          {
            "name": "X-Multas-Signature",
            "in": "header",
            "required": true,
            "description": "HMAC-SHA256 en hexadecimal del body crudo, con el `webhookSecret` de la cuenta. Verifícala con una comparación de tiempo constante antes de procesar.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-Multas-Request-Id",
            "in": "header",
            "required": true,
            "description": "`requestId` de la consulta.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Multas-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "state_completed"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StateCompletedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Cualquier 2xx confirma la entrega. Si tu endpoint falla se reintenta 3 veces (0 s, 2 s y 6 s); después el resultado sigue disponible en `GET /lookup/{requestId}`."
          }
        }
      }
    },
    "lookupCompleted": {
      "post": {
        "operationId": "onLookupCompleted",
        "summary": "Terminó la consulta completa",
        "description": "Aviso final con todos los resultados y un `summary`. Si después un estado en reintento se resuelve, llega otro `completed` con los resultados actualizados para el mismo `requestId`.",
        "parameters": [
          {
            "name": "X-Multas-Signature",
            "in": "header",
            "required": true,
            "description": "HMAC-SHA256 en hexadecimal del body crudo, con el `webhookSecret` de la cuenta. Verifícala con una comparación de tiempo constante antes de procesar.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-Multas-Request-Id",
            "in": "header",
            "required": true,
            "description": "`requestId` de la consulta.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Multas-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "completed"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LookupCompletedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Cualquier 2xx confirma la entrega. Si tu endpoint falla se reintenta 3 veces (0 s, 2 s y 6 s); después el resultado sigue disponible en `GET /lookup/{requestId}`."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "API key de la cuenta, del dashboard (https://dashboard.multasmx.com). Se envía en cada petición."
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "Cuota de la API key (draft-ietf-httpapi-ratelimit-headers): `q` peticiones por ventana fija de `w` segundos. Ejemplo: `\"api-key\";q=60;w=60`.",
        "schema": {
          "type": "string",
          "examples": [
            "\"api-key\";q=60;w=60"
          ]
        }
      },
      "RateLimit": {
        "description": "Estado de la cuota (draft-ietf-httpapi-ratelimit-headers): `r` peticiones restantes en la ventana y `t` segundos para que se reinicie. Ejemplo: `\"api-key\";r=59;t=42`.",
        "schema": {
          "type": "string",
          "examples": [
            "\"api-key\";r=59;t=42"
          ]
        }
      },
      "Retry-After": {
        "description": "Segundos que hay que esperar antes de reintentar.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "examples": [
            23
          ]
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Falta el header `x-api-key` o la API key no es válida.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "statusCode": 401,
              "message": "Invalid API key",
              "error": "Unauthorized"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Superaste el límite de peticiones por minuto de tu API key (`rate_limit_exceeded`). Espera los segundos de `Retry-After` antes de reintentar.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RateLimitExceeded"
            },
            "example": {
              "error": "rate_limit_exceeded",
              "message": "max 60 req/min",
              "retryAfterSeconds": 23
            }
          }
        }
      }
    },
    "schemas": {
      "LookupRequest": {
        "type": "object",
        "description": "`plate` es obligatoria salvo que se consulte sólo `repuve`, que acepta `niv` sin placa.",
        "properties": {
          "plate": {
            "type": "string",
            "minLength": 1,
            "maxLength": 16,
            "pattern": "^[A-Za-z0-9-]+$",
            "description": "Placa del vehículo, sin espacios. Se normaliza a mayúsculas. Las que empiezan con `PRUEBA` son placas de prueba.",
            "examples": [
              "PRUEBA-ADEUDOS"
            ]
          },
          "niv": {
            "type": "string",
            "minLength": 11,
            "maxLength": 17,
            "pattern": "^[A-HJ-NPR-Za-hj-npr-z0-9]+$",
            "description": "NIV / VIN (número de serie). Sólo lo usa `repuve`; permite consultar vehículos sin placa."
          },
          "states": {
            "type": "array",
            "minItems": 1,
            "maxItems": 64,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "examples": [
                "edomex"
              ]
            },
            "description": "`stateCode` a consultar, tal como los lista `GET /states` (estados de multas como `cdmx` o `edomex`, y servicios como `tenencia-edomex`, `vigencia-edomex`, `verificacion-edomex` o `repuve`)."
          }
        },
        "required": [
          "states"
        ],
        "additionalProperties": false
      },
      "RequestStatus": {
        "type": "string",
        "enum": [
          "PENDING",
          "PROCESSING",
          "COMPLETED",
          "PARTIAL",
          "QUEUED_RETRY"
        ],
        "description": "`COMPLETED`: todos los estados terminaron bien. `PARTIAL` / `QUEUED_RETRY`: alguno quedó en reintento y se reintentará solo."
      },
      "StateResultStatus": {
        "type": "string",
        "enum": [
          "PENDING",
          "PROCESSING",
          "SUCCESS",
          "QUEUED_RETRY",
          "FAILED"
        ],
        "description": "`SUCCESS`: hay resultado. `QUEUED_RETRY`: el servicio no respondió y se reintenta solo. `FAILED`: se agotaron los reintentos; el crédito se devuelve."
      },
      "NextExpiry": {
        "type": "object",
        "description": "Lote de créditos que vence antes (el próximo que se consume).",
        "properties": {
          "credits": {
            "type": "integer",
            "minimum": 0
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "credits",
          "expiresAt"
        ]
      },
      "LookupAccepted": {
        "type": "object",
        "properties": {
          "requestId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/RequestStatus"
          },
          "plate": {
            "type": "string",
            "description": "Placa consultada (o el NIV, si se consultó sólo por NIV)."
          },
          "creditsCharged": {
            "type": "integer",
            "minimum": 0
          },
          "creditsBreakdown": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Créditos cobrados por cada `stateCode`."
          },
          "balance": {
            "type": "object",
            "description": "Saldo después del cargo.",
            "properties": {
              "credits": {
                "type": "integer",
                "minimum": 0
              },
              "nextExpiry": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/NextExpiry"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "credits",
              "nextExpiry"
            ]
          },
          "states": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "stateCode": {
                  "type": "string"
                },
                "status": {
                  "$ref": "#/components/schemas/StateResultStatus"
                }
              },
              "required": [
                "stateCode",
                "status"
              ]
            }
          },
          "skipped": {
            "type": "array",
            "description": "Estados pedidos que no están dando servicio ahora. No se consultan ni se cobran.",
            "items": {
              "type": "object",
              "properties": {
                "stateCode": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                }
              },
              "required": [
                "stateCode",
                "reason"
              ]
            }
          },
          "sandbox": {
            "type": "boolean",
            "const": true,
            "description": "Sólo en consultas con placas de prueba."
          }
        },
        "required": [
          "requestId",
          "status",
          "plate",
          "creditsCharged",
          "creditsBreakdown",
          "balance",
          "states",
          "skipped"
        ]
      },
      "LookupDetail": {
        "type": "object",
        "properties": {
          "requestId": {
            "type": "string",
            "format": "uuid"
          },
          "plate": {
            "type": "string"
          },
          "niv": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "$ref": "#/components/schemas/RequestStatus"
          },
          "creditsCharged": {
            "type": "integer",
            "minimum": 0
          },
          "webhookSent": {
            "type": "boolean",
            "description": "Si al menos una URL de webhook aceptó el aviso final."
          },
          "webhookSentAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "startedAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "completedAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StateResult"
            }
          },
          "sandbox": {
            "type": "boolean",
            "const": true,
            "description": "Sólo en consultas con placas de prueba."
          }
        },
        "required": [
          "requestId",
          "plate",
          "status",
          "creditsCharged",
          "results"
        ]
      },
      "StateResult": {
        "type": "object",
        "description": "Resultado de un estado o servicio dentro de la consulta.",
        "properties": {
          "state": {
            "type": "string",
            "description": "`stateCode` del resultado."
          },
          "status": {
            "$ref": "#/components/schemas/StateResultStatus"
          },
          "hasInfractions": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Si hay infracciones. `null` en servicios que no son de multas (tenencia, vigencia, verificación, REPUVE) o sin resultado."
          },
          "totalDebt": {
            "type": [
              "string",
              "null"
            ],
            "description": "Total de adeudos de multas como cadena decimal (p. ej. `\"905\"`). `null` en servicios que no son de multas."
          },
          "data": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ResultData"
              },
              {
                "type": "null"
              }
            ]
          },
          "errorCode": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "`scrape_failed` si el servicio no dio resultado; `null` si lo dio."
          },
          "errorMessage": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "`service_unavailable` (no respondió), `unexpected_response` (respondió algo que no se pudo leer) o `plate_not_supported` (el servicio no acepta esa placa). `null` si hubo resultado."
          },
          "consultedAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "description": "Cuándo se respondió este resultado."
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "state",
          "status"
        ]
      },
      "WebhookStateResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/StateResult"
          },
          {
            "type": "object",
            "properties": {
              "retryable": {
                "type": "boolean",
                "description": "`true` si el estado quedó en reintento (`QUEUED_RETRY`)."
              }
            },
            "required": [
              "retryable"
            ]
          }
        ]
      },
      "ResultData": {
        "description": "Forma según el servicio. Los de forma propia traen `source`; los de multas (y las tenencias de Chihuahua, Guanajuato y Aguascalientes) usan `FinesData`. Detalle en https://multasmx.com/docs#multas.",
        "anyOf": [
          {
            "$ref": "#/components/schemas/FinesData"
          },
          {
            "$ref": "#/components/schemas/PlateValidityData"
          },
          {
            "$ref": "#/components/schemas/EdomexInspectionData"
          },
          {
            "$ref": "#/components/schemas/JaliscoInspectionData"
          },
          {
            "$ref": "#/components/schemas/EdomexVehicleTaxData"
          },
          {
            "$ref": "#/components/schemas/RepuveData"
          }
        ]
      },
      "Infraction": {
        "type": "object",
        "description": "Una infracción, pendiente o pagada. Los campos opcionales sólo aparecen cuando el portal del estado publica ese dato.",
        "properties": {
          "folio": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "description": "Fecha ISO 8601."
          },
          "amount": {
            "type": "number"
          },
          "unit": {
            "type": "string",
            "default": "MXN",
            "description": "Unidad del importe: `MXN` por defecto; CDMX usa `UC`."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid"
            ]
          },
          "type": {
            "type": "string",
            "description": "Clasificación del portal (programa y conducta)."
          },
          "article": {
            "type": "string",
            "description": "Artículo y fracción; si son varios, unidos con `\"; \"`."
          },
          "description": {
            "type": "string",
            "description": "Texto del reglamento, tal como lo publica el portal."
          },
          "articles": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "article": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                }
              },
              "required": [
                "article"
              ]
            }
          },
          "regulation": {
            "type": "string",
            "description": "Ámbito del reglamento."
          },
          "paymentDate": {
            "type": "string",
            "description": "Fecha de pago, si está pagada."
          }
        },
        "required": [
          "date",
          "amount",
          "unit",
          "status"
        ]
      },
      "Debt": {
        "type": "object",
        "description": "Un adeudo vehicular (p. ej. tenencia o derechos).",
        "properties": {
          "concept": {
            "type": "string"
          },
          "year": {
            "type": "integer"
          },
          "amount": {
            "type": "number"
          },
          "unit": {
            "type": "string",
            "default": "MXN"
          },
          "surcharges": {
            "type": "number"
          },
          "total": {
            "type": "number"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid"
            ]
          }
        },
        "required": [
          "concept",
          "amount",
          "unit",
          "total",
          "status"
        ]
      },
      "FinesData": {
        "type": "object",
        "description": "Resultado de un estado de multas (y de `tenencia-chihuahua`, `tenencia-guanajuato` y `tenencia-aguascalientes`, con el adeudo en `debts[]`). Ver https://multasmx.com/docs#multas y https://multasmx.com/docs#extra.",
        "properties": {
          "state": {
            "type": "string"
          },
          "plate": {
            "type": "string"
          },
          "consultedAt": {
            "type": "string",
            "format": "date-time"
          },
          "vehicle": {
            "type": "object",
            "properties": {
              "brand": {
                "type": "string"
              },
              "model": {
                "type": "string"
              },
              "year": {
                "type": "integer"
              },
              "color": {
                "type": "string"
              }
            }
          },
          "hasInfractions": {
            "type": "boolean"
          },
          "infractions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Infraction"
            }
          },
          "totalInfractionDebt": {
            "type": "number"
          },
          "hasDebts": {
            "type": "boolean"
          },
          "debts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Debt"
            }
          },
          "totalDebtAmount": {
            "type": "number"
          },
          "totalDebt": {
            "type": "number"
          },
          "totalPendingDebt": {
            "type": "number"
          },
          "extra": {
            "type": "object",
            "description": "Datos adicionales que publica el portal. Sólo las claves documentadas forman parte del contrato.",
            "properties": {
              "fotocivicaPoints": {
                "type": "integer",
                "minimum": 0,
                "maximum": 10,
                "description": "CDMX: puntos de fotocívicas de la placa."
              },
              "circulationCardStatus": {
                "type": "string",
                "description": "CDMX: `Vigente`, `Vencida` o `Placa no localizada`."
              },
              "circulationCardValidUntil": {
                "type": "string",
                "format": "date",
                "description": "CDMX: fin de vigencia de la tarjeta de circulación."
              },
              "plateRegistryStatus": {
                "type": "string",
                "enum": [
                  "deregistered",
                  "not_registered"
                ],
                "description": "Sinaloa y tenencias estatales: placa dada de baja o fuera del padrón. No significa \"al corriente\"."
              },
              "plateRegistryMessage": {
                "type": "string",
                "description": "Aviso del portal, tal cual."
              }
            },
            "additionalProperties": true
          }
        },
        "required": [
          "hasInfractions",
          "infractions",
          "hasDebts",
          "debts",
          "totalDebt"
        ]
      },
      "PlateValidityData": {
        "type": "object",
        "description": "`vigencia-edomex`: vigencia de la placa en el Estado de México. Ver https://multasmx.com/docs#vigencia.",
        "properties": {
          "source": {
            "type": "string",
            "const": "edomex-controlv"
          },
          "plate": {
            "type": "string"
          },
          "found": {
            "type": "boolean",
            "description": "`false` si la placa no está en el padrón (respuesta definitiva, no un error)."
          },
          "plateStatus": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "VIGENTE",
              "VENCIDA",
              "INACTIVA",
              "EN_PROCESO",
              null
            ]
          },
          "validityStart": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "validityEnd": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "previousPlate": {
            "type": [
              "string",
              "null"
            ]
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "source",
          "found"
        ]
      },
      "EdomexInspectionData": {
        "type": "object",
        "description": "`verificacion-edomex`: verificación vehicular del semestre en el Estado de México. Ver https://multasmx.com/docs#verificacion.",
        "properties": {
          "source": {
            "type": "string",
            "const": "edomex-verificacion"
          },
          "plate": {
            "type": "string"
          },
          "found": {
            "type": "boolean",
            "description": "`false` si no hay verificaciones registradas (respuesta definitiva, no un error)."
          },
          "verifiedThisSemester": {
            "type": "boolean"
          },
          "verification": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "center": {
                    "type": "string"
                  },
                  "date": {
                    "type": "string",
                    "format": "date"
                  },
                  "certificate": {
                    "type": "string",
                    "description": "Holograma, tal como lo publica el portal (\"Cero\", \"Doble Cero\", …)."
                  },
                  "reason": {
                    "type": "string"
                  },
                  "folio": {
                    "type": "string"
                  },
                  "vin": {
                    "type": "string"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "source",
          "found"
        ]
      },
      "JaliscoInspectionData": {
        "type": "object",
        "description": "`verificacion-jalisco`: verificación vehicular registrada en Jalisco. Ver https://multasmx.com/docs#verificacion-jalisco.",
        "properties": {
          "source": {
            "type": "string",
            "const": "jalisco-verificacion"
          },
          "plate": {
            "type": "string"
          },
          "consultedAt": {
            "type": "string",
            "format": "date-time"
          },
          "found": {
            "type": "boolean",
            "description": "`false` si no hay verificación registrada. No significa que el vehículo no haya verificado."
          },
          "current": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` si está aprobada y su `validUntil` no ha pasado."
          },
          "verification": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "result": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "\"APROBADO\" o \"NO APROBADO\"."
                  },
                  "date": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date"
                  },
                  "validUntil": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^\\d{4}-\\d{2}$",
                    "description": "Mes hasta el que es válida, `AAAA-MM`."
                  },
                  "type": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "center": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "vehicle": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "brand": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "model": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "year": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "fuel": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "source",
          "found"
        ]
      },
      "EdomexVehicleTaxData": {
        "type": "object",
        "description": "`tenencia-edomex`: adeudo de tenencia en el Estado de México. Ver https://multasmx.com/docs#tenencia.",
        "properties": {
          "source": {
            "type": "string",
            "const": "edomex-tenencia"
          },
          "plate": {
            "type": "string"
          },
          "found": {
            "type": "boolean",
            "description": "`false` si la placa no está en el padrón o está dada de baja."
          },
          "plateStatus": {
            "type": "string",
            "enum": [
              "registered",
              "not_registered",
              "deregistered"
            ]
          },
          "hasDebt": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "debtDescription": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalDebt": {
            "type": [
              "number",
              "null"
            ],
            "description": "Adeudo total en MXN."
          },
          "debtYears": {
            "type": [
              "array",
              "null"
            ],
            "items": {}
          },
          "vehicle": {
            "type": [
              "object",
              "null"
            ],
            "description": "Modelo, descripción, NIV, clave vehicular, cilindros y factura.",
            "additionalProperties": true
          },
          "plateValidityStart": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "plateValidityEnd": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "source",
          "found"
        ]
      },
      "RepuveData": {
        "type": "object",
        "description": "`repuve`: inscripción en el Registro Público Vehicular y reportes de robo (FGJ, OCRA, Robo USA/CAN). Ver https://multasmx.com/docs#repuve.",
        "properties": {
          "source": {
            "type": "string",
            "const": "repuve"
          },
          "queriedBy": {
            "type": "string",
            "enum": [
              "plate",
              "niv"
            ]
          },
          "registered": {
            "type": "boolean",
            "description": "`false` si el vehículo no está inscrito (respuesta definitiva, no un error)."
          },
          "hasTheftReport": {
            "type": "boolean",
            "description": "`true` si alguna de las fuentes reporta robo."
          },
          "vehicle": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "fgj": {
            "$ref": "#/components/schemas/TheftSource"
          },
          "ocra": {
            "$ref": "#/components/schemas/TheftSource"
          },
          "roboUsaCan": {
            "$ref": "#/components/schemas/TheftSource"
          }
        },
        "required": [
          "source",
          "registered",
          "hasTheftReport"
        ]
      },
      "TheftSource": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "con_reporte",
              "sin_reporte",
              "requiere_niv",
              "desconocido"
            ],
            "description": "`desconocido` significa que esa fuente no respondió: es dato ausente, no un vehículo limpio."
          },
          "records": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        },
        "required": [
          "status"
        ]
      },
      "StateCatalogEntry": {
        "type": "object",
        "properties": {
          "stateCode": {
            "type": "string",
            "description": "Código que va en `states[]` de `POST /lookup`."
          },
          "stateName": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "Tipo de consulta: `tenencia`, `vigencia`, `verificacion` o `repuve`; en los estados de multas varía (p. ej. `infracciones`)."
          },
          "creditCost": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": [
          "stateCode",
          "stateName",
          "type",
          "creditCost"
        ]
      },
      "StateCatalog": {
        "type": "object",
        "properties": {
          "states": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StateCatalogEntry"
            }
          },
          "count": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "states",
          "count"
        ]
      },
      "CreditLot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "credits": {
            "type": "integer",
            "minimum": 0
          },
          "remaining": {
            "type": "integer",
            "minimum": 0
          },
          "source": {
            "type": "string",
            "description": "Origen del lote (p. ej. `purchase`)."
          },
          "grantedAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "credits",
          "remaining",
          "expiresAt"
        ]
      },
      "CreditBalance": {
        "type": "object",
        "properties": {
          "credits": {
            "type": "integer",
            "minimum": 0
          },
          "nextExpiry": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/NextExpiry"
              },
              {
                "type": "null"
              }
            ]
          },
          "expiryMonths": {
            "type": "integer"
          },
          "lots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreditLot"
            }
          }
        },
        "required": [
          "credits",
          "nextExpiry",
          "expiryMonths",
          "lots"
        ]
      },
      "Pricing": {
        "type": "object",
        "properties": {
          "packages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "examples": [
                    "starter"
                  ]
                },
                "credits": {
                  "type": "integer"
                },
                "priceMxn": {
                  "type": "number"
                },
                "perCredit": {
                  "type": "number",
                  "description": "Precio por crédito en MXN."
                }
              },
              "required": [
                "key",
                "credits",
                "priceMxn",
                "perCredit"
              ]
            }
          },
          "creditCosts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "query": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "credits": {
                  "type": "integer"
                }
              },
              "required": [
                "query",
                "label",
                "credits"
              ]
            }
          },
          "expiryMonths": {
            "type": "integer"
          }
        },
        "required": [
          "packages",
          "creditCosts",
          "expiryMonths"
        ]
      },
      "StateCompletedEvent": {
        "type": "object",
        "properties": {
          "requestId": {
            "type": "string",
            "format": "uuid"
          },
          "plate": {
            "type": "string"
          },
          "niv": {
            "type": [
              "string",
              "null"
            ]
          },
          "event": {
            "type": "string",
            "const": "state_completed"
          },
          "status": {
            "type": "string",
            "const": "PROCESSING"
          },
          "completedStates": {
            "type": "integer"
          },
          "totalStates": {
            "type": "integer"
          },
          "stateResult": {
            "$ref": "#/components/schemas/WebhookStateResult"
          },
          "sandbox": {
            "type": "boolean",
            "const": true
          }
        },
        "required": [
          "requestId",
          "plate",
          "niv",
          "event",
          "status",
          "completedStates",
          "totalStates",
          "stateResult"
        ]
      },
      "LookupCompletedEvent": {
        "type": "object",
        "properties": {
          "requestId": {
            "type": "string",
            "format": "uuid"
          },
          "plate": {
            "type": "string"
          },
          "niv": {
            "type": [
              "string",
              "null"
            ]
          },
          "event": {
            "type": "string",
            "const": "completed"
          },
          "status": {
            "$ref": "#/components/schemas/RequestStatus"
          },
          "completedStates": {
            "type": "integer"
          },
          "totalStates": {
            "type": "integer"
          },
          "completedAt": {
            "type": "string",
            "format": "date-time"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookStateResult"
            }
          },
          "summary": {
            "type": "object",
            "properties": {
              "successful": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "totalInfractions": {
                "type": "integer"
              },
              "totalDebt": {
                "type": "number",
                "description": "Suma numérica de `totalDebt`; sólo tiene sentido si todos los importes comparten unidad."
              },
              "failedStates": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "repuve": {
                "type": "object",
                "properties": {
                  "registered": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  },
                  "hasTheftReport": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  }
                }
              }
            },
            "required": [
              "successful",
              "failed",
              "totalInfractions",
              "totalDebt",
              "failedStates"
            ]
          },
          "sandbox": {
            "type": "boolean",
            "const": true
          }
        },
        "required": [
          "requestId",
          "plate",
          "niv",
          "event",
          "status",
          "completedStates",
          "totalStates",
          "completedAt",
          "results",
          "summary"
        ]
      },
      "Error": {
        "type": "object",
        "description": "Cuerpo de error. `error` trae un código estable cuando existe; `message` puede ser texto o, en errores de validación, una lista.",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "statusCode": {
            "type": "integer"
          }
        },
        "additionalProperties": true
      },
      "InsufficientCredits": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "const": "insufficient_credits"
          },
          "message": {
            "type": "string"
          },
          "required": {
            "type": "integer"
          },
          "available": {
            "type": "integer"
          },
          "missing": {
            "type": "integer"
          },
          "breakdown": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          }
        },
        "required": [
          "error",
          "required",
          "available",
          "missing",
          "breakdown"
        ]
      },
      "TrialExpired": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "const": "trial_expired"
          },
          "message": {
            "type": "string"
          },
          "trialExpiredAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "error"
        ]
      },
      "RateLimitExceeded": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "const": "rate_limit_exceeded"
          },
          "message": {
            "type": "string"
          },
          "retryAfterSeconds": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "error",
          "retryAfterSeconds"
        ]
      }
    }
  }
}
