{
  "openapi": "3.1.0",
  "info": {
    "title": "Jithox REST — EU VAT number check (VIES)",
    "version": "1.0.0",
    "summary": "Check EU VAT numbers against the EU VIES register from a backend, with a connection credential a person creates.",
    "description": "One operation. A person creates a Jithox connection at https://jithox.com/mcp/account#connection and gives its secret (jxc_live_…) to the backend, which sends it as a Bearer token. Price: 2 credits (EUR 0.02) per row VIES answered (valid or invalid), charged once per delivery from the workspace's prepaid credits; credits are added at https://jithox.com/plan. A row VIES did not answer (unavailable), a malformed or duplicate number, or a number VIES does not cover costs nothing. The answer carries the ledger booking id (billing.receiptRunId), which is not a signed receipt; no signed or hash-bound receipt is returned. Not legal or tax advice: VIES states whether a number is registered for intra-EU trade at the moment it answered.",
    "contact": {
      "url": "https://jithox.com/vat-list-check"
    }
  },
  "servers": [
    {
      "url": "https://jithox.com"
    }
  ],
  "paths": {
    "/api/v1/vat/verify": {
      "post": {
        "operationId": "verifyVat",
        "summary": "Check up to 20 EU VAT numbers against VIES",
        "description": "Send up to 20 rows; the answer has one row per input row, in order. Branch on rows[].verdict. The whole call is bounded at 40000 ms; rows VIES did not answer in time come back unavailable and are listed in result.retry.indexes. Re-sending the same rows with the same x-jithox-idempotency-key is charged once.",
        "security": [
          {
            "jithoxConnection": []
          }
        ],
        "parameters": [
          {
            "name": "x-jithox-idempotency-key",
            "in": "header",
            "required": false,
            "description": "Optional. 8–100 characters of A–Z a–z 0–9 . _ : -. The same key with the same rows is charged once.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9._:-]{8,100}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyRequest"
              },
              "example": {
                "rows": [
                  {
                    "reference": "C-001",
                    "vatId": "BE0403170701"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The check ran. Branch per row on `verdict`. `billing` says what was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request could not be read (not JSON, no rows, a reference too long, a malformed idempotency key). Nothing ran; nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListRefusal"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or not an active connection (unknown, revoked, expired). Nothing ran; nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "schema": {
                  "type": "string"
                },
                "description": "Bearer realm=\"jithox-connection\""
              }
            }
          },
          "402": {
            "description": "The workspace's credits cannot cover the most these rows can cost. VIES was not asked; nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InsufficientCredits"
                }
              }
            }
          },
          "403": {
            "description": "These rows can cost more than the per-call bound the person set on this connection. Nothing ran; nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetExceeded"
                }
              }
            }
          },
          "413": {
            "description": "More than 20 rows, or a body over 65536 bytes. Nothing ran; nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListRefusal"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests for this workspace. Nothing ran; nothing was charged.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "503": {
            "description": "The connection store is not answering, so the credential cannot be checked. Nothing ran; nothing was charged.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoreUnavailable"
                }
              }
            }
          }
        },
        "x-jithox-pricing": {
          "tool": "check_vat_list",
          "unit": "answered_row",
          "pricePerUnitCredits": 2,
          "pricePerUnitEur": "EUR 0.02",
          "maxCreditsPerCall": 40,
          "source": "TOOL_PRICE_CREDITS.check_vat_list — the same price the check_vat_list MCP tool and the website charge."
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "jithoxConnection": {
        "type": "http",
        "scheme": "bearer",
        "description": "A Jithox connection secret (jxc_live_…). A person signs in at https://jithox.com/mcp/account#connection, creates a connection and copies the secret (shown once); the same page revokes it. It bills that person's workspace."
      }
    },
    "schemas": {
      "VerifyRequest": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "type": "object",
              "properties": {
                "reference": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 80,
                  "description": "Your own label, echoed back."
                },
                "vatId": {
                  "type": "string",
                  "description": "The VAT number as written, with its country prefix. Spaces, dots and dashes are fine."
                }
              },
              "required": [
                "vatId"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "rows"
        ],
        "additionalProperties": false
      },
      "VerifyResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "const": true
          },
          "result": {
            "type": "object",
            "properties": {
              "ok": {
                "const": true
              },
              "schema_version": {
                "const": "jx.vat-list-check/v1"
              },
              "registerTier": {
                "type": "boolean",
                "description": "Whether VIES was asked for this request. False when no row was well-formed."
              },
              "rows": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/VatRow"
                }
              },
              "totals": {
                "$ref": "#/components/schemas/VatTotals"
              },
              "answeredRows": {
                "type": "integer",
                "minimum": 0,
                "description": "valid + invalid: the rows VIES answered. This is what is billed."
              },
              "retry": {
                "type": "object",
                "description": "The rows worth sending again (VIES gave no answer).",
                "properties": {
                  "indexes": {
                    "type": "array",
                    "items": {
                      "type": "integer",
                      "minimum": 0
                    }
                  },
                  "advice": {
                    "type": "string"
                  }
                },
                "required": [
                  "indexes",
                  "advice"
                ],
                "additionalProperties": false
              }
            },
            "required": [
              "ok",
              "schema_version",
              "registerTier",
              "rows",
              "totals",
              "answeredRows",
              "retry"
            ],
            "additionalProperties": false
          },
          "source": {
            "type": "object",
            "properties": {
              "register": {
                "const": "EU VIES"
              }
            },
            "required": [
              "register"
            ],
            "additionalProperties": false
          },
          "billing": {
            "$ref": "#/components/schemas/Billing"
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "limits": {
            "$ref": "#/components/schemas/Limits"
          }
        },
        "required": [
          "ok",
          "result",
          "source",
          "billing",
          "pricing",
          "limits"
        ],
        "additionalProperties": false
      },
      "VatRow": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "minimum": 0,
            "description": "Position in the request, from 0. Output row i is input row i."
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own label for the row, echoed."
          },
          "input": {
            "type": "string",
            "description": "The number as sent."
          },
          "normalized": {
            "type": [
              "string",
              "null"
            ],
            "description": "Upper case, separators removed; Greek numbers under VIES's prefix EL."
          },
          "countryCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 3166-1 alpha-2 of the issuing member state."
          },
          "local": {
            "enum": [
              "well_formed",
              "empty",
              "malformed",
              "not_covered",
              "duplicate"
            ],
            "description": "What the free local check found."
          },
          "duplicateOf": {
            "type": "integer",
            "minimum": 0,
            "description": "For a duplicate: the index of the earlier row that carries the answer."
          },
          "register": {
            "enum": [
              "valid",
              "invalid",
              "unknown",
              "not_run"
            ],
            "description": "What the EU register (VIES) said. `unknown` = no answer; never folded into `invalid`."
          },
          "verdict": {
            "enum": [
              "valid",
              "invalid",
              "unavailable",
              "not_checked"
            ],
            "description": "The one word to branch on. valid / invalid: VIES answered (charged). unavailable: VIES was asked and gave no answer — outage, rate limit, time ran out; free; the row is in result.retry.indexes. not_checked: VIES was not asked for this row (malformed, empty, not covered, duplicate); free."
          },
          "registeredName": {
            "type": "string",
            "description": "The name VIES returned. Often absent: several member states never share it."
          },
          "unknownReason": {
            "enum": [
              "rate_limited",
              "no_answer",
              "not_reached",
              "not_authorised"
            ]
          },
          "detail": {
            "type": "string",
            "description": "One sentence a bookkeeper can act on."
          },
          "checkedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When VIES answered. Only on valid and invalid."
          }
        },
        "required": [
          "index",
          "reference",
          "input",
          "normalized",
          "countryCode",
          "local",
          "register",
          "verdict",
          "detail"
        ],
        "additionalProperties": false
      },
      "VatTotals": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "integer",
            "minimum": 0
          },
          "wellFormed": {
            "type": "integer",
            "minimum": 0
          },
          "empty": {
            "type": "integer",
            "minimum": 0
          },
          "malformed": {
            "type": "integer",
            "minimum": 0
          },
          "notCovered": {
            "type": "integer",
            "minimum": 0
          },
          "duplicate": {
            "type": "integer",
            "minimum": 0
          },
          "valid": {
            "type": "integer",
            "minimum": 0
          },
          "invalid": {
            "type": "integer",
            "minimum": 0
          },
          "unknown": {
            "type": "integer",
            "minimum": 0
          },
          "notRun": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "rows",
          "wellFormed",
          "empty",
          "malformed",
          "notCovered",
          "duplicate",
          "valid",
          "invalid",
          "unknown",
          "notRun"
        ],
        "additionalProperties": false
      },
      "Billing": {
        "type": "object",
        "properties": {
          "charged": {
            "type": "boolean",
            "description": "Whether this request moved credits."
          },
          "credits": {
            "type": "integer",
            "minimum": 0,
            "description": "Credits this delivery cost (1 credit = EUR 0.01)."
          },
          "units": {
            "type": "integer",
            "minimum": 0,
            "description": "Rows VIES answered — what `credits` was computed from."
          },
          "pricePerUnitCredits": {
            "type": "integer",
            "minimum": 0
          },
          "reason": {
            "type": "string"
          },
          "receiptRunId": {
            "type": "string",
            "description": "The id of the credit-ledger booking for this delivery. A reference, not a signed receipt: this route returns no signed or hash-bound receipt, and /plan lists the booking without this id."
          },
          "balanceAfter": {
            "type": "integer",
            "description": "The workspace's credit balance after this charge."
          }
        },
        "required": [
          "charged",
          "credits",
          "units",
          "pricePerUnitCredits",
          "reason"
        ],
        "additionalProperties": false
      },
      "Pricing": {
        "type": "object",
        "properties": {
          "localChecks": {
            "const": "included"
          },
          "registerCheck": {
            "type": "string"
          },
          "unit": {
            "const": "answered_row"
          },
          "pricePerUnitCredits": {
            "type": "integer",
            "minimum": 0
          },
          "pricePerUnitEur": {
            "type": "string"
          },
          "maxCreditsPerCall": {
            "type": "integer",
            "minimum": 0
          },
          "chargedWhen": {
            "type": "string"
          },
          "neverCharged": {
            "type": "string"
          }
        },
        "required": [
          "localChecks",
          "registerCheck",
          "unit",
          "pricePerUnitCredits",
          "pricePerUnitEur",
          "maxCreditsPerCall",
          "chargedWhen",
          "neverCharged"
        ],
        "additionalProperties": false
      },
      "Limits": {
        "type": "object",
        "description": "The limits every answer repeats.",
        "properties": {
          "maxRowsPerCall": {
            "type": "integer",
            "minimum": 0
          },
          "maxReferenceChars": {
            "type": "integer",
            "minimum": 0
          },
          "maxVatIdChars": {
            "type": "integer",
            "minimum": 0
          },
          "deadlineMs": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "maxRowsPerCall",
          "maxReferenceChars",
          "maxVatIdChars",
          "deadlineMs"
        ],
        "additionalProperties": false
      },
      "ListRefusal": {
        "type": "object",
        "properties": {
          "ok": {
            "const": false
          },
          "schema_version": {
            "const": "jx.vat-list-check/v1"
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "enum": [
                  "bad_request",
                  "no_rows",
                  "too_many_rows",
                  "reference_too_long",
                  "request_too_large",
                  "invalid_idempotency_key"
                ]
              },
              "message": {
                "type": "string"
              },
              "limit": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "max": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "actual": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  }
                },
                "required": [
                  "name",
                  "max",
                  "actual"
                ],
                "additionalProperties": false
              }
            },
            "required": [
              "code",
              "message"
            ],
            "additionalProperties": false
          },
          "limits": {
            "$ref": "#/components/schemas/Limits"
          }
        },
        "required": [
          "ok",
          "schema_version",
          "error"
        ],
        "additionalProperties": false
      },
      "Unauthorized": {
        "type": "object",
        "properties": {
          "ok": {
            "const": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "const": "unauthorized"
              },
              "reason": {
                "enum": [
                  "missing_credential",
                  "invalid_credential"
                ]
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "reason",
              "message"
            ],
            "additionalProperties": false
          },
          "authorization": {
            "type": "object",
            "properties": {
              "scheme": {
                "const": "Bearer"
              },
              "credentialPrefix": {
                "const": "jxc_live_"
              },
              "createAt": {
                "const": "https://jithox.com/mcp/account#connection",
                "description": "Where a person creates (and revokes) the credential."
              },
              "howToCreate": {
                "type": "string"
              }
            },
            "required": [
              "scheme",
              "credentialPrefix",
              "createAt",
              "howToCreate"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "ok",
          "error",
          "authorization"
        ],
        "additionalProperties": false
      },
      "InsufficientCredits": {
        "type": "object",
        "properties": {
          "ok": {
            "const": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "const": "insufficient_credits"
              },
              "message": {
                "type": "string"
              },
              "requiredCredits": {
                "type": "integer",
                "minimum": 0,
                "description": "The most these rows can cost: price × well-formed rows."
              },
              "pricePerAnsweredRowCredits": {
                "type": "integer",
                "minimum": 0
              },
              "topUpUrl": {
                "const": "https://jithox.com/plan",
                "description": "Where a person adds credits to this workspace."
              }
            },
            "required": [
              "code",
              "message",
              "requiredCredits",
              "pricePerAnsweredRowCredits",
              "topUpUrl"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "ok",
          "error"
        ],
        "additionalProperties": false
      },
      "BudgetExceeded": {
        "type": "object",
        "properties": {
          "ok": {
            "const": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "const": "budget_exceeded"
              },
              "reason": {
                "const": "per_call_budget_exceeded"
              },
              "message": {
                "type": "string"
              },
              "requiredMinorUnits": {
                "type": "integer",
                "minimum": 0,
                "description": "The most these rows can cost, in euro cents."
              },
              "maxMinorUnitsPerCall": {
                "type": "integer",
                "minimum": 0,
                "description": "The bound the person set on this connection."
              },
              "currency": {
                "const": "EUR"
              },
              "manageAt": {
                "const": "https://jithox.com/mcp/account#connection"
              }
            },
            "required": [
              "code",
              "reason",
              "message",
              "requiredMinorUnits",
              "maxMinorUnitsPerCall",
              "currency",
              "manageAt"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "ok",
          "error"
        ],
        "additionalProperties": false
      },
      "RateLimited": {
        "type": "object",
        "description": "The shared rate limiter's answer. See the Retry-After header.",
        "properties": {
          "error": {
            "const": "rate_limited"
          },
          "retryAfterSeconds": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "error",
          "retryAfterSeconds"
        ],
        "additionalProperties": false
      },
      "StoreUnavailable": {
        "type": "object",
        "properties": {
          "ok": {
            "const": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "const": "connection_store_unavailable"
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "ok",
          "error"
        ],
        "additionalProperties": false
      }
    }
  }
}