{
  "openapi": "3.0.3",
  "info": {
    "title": "VatPulse - EU VAT & IBAN Validation API",
    "version": "1.0.0",
    "description": "Validate EU VAT numbers (VIES) and IBANs with caching, outage fallback and BIC lookup.\n\nValidate EU VAT numbers against the official **VIES** service (EU member states + Northern Ireland `XI`), and IBANs offline with mod-97 checksum, per-country structure and bank/BIC lookup.\n\n**Reliability:** successful VAT results are cached for 24 hours. When VIES or a member state is down, the API returns the last known good result (`stale: true` plus a `warning`) instead of failing; if nothing is cached you get an explicit `service_unavailable` / `member_state_unavailable` error.\n\n**Billing:** you only pay for fresh, definitive validations. Cache hits (`cached: true`), stale fallbacks, and errors (malformed input, upstream outages, auth, rate limits) never count against your quota. One request counts at most once, even if it validates both a VAT number and an IBAN.\n\n**Plans:** Free 100/month ($0/mo, 10 req/min) · Basic 2000/month ($19/mo, 60 req/min) · Pro 10000/month ($49/mo, 180 req/min).",
    "contact": {
      "name": "VatPulse",
      "url": "https://rapidapi.com/vatpulse/api/vatpulse1"
    },
    "termsOfService": "https://api.vatpulse.workers.dev/terms"
  },
  "servers": [
    {
      "url": "https://api.vatpulse.workers.dev"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Validation",
      "description": "VAT and IBAN validation"
    },
    {
      "name": "Account",
      "description": "Usage and quota"
    },
    {
      "name": "Status",
      "description": "Upstream availability"
    }
  ],
  "paths": {
    "/validate": {
      "post": {
        "tags": [
          "Validation"
        ],
        "operationId": "validate",
        "summary": "Validate a VAT number and/or an IBAN",
        "description": "Send `vat`, `iban`, or both. VAT numbers must include the 2-letter prefix (`DE123456789`, `EL...` or `GR...` for Greece, `XI...` for Northern Ireland). UK `GB...` numbers via HMRC are coming soon (built, not yet enabled); until then they return `service_unavailable` (not billed). Spaces, dots and dashes are ignored.\n\nReturns HTTP 200 whenever at least one component produced a definitive answer (`valid: true|false`). If no component could be answered, the HTTP status reflects the first component error (e.g. 503 when VIES is down and nothing is cached).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ValidateRequest"
              },
              "examples": {
                "both": {
                  "value": {
                    "vat": "IE6388047V",
                    "iban": "DE89 3704 0044 0532 0130 00"
                  }
                },
                "vatOnly": {
                  "value": {
                    "vat": "NL004495445B01"
                  }
                },
                "ibanOnly": {
                  "value": {
                    "iban": "GB33BUKB20201555555555"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation result",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed per minute"
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Quota-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Monthly billable validations (direct API keys only)"
              },
              "X-Quota-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Quota-Reset": {
                "schema": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateResponse"
                },
                "examples": {
                  "fresh": {
                    "summary": "Fresh VIES result + IBAN",
                    "value": {
                      "request_id": "8c1f0a2b3d4e5f60",
                      "vat": {
                        "input": "IE6388047V",
                        "valid": true,
                        "vat_number": "IE6388047V",
                        "country_code": "IE",
                        "country_name": "Ireland",
                        "name": "GOOGLE IRELAND LIMITED",
                        "address": "3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4",
                        "source": "vies",
                        "cached": false,
                        "stale": false,
                        "checked_at": "2026-10-17T22:44:26.350Z",
                        "consultation_number": null,
                        "error": null,
                        "warning": null
                      },
                      "iban": {
                        "input": "DE89 3704 0044 0532 0130 00",
                        "valid": true,
                        "iban": "DE89370400440532013000",
                        "iban_formatted": "DE89 3704 0044 0532 0130 00",
                        "country_code": "DE",
                        "check_digits": "89",
                        "bban": "370400440532013000",
                        "bank_code": "37040044",
                        "branch_code": null,
                        "account_number": "0532013000",
                        "sepa": true,
                        "bank": {
                          "name": "Commerzbank Köln",
                          "bic": "COBADEFFXXX"
                        },
                        "source": "offline",
                        "cached": false,
                        "error": null
                      },
                      "billable": true,
                      "usage": {
                        "plan": "free",
                        "used": 1,
                        "limit": 100,
                        "remaining": 99,
                        "reset_at": "2026-11-01T00:00:00.000Z"
                      }
                    }
                  },
                  "staleDuringOutage": {
                    "summary": "VIES down: last known good result",
                    "value": {
                      "request_id": "8c1f0a2b3d4e5f61",
                      "vat": {
                        "input": "DE811907980",
                        "valid": true,
                        "vat_number": "DE811907980",
                        "country_code": "DE",
                        "country_name": "Germany",
                        "name": null,
                        "address": null,
                        "source": "vies",
                        "cached": true,
                        "stale": true,
                        "checked_at": "2026-10-15T09:12:00.000Z",
                        "consultation_number": null,
                        "error": null,
                        "warning": {
                          "code": "member_state_unavailable",
                          "message": "The member state's VAT database is temporarily unavailable via VIES. Try again later. Serving the last successful result from 2026-10-15T09:12:00.000Z."
                        }
                      },
                      "billable": false,
                      "usage": {
                        "plan": "free",
                        "used": 1,
                        "limit": 100,
                        "remaining": 99,
                        "reset_at": "2026-11-01T00:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body or input (invalid_request, invalid_format, invalid_country, invalid_length).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "invalid_request",
                    "message": "Malformed request body or input (invalid_request, invalid_format, invalid_country, invalid_length)."
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "unauthorized",
                    "message": "Missing or invalid API key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "API key revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "forbidden",
                    "message": "API key revoked."
                  }
                }
              }
            }
          },
          "429": {
            "description": "Per-minute rate limit (rate_limited) or monthly quota (quota_exceeded) exceeded. See Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "rate_limited",
                    "message": "Per-minute rate limit (rate_limited) or monthly quota (quota_exceeded) exceeded. See Retry-After."
                  }
                }
              }
            }
          },
          "503": {
            "description": "VIES/HMRC or the member state is unavailable and no cached result exists (service_unavailable, member_state_unavailable, upstream_rate_limited).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "service_unavailable",
                    "message": "VIES/HMRC or the member state is unavailable and no cached result exists (service_unavailable, member_state_unavailable, upstream_rate_limited)."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/usage": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getUsage",
        "summary": "Current plan, usage and remaining quota",
        "description": "Does not count against your quota.",
        "responses": {
          "200": {
            "description": "Usage for the calling key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "unauthorized",
                    "message": "Missing or invalid API key."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/status": {
      "get": {
        "tags": [
          "Status"
        ],
        "operationId": "getStatus",
        "summary": "Live VIES availability per member state",
        "security": [],
        "responses": {
          "200": {
            "description": "Upstream status (cached 60s)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "VIES status endpoint unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "service_unavailable",
                    "message": "VIES status endpoint unreachable."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Status"
        ],
        "operationId": "health",
        "summary": "Liveness probe",
        "security": [],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Subscribe on RapidAPI (free plan: 100 validations/month) and call through the RapidAPI gateway with X-RapidAPI-Key / X-RapidAPI-Host. Direct X-API-Key keys are issued to direct customers on request."
      }
    },
    "schemas": {
      "ValidateRequest": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "vat": {
            "type": "string",
            "description": "VAT number with 2-letter country prefix.",
            "example": "IE6388047V",
            "maxLength": 40
          },
          "iban": {
            "type": "string",
            "description": "IBAN, with or without spaces.",
            "example": "DE89370400440532013000",
            "maxLength": 60
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "invalid_request",
              "invalid_format",
              "invalid_country",
              "invalid_length",
              "invalid_checksum",
              "service_unavailable",
              "member_state_unavailable",
              "upstream_rate_limited",
              "rate_limited",
              "quota_exceeded",
              "unauthorized",
              "forbidden",
              "not_found",
              "method_not_allowed",
              "internal_error"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/Error"
          }
        }
      },
      "VatResult": {
        "type": "object",
        "properties": {
          "input": {
            "type": "string"
          },
          "valid": {
            "type": "boolean",
            "description": "true/false when known; null when it could not be determined (see error).",
            "nullable": true
          },
          "vat_number": {
            "type": "string",
            "description": "Normalized number with prefix.",
            "nullable": true
          },
          "country_code": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 (GR for EL, GB for XI).",
            "nullable": true
          },
          "country_name": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Registered name, when the member state discloses it.",
            "nullable": true
          },
          "address": {
            "type": "string",
            "nullable": true
          },
          "source": {
            "type": "string",
            "enum": [
              "vies",
              "hmrc",
              "syntax",
              null
            ],
            "nullable": true
          },
          "cached": {
            "type": "boolean"
          },
          "stale": {
            "type": "boolean",
            "description": "true when served from the last-known-good copy during an upstream outage."
          },
          "checked_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the authority confirmed this result.",
            "nullable": true
          },
          "consultation_number": {
            "type": "string",
            "description": "VIES/HMRC consultation reference, when available.",
            "nullable": true
          },
          "error": {
            "nullable": true,
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/Error"
              }
            ]
          },
          "warning": {
            "nullable": true,
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/Error"
              }
            ]
          }
        }
      },
      "IbanResult": {
        "type": "object",
        "properties": {
          "input": {
            "type": "string"
          },
          "valid": {
            "type": "boolean",
            "nullable": true
          },
          "iban": {
            "type": "string",
            "description": "Electronic format (no spaces).",
            "nullable": true
          },
          "iban_formatted": {
            "type": "string",
            "description": "Print format (groups of 4).",
            "nullable": true
          },
          "country_code": {
            "type": "string",
            "nullable": true
          },
          "check_digits": {
            "type": "string",
            "nullable": true
          },
          "bban": {
            "type": "string",
            "nullable": true
          },
          "bank_code": {
            "type": "string",
            "nullable": true
          },
          "branch_code": {
            "type": "string",
            "nullable": true
          },
          "account_number": {
            "type": "string",
            "nullable": true
          },
          "sepa": {
            "type": "boolean",
            "nullable": true
          },
          "bank": {
            "description": "Bank name and BIC from the bundled registry; null when the bank code is not in our dataset.",
            "nullable": true,
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "bic": {
                "type": "string"
              }
            }
          },
          "source": {
            "type": "string",
            "enum": [
              "offline"
            ]
          },
          "cached": {
            "type": "boolean"
          },
          "error": {
            "nullable": true,
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/Error"
              }
            ]
          }
        }
      },
      "UsageBlock": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "basic",
              "pro",
              "rapidapi"
            ]
          },
          "used": {
            "type": "integer"
          },
          "limit": {
            "type": "integer",
            "nullable": true
          },
          "remaining": {
            "type": "integer",
            "nullable": true
          },
          "reset_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ValidateResponse": {
        "type": "object",
        "properties": {
          "request_id": {
            "type": "string"
          },
          "vat": {
            "$ref": "#/components/schemas/VatResult"
          },
          "iban": {
            "$ref": "#/components/schemas/IbanResult"
          },
          "billable": {
            "type": "boolean",
            "description": "Whether this request counted against your monthly quota."
          },
          "usage": {
            "$ref": "#/components/schemas/UsageBlock"
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "string"
          },
          "plan_name": {
            "type": "string"
          },
          "price_usd_per_month": {
            "type": "number"
          },
          "monthly_quota": {
            "type": "integer",
            "nullable": true
          },
          "used": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer",
            "nullable": true
          },
          "period_start": {
            "type": "string",
            "format": "date-time"
          },
          "reset_at": {
            "type": "string",
            "format": "date-time"
          },
          "rate_limit_per_minute": {
            "type": "integer"
          },
          "requests_this_month": {
            "type": "integer",
            "description": "All /validate requests incl. cache hits (not billed)."
          },
          "key_id": {
            "type": "string",
            "nullable": true
          }
        }
      }
    }
  }
}