{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "EORICheck API",
    "version": "0.3.0",
    "description": "Validate EORI numbers against the registry selected by their prefix (HMRC for GB; TAXUD for XI and supported European prefixes), validate VAT numbers through VIES, and inspect API-key usage. Validation results can come from the live authority or the service cache and are not an official certificate or legal advice.",
    "contact": {
      "name": "EORICheck support",
      "email": "hello@eoricheck.com"
    }
  },
  "servers": [
    {
      "url": "https://eoricheck.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "API keys",
      "description": "Self-service Free key issuance."
    },
    {
      "name": "Validation",
      "description": "Metered EORI and VAT validation."
    },
    {
      "name": "Usage",
      "description": "Unmetered usage inspection for the authenticated key."
    }
  ],
  "paths": {
    "/api/keys": {
      "post": {
        "tags": [
          "API keys"
        ],
        "operationId": "issueFreeApiKey",
        "summary": "Issue a Free API key",
        "description": "Issues one Free key for a valid email address. An email that already owns a key is rejected because existing key secrets are not displayed again. Requests are throttled by network address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyIssuanceRequest"
              },
              "example": {
                "email": "developer@example.com"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "A Free API key was created. The key secret is returned in this response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyIssuanceResponse"
                },
                "example": {
                  "api_key": "ek_0123456789abcdef0123456789abcdef01234567",
                  "key_id": "01234567",
                  "email": "developer@example.com",
                  "tier": "free",
                  "monthly_quota": 100,
                  "rate_per_minute": 10,
                  "reused": false,
                  "docs": "https://eoricheck.com/eori-validation-api"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "description": "The email already has a key, or its key identity cannot be issued again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "Unable to issue a key for this email. Existing keys are never displayed again."
                }
              }
            }
          },
          "429": {
            "description": "Too many key-issuance requests came from the same network.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "Too many keys requested from this network. Try again later."
                }
              }
            }
          },
          "502": {
            "description": "The website could not reach or read the key-issuance service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "Could not issue a key: Connectivity error."
                }
              }
            }
          },
          "503": {
            "description": "Key issuance is not configured or its storage is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notConfigured": {
                    "value": {
                      "error": "Key issuance is being activated. Leave your email and we'll send a key."
                    }
                  },
                  "temporarilyUnavailable": {
                    "value": {
                      "error": "Key issuance is temporarily unavailable."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/eori": {
      "get": {
        "tags": [
          "Validation"
        ],
        "operationId": "validateEori",
        "summary": "Validate an EORI number",
        "description": "Normalizes spaces and hyphens, then routes GB prefixes to HMRC and XI or supported European prefixes to TAXUD. A successful validation consumes one monthly call. Format errors and upstream failures release the monthly reservation, although the per-minute token remains consumed.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "number",
            "in": "query",
            "required": true,
            "description": "EORI number with a supported two-letter prefix. Spaces and hyphens are ignored.",
            "schema": {
              "type": "string",
              "minLength": 3
            },
            "example": "IT00159560366"
          }
        ],
        "responses": {
          "200": {
            "description": "The selected registry returned a non-error result. `estado` reports whether the number is valid, invalid, not found, or outside that registry's coverage.",
            "headers": {
              "X-Plan": {
                "$ref": "#/components/headers/XPlan"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/XQuotaLimit"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/XQuotaUsed"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/XQuotaRemaining"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EoriResult"
                },
                "example": {
                  "eori": "IT00159560366",
                  "country": "IT",
                  "estado": "valido",
                  "status_raw": "Valid",
                  "request_date": "08/07/2026",
                  "fuente": "taxud",
                  "checked_at": "2026-07-08T04:11:41.539Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "422": {
            "$ref": "#/components/responses/ValidationUnprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "HMRC or TAXUD returned an error, could not be reached, or returned an uninterpretable response. The monthly reservation is released.",
            "headers": {
              "X-Plan": {
                "$ref": "#/components/headers/XPlan"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/XQuotaLimit"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/XQuotaUsed"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/XQuotaRemaining"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/EoriLookupError"
                    }
                  ]
                },
                "examples": {
                  "serviceUnavailable": {
                    "value": {
                      "error": "The official EORI register is temporarily unavailable."
                    }
                  },
                  "registryError": {
                    "value": {
                      "eori": "IT00159560366",
                      "country": "IT",
                      "estado": "error",
                      "fuente": "error",
                      "checked_at": "2026-07-08T04:11:41.539Z",
                      "error": "Unexpected response from TAXUD EOS service."
                    }
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/MeteringUnavailable"
          }
        }
      }
    },
    "/v1/vat": {
      "get": {
        "tags": [
          "Validation"
        ],
        "operationId": "validateVat",
        "summary": "Validate an EU VAT number",
        "description": "Validates a VAT number through VIES. Supply a prefixed number, or a bare number together with `country`. Greece is normalized from GR to the VIES code EL. A successful validation consumes one monthly call. Format errors and upstream failures release the monthly reservation, although the per-minute token remains consumed.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "number",
            "in": "query",
            "required": true,
            "description": "VAT number including its two-letter member-state prefix, unless `country` is also supplied. Spaces, periods, and hyphens are ignored.",
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "example": "IE6388047V"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Two-letter member-state code used when `number` has no prefix. GR is accepted as an alias for EL.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            },
            "example": "IE"
          }
        ],
        "responses": {
          "200": {
            "description": "VIES returned a non-error result. Trader name and address are present only when the registry supplies them.",
            "headers": {
              "X-Plan": {
                "$ref": "#/components/headers/XPlan"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/XQuotaLimit"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/XQuotaUsed"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/XQuotaRemaining"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VatResult"
                },
                "example": {
                  "vat": "IE6388047V",
                  "country": "IE",
                  "number": "6388047V",
                  "estado": "valido",
                  "name": "GOOGLE IRELAND LIMITED",
                  "address": "3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4",
                  "fuente": "vies",
                  "checked_at": "2026-07-08T04:11:41.940Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "422": {
            "$ref": "#/components/responses/ValidationUnprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "VIES returned an error, could not be reached, or returned an uninterpretable response. The monthly reservation is released.",
            "headers": {
              "X-Plan": {
                "$ref": "#/components/headers/XPlan"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/XQuotaLimit"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/XQuotaUsed"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/XQuotaRemaining"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/VatLookupError"
                    }
                  ]
                },
                "examples": {
                  "serviceUnavailable": {
                    "value": {
                      "error": "VIES validation is temporarily unavailable."
                    }
                  },
                  "registryError": {
                    "value": {
                      "vat": "IE6388047V",
                      "country": "IE",
                      "number": "6388047V",
                      "estado": "error",
                      "fuente": "error",
                      "checked_at": "2026-07-08T04:11:41.940Z",
                      "error": "VIES returned a response that could not be interpreted."
                    }
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/MeteringUnavailable"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Usage"
        ],
        "operationId": "getUsage",
        "summary": "Get current API-key usage",
        "description": "Returns the authenticated key's current calendar-month usage, quota, remaining calls, and per-minute limit. Reading usage does not consume quota.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current usage snapshot for the authenticated key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageSnapshot"
                },
                "example": {
                  "tier": "growth",
                  "period": "2026-07",
                  "used": 3412,
                  "quota": 25000,
                  "remaining": 21588,
                  "rate_per_minute": 120
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/UsageUnavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Send the key as `Authorization: Bearer ek_...`."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Send the API key in the `x-api-key` header."
      },
      "apiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "api_key",
        "description": "Legacy query-string fallback. Prefer a header so credentials are not placed in URLs."
      }
    },
    "headers": {
      "XPlan": {
        "description": "Plan attached to the authenticated key.",
        "schema": {
          "$ref": "#/components/schemas/Tier"
        }
      },
      "XQuotaLimit": {
        "description": "Monthly validation-call quota for the plan.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "XQuotaUsed": {
        "description": "Committed validation calls in the current period after this response is finalized.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "XQuotaRemaining": {
        "description": "Validation calls remaining in the current period.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "XRateLimitLimit": {
        "description": "Per-minute request limit for the plan.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The JSON body or email address is invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "Enter a valid email."
            }
          }
        }
      },
      "ValidationBadRequest": {
        "description": "The required `number` query parameter is missing.",
        "headers": {
          "X-Plan": {
            "$ref": "#/components/headers/XPlan"
          },
          "X-Quota-Limit": {
            "$ref": "#/components/headers/XQuotaLimit"
          },
          "X-Quota-Used": {
            "$ref": "#/components/headers/XQuotaUsed"
          },
          "X-Quota-Remaining": {
            "$ref": "#/components/headers/XQuotaRemaining"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "Missing 'number' query parameter."
            }
          }
        }
      },
      "ValidationUnprocessable": {
        "description": "The supplied number does not match the endpoint's accepted format.",
        "headers": {
          "X-Plan": {
            "$ref": "#/components/headers/XPlan"
          },
          "X-Quota-Limit": {
            "$ref": "#/components/headers/XQuotaLimit"
          },
          "X-Quota-Used": {
            "$ref": "#/components/headers/XQuotaUsed"
          },
          "X-Quota-Remaining": {
            "$ref": "#/components/headers/XQuotaRemaining"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "The supplied number has an invalid format."
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The API key is missing or invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "missing": {
                "value": {
                  "error": "Missing API key. Send it as 'Authorization: Bearer <key>' or 'x-api-key'."
                }
              },
              "invalid": {
                "value": {
                  "error": "Invalid API key."
                }
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The key is canceled or its monthly quota is exhausted.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "quotaReached": {
                "value": {
                  "error": "Monthly quota of 100 reached. Upgrade to continue."
                }
              },
              "canceled": {
                "value": {
                  "error": "This key is canceled. Reactivate a plan to continue."
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "The API key exceeded its plan's per-minute request limit.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the current fixed-minute window ends.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 60
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "Rate limit of 10/min exceeded."
            }
          }
        }
      },
      "MeteringUnavailable": {
        "description": "Usage metering is temporarily unavailable, so validation fails closed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "beforeValidation": {
                "value": {
                  "error": "Usage metering is temporarily unavailable. No validation was run; retry later."
                }
              },
              "duringFinalization": {
                "value": {
                  "error": "Usage metering is temporarily unavailable. Retry later."
                }
              }
            }
          }
        }
      },
      "UsageUnavailable": {
        "description": "Usage metering could not return a snapshot.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "Usage metering is temporarily unavailable. Retry later."
            }
          }
        }
      }
    },
    "schemas": {
      "Tier": {
        "type": "string",
        "enum": [
          "free",
          "starter",
          "growth",
          "scale"
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "KeyIssuanceRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          }
        }
      },
      "KeyIssuanceResponse": {
        "type": "object",
        "required": [
          "api_key",
          "key_id",
          "email",
          "tier",
          "monthly_quota",
          "rate_per_minute",
          "reused",
          "docs"
        ],
        "properties": {
          "api_key": {
            "type": "string",
            "pattern": "^ek_[a-f0-9]{40}$",
            "description": "The API-key secret. Store it when this response is received."
          },
          "key_id": {
            "type": "string",
            "pattern": "^[a-f0-9]{8}$"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "tier": {
            "$ref": "#/components/schemas/Tier"
          },
          "monthly_quota": {
            "type": "integer",
            "minimum": 1
          },
          "rate_per_minute": {
            "type": "integer",
            "minimum": 1
          },
          "reused": {
            "type": "boolean"
          },
          "docs": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "EoriResult": {
        "type": "object",
        "required": [
          "eori",
          "country",
          "estado",
          "fuente",
          "checked_at"
        ],
        "properties": {
          "eori": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "enum": [
              "AT",
              "BE",
              "BG",
              "HR",
              "CY",
              "CZ",
              "DK",
              "EE",
              "FI",
              "FR",
              "DE",
              "GR",
              "HU",
              "IE",
              "IT",
              "LV",
              "LT",
              "LU",
              "MT",
              "NL",
              "PL",
              "PT",
              "RO",
              "SK",
              "SI",
              "ES",
              "SE",
              "XI",
              "GB"
            ]
          },
          "estado": {
            "type": "string",
            "enum": [
              "valido",
              "no_valido",
              "no_encontrado",
              "no_cubierto"
            ]
          },
          "status_raw": {
            "type": "string"
          },
          "request_date": {
            "type": "string"
          },
          "fuente": {
            "type": "string",
            "enum": [
              "taxud",
              "hmrc",
              "kv-cache"
            ]
          },
          "registro": {
            "type": "string",
            "enum": [
              "taxud",
              "hmrc"
            ],
            "description": "Original authority retained when `fuente` is `kv-cache`."
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "string",
            "description": "Can explain a non-error `no_cubierto` result."
          },
          "empresa": {
            "$ref": "#/components/schemas/EoriCompany"
          },
          "autoridad_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "EoriCompany": {
        "type": "object",
        "properties": {
          "nombre": {
            "type": "string"
          },
          "direccion": {
            "type": "string"
          }
        }
      },
      "EoriLookupError": {
        "type": "object",
        "required": [
          "eori",
          "country",
          "estado",
          "fuente",
          "checked_at",
          "error"
        ],
        "properties": {
          "eori": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2
          },
          "estado": {
            "const": "error"
          },
          "fuente": {
            "const": "error"
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "VatResult": {
        "type": "object",
        "required": [
          "vat",
          "country",
          "number",
          "estado",
          "fuente",
          "checked_at"
        ],
        "properties": {
          "vat": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "enum": [
              "AT",
              "BE",
              "BG",
              "CY",
              "CZ",
              "DE",
              "DK",
              "EE",
              "EL",
              "ES",
              "FI",
              "FR",
              "HR",
              "HU",
              "IE",
              "IT",
              "LT",
              "LU",
              "LV",
              "MT",
              "NL",
              "PL",
              "PT",
              "RO",
              "SE",
              "SI",
              "SK",
              "XI"
            ]
          },
          "number": {
            "type": "string"
          },
          "estado": {
            "type": "string",
            "enum": [
              "valido",
              "no_valido",
              "no_encontrado"
            ]
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "request_date": {
            "type": "string"
          },
          "fuente": {
            "type": "string",
            "enum": [
              "vies",
              "kv-cache"
            ]
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VatLookupError": {
        "type": "object",
        "required": [
          "vat",
          "country",
          "number",
          "estado",
          "fuente",
          "checked_at",
          "error"
        ],
        "properties": {
          "vat": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2
          },
          "number": {
            "type": "string"
          },
          "estado": {
            "const": "error"
          },
          "fuente": {
            "const": "error"
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "UsageSnapshot": {
        "type": "object",
        "required": [
          "tier",
          "period",
          "used",
          "quota",
          "remaining",
          "rate_per_minute"
        ],
        "properties": {
          "tier": {
            "$ref": "#/components/schemas/Tier"
          },
          "period": {
            "type": "string",
            "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$",
            "description": "UTC calendar month in YYYY-MM form."
          },
          "used": {
            "type": "integer",
            "minimum": 0
          },
          "quota": {
            "type": "integer",
            "minimum": 1
          },
          "remaining": {
            "type": "integer",
            "minimum": 0
          },
          "rate_per_minute": {
            "type": "integer",
            "minimum": 1
          }
        }
      }
    }
  }
}
