Jithox — developer guide

Five read-only tools for VAT / VIES / Peppol readiness over MCP. Endpoint: https://commerce.mcp.jithox.com/mcp. Validation only — no sending, no compliance guarantee. A competent developer goes from zero to a verified first check in about 15 minutes on this page.

New here? See the decision workflow first — three concrete decisions (ready / needs_review / unavailable) and a verifiable demo receipt, fixture data, no signup.

1. Get started free (no card, 25 calls over 14 days)

Sign up at https://jithox.com and create a one-time developer access key — it is shown once, so copy it immediately. No application, no approval, no sales contact. When the free calls are used up the server answers payment_required; add prepaid credit from €10 and the same call works. One balance covers every Jithox MCP server, and you pay only for an ACCEPTED call — a failure, a refusal, a rate limit or an unavailable source costs €0,00.

2. Connect your MCP client

The endpoint is the same for every client: https://commerce.mcp.jithox.com/mcp. It serves both MCP HTTP transports on that single URL — Streamable HTTP (2025-03-26 and later) and the older HTTP+SSE transport that a large part of the installed editor base still speaks — so you never have to pick one. Your client discovers the OAuth server automatically; you enter your developer access key once on the consent screen; it then keeps a refreshing connection — no copying tokens by hand.

Recommended path — Claude Code (one command, then /mcp to authenticate):

claude mcp add --transport http jithox-einvoice https://commerce.mcp.jithox.com/mcp

That is the whole setup: approve the OAuth consent, paste your access key once, and continue to step 3.

Other clients — Cursor, VS Code, Claude web/desktop, or anything else

Cursor — add to ~/.cursor/mcp.json, then approve the consent Cursor opens:

{ "mcpServers": { "jithox-einvoice": { "url": "https://commerce.mcp.jithox.com/mcp" } } }

VS Code — add to .vscode/mcp.json:

{ "servers": { "jithox-einvoice": { "type": "http", "url": "https://commerce.mcp.jithox.com/mcp" } } }

Claude (web/desktop): Settings → Connectors → Add custom connector → paste the URL. Any other MCP client: add a remote server (Streamable HTTP or SSE — both work) with the URL and approve OAuth.

An older client that only speaks SSE works on the same URL. If yours insists on a dedicated SSE address, https://commerce.mcp.jithox.com/sse is the same server: it opens the stream and tells your client where to POST. Nothing else changes — same tools, same OAuth, same key.

No browser? Put your access key in the client config (headless / CI / blocked OAuth callback)

If your editor cannot complete the OAuth browser redirect (locked-down loopback, remote devbox, CI), the server also accepts your developer access key directly — the same pcd_… key the consent screen takes, sent as a header on every call. No browser opens; tools appear the moment the client connects.

VS Code.vscode/mcp.json. VS Code prompts for the key once (masked) and stores it itself, so the key is never written into the file:

{
  "inputs": [
    { "id": "jithox-key", "type": "promptString", "password": true,
      "description": "Jithox developer access key (pcd_…)" }
  ],
  "servers": {
    "jithox-einvoice": {
      "type": "http",
      "url": "https://commerce.mcp.jithox.com/mcp",
      "headers": { "Authorization": "Bearer ${input:jithox-key}" }
    }
  }
}

Cursor~/.cursor/mcp.json (Cursor has no prompt mechanism; the key sits in this file, so treat the file like a password and keep it out of every repository):

{ "mcpServers": { "jithox-einvoice": {
    "url": "https://commerce.mcp.jithox.com/mcp",
    "headers": { "Authorization": "Bearer YOUR_ACCESS_KEY" } } } }

Claude Code:

claude mcp add --transport http jithox-einvoice https://commerce.mcp.jithox.com/mcp --header "Authorization: Bearer YOUR_ACCESS_KEY"

Anything else (SDKs, curl, gateways): send Authorization: Bearer YOUR_ACCESS_KEY — or, if your client reserves that header for OAuth, X-Jithox-Api-Key: YOUR_ACCESS_KEY — on POST https://commerce.mcp.jithox.com/mcp. Never both headers at once.

Same key, same lifecycle: per-product scopes, rotation and revocation on jithox.com (MCP servers → Connect) take effect on the very next call, and workspace keys expire after 90 days. A key in a config file can leak — prefer the OAuth consent flow when your environment allows it, and rotate the key immediately if you ever commit it.

PKCE is required, scopes are least-privilege per tool, tokens refresh automatically, and you can revoke anytime from your client. Connecting before you paid is fine — the consent screen links you to /pricing and tool calls answer 402 until access is active.

3. The five tools

There is no sixth tool and no send; send_peppol_invoice is absent. A tool that can't reach its provider returns a truthful unavailable rather than a guess.

4. Your first workflow (what to expect, tool by tool)

Run these over the connected session — in an agent chat you can simply ask ("verify VAT BE0123456749 in VIES"); raw JSON-RPC shown for direct calls:

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"validate_vat_number","arguments":{"vatNumber":"BE0123456749"}}}
  1. validate_vat_number with BE0123456749valid: format + checksum only, no network.
  2. verify_vat_vies with the same number → live status invalid, and that is correct: this docs example is checksum-valid but not an active EU VAT registration — exactly the gap the live check exists to catch. Use your own VAT number to see valid with provenance (source, checkedAt). unavailable means VIES itself could not answer: truthful, retry later, never a guess.
  3. lookup_peppol_participant with {"countryCode":"BE","vatId":"BE0671516647"} → status registered with supportsBis30Invoice: true — that is FPS BOSA, a Belgian public service whose Peppol registration is public record (it is listed in the public Peppol Directory), so this example is deterministic. Your own number returns registered or not_registered (both definitive answers, not errors); unavailable is truthful and never a hidden "no".
  4. get_einvoice_readiness with {} → one plain state + what is missing + the next step. In this validation pilot expect not_configured: your workspace has no send/receive provider bound — truthful, because sending is not part of this product. The four tools above work regardless.

Every accepted execution returns its result, counts one against your plan, and carries a signed receipt in _meta.receipt — verify it offline in §7.

"Accepted" is not a verdict. A 200 that counts one check only means Jithox ran the check and returned a truthful answer — it is never itself an approval of your invoice or counterparty. The verdict is decision.status below, and an answer of not_ready, needs_review or unavailable is a fully successful, billed call whose content is "not yet / can't tell". Read the status, not the HTTP code.

4b. Reading the result — the decision envelope

Every accepted result carries a uniform decision object (added alongside the tool's native fields — nothing was removed). One shape, whichever tool you call, so software and agents can act without learning five vocabularies. Full contract: docs/DECISION_GRADE_EINVOICING.md.

"decision": {
  "status": "ready | not_ready | needs_review | unavailable",
  "summary": "one-line conclusion",
  "decision": { "canProceed": bool, "requiresHumanReview": bool,
                "blockingReasons": [], "warnings": [], "nextActions": [] },
  "evidence": [ { "source": "vies", "statement": "…", "observedAt": "ISO" } ],
  "limitations": [ "Technical readiness only: not a legal/fiscal/compliance guarantee…" ],
  "provenance": { "source": "…", "checkedAt": "ISO" },
  "requestId": "…", "receiptId": "…"
}

The decision is committed by the signed receipt (resultHash covers it), so the whole decision — not just the raw result — is verifiable offline (§7). Worked outcomes:

validate_vat_number "BE0123456749"      → decision.status "ready"        (format valid; warns: run VIES next)
verify_vat_vies     "BE0123456749"      → decision.status "not_ready"    (checksum-valid but not VIES-active — by design)
lookup_peppol_participant BOSA BE0671516647 → decision.status "ready"    (registered, BIS 3.0)
verify_vat_vies     (VIES timeout)      → decision.status "unavailable"  (retry; NOT "invalid")
get_einvoice_readiness {}               → decision.status "needs_review" (validation-only: no send provider bound)

5. Limits & errors

Entitlement errors carry error.data.pricing and error.data.docs URLs, so an agent hitting 402 can hand its human the exact next step.

6. Privacy

verify_vat_vies sends only the VAT number to the EU VIES service — never an invoice, name, or email. Full VAT numbers are not logged; request logs hold only coded errors and privacy-safe timing.

7. Verify your receipts (offline, no account needed)

Every accepted execution returns a signed receipt in _meta.receipt (Ed25519). Anyone can verify one offline with our public key — no account, no payment, no API call to us at verification time. Try it in two minutes, before you even buy access:

curl -sO https://commerce.mcp.jithox.com/receipts/public-key.pem
curl -sO https://commerce.mcp.jithox.com/receipts/verify-receipt.mjs
curl -s  https://commerce.mcp.jithox.com/receipts/sample.json -o receipt.json
node verify-receipt.mjs receipt.json public-key.pem   # → VALID

The sample is a REAL receipt signed by the live key; its kind is "sample", which marks it a dry-run artifact — it is never proof of an execution or a payment. To verify one of your receipts, save the _meta.receipt object as receipt.json and re-run the last command. Machine-readable key metadata (JWK, key ids, canonicalization rules): /receipts/verification.

The downloadable script is exactly this, in full — audit it; it is self-contained (Node 18+, no dependencies) and never phones home:

// verify-receipt.mjs — node verify-receipt.mjs receipt.json public-key.pem [expected-capability-id]
import { readFileSync } from "node:fs";
import { createPublicKey, createHash, verify } from "node:crypto";
const receipt = JSON.parse(readFileSync(process.argv[2], "utf8"));
const key = createPublicKey(readFileSync(process.argv[3], "utf8"));
const expectCapability = process.argv[4]; // optional; v3 only
const canon = (v) => v === null || typeof v !== "object" ? JSON.stringify(v)
  : Array.isArray(v) ? "[" + v.map(canon).join(",") + "]"
  : "{" + Object.entries(v).filter(([,x]) => x !== undefined).sort(([a],[b]) => a < b ? -1 : 1)
      .map(([k,x]) => JSON.stringify(k) + ":" + canon(x)).join(",") + "}";
const spki = key.export({ type: "spki", format: "der" });
const ids = new Set(["canary-ed25519-" + createHash("sha256").update(spki).digest("hex").slice(0,16),
                     "canary-ed25519-" + Buffer.from(spki).toString("hex").slice(0,16)]); // legacy alias
if (receipt.alg !== "Ed25519") throw new Error("unsupported algorithm");
if (!ids.has(receipt.keyId)) throw new Error("unknown key id (not this public key)");
let input;
// v3 = capability-bound envelope. It excludes keyId AND alg from the signed payload (v2 keeps
// them) and uses its own domain-separated context, so a v3 receipt checked as v2 fails closed.
if (receipt.version === "3") { const { keyId, alg, signature, ...rest } = receipt; input = "jithox.receipt.v3\n" + canon(rest); }
else if (receipt.version === "2") { const { signature, ...rest } = receipt; input = "jithox.receipt.v2\n" + canon(rest); }
else if (receipt.version === "1") input = ["jithox.receipt.v1", receipt.keyId, receipt.alg, receipt.executionId,
  receipt.requestHash, receipt.resultHash, receipt.tool, receipt.identityClass, receipt.usageRef,
  receipt.outcome, String(receipt.billable), receipt.ts].join("\n");
else throw new Error("unsupported receipt version: " + JSON.stringify(receipt.version));
const ok = verify(null, Buffer.from(input, "utf8"), key, Buffer.from(receipt.signature, "base64"));
// A v3 signature proves the payload; it does NOT prove the payload is the capability you asked
// about. Check that too, or one capability's receipt can be presented as another's.
if (ok && receipt.version === "3" && expectCapability !== undefined && receipt.capabilityId !== expectCapability) {
  throw new Error("capability mismatch: receipt is " + receipt.capabilityId + ", expected " + expectCapability);
}
// JX-MCP-065: a kind:"demo" receipt (signed by the SEPARATE demo key) is labelled
// so fixture evidence is never mistaken for a live/production verification.
const detail = receipt.version === "3"
  ? " — capability " + receipt.capabilityId + " v" + receipt.capabilityVersion + ", tool " + receipt.toolId + ", issuer " + receipt.issuer
  : "";
console.log(ok ? (receipt.kind === "demo" ? "VALID DEMO RECEIPT (fixture — not a live company verification)" : "VALID" + detail) : "INVALID (signature does not match payload)");

Error meanings: unknown key id — the receipt was not signed by this key (wrong key file, or a forged receipt); INVALID signature — the payload was altered after signing (any field, even one character); unsupported receipt version — versions other than 1/2 do not exist today; a malformed/re-ordered file is safe — canonicalization sorts keys, so formatting never changes the outcome, only content does.

What a valid signature proves: this exact receipt was issued by Jithox at the stated time and is unaltered. What it does not prove: it is not a legal/fiscal/compliance guarantee, not proof the underlying VAT/VIES status is still current, and not a payment instrument.

Receipts reference inputs/results only as SHA-256 commitments (requestHash/resultHash) — your data is never inside a receipt.

8. Versioning & deprecation

MCP protocol. Supported protocol versions today: 2026-07-28 (the current stateless revision — per-request _meta envelope, server/discover, routing headers, resultType) plus the handshake era 2025-11-25, 2025-06-18, 2025-03-26initialize echoes your requested handshake version when supported. Dropping support for a protocol version is a breaking change (see notice policy below).

Tools. The five tool names are stable and will not be renamed or removed. Input/output schemas evolve additively only: new optional fields may appear; existing fields never silently change meaning or type. A field we intend to remove is first marked deprecated in its description here for at least 30 days. Stable error codes (payment_required, rate_limited, insufficient_scope, …) are additive-only. Provider statuses stay truthful: unavailable / not_configured mean exactly that — never a guessed answer.

Breaking changes. Announced on this page at least 30 days in advance. One explicit exception: security emergencies may change behaviour immediately; we disclose what changed here afterwards.

Receipts. Receipt versions currently issued: v2 (v1 was issued before 2026-07-14). Every receipt ever issued stays verifiable: old versions remain supported by the published verification rules, rotated-out public keys stay listed at /receipts/verification for at least 24 months, the legacy key-id alias is documented there, and historical receipts are never re-signed or rewritten — including after an entitlement expires or is revoked.

No uptime SLA is offered; health is observable at /healthz and /readyz.

9. Support & status

Founder-direct: info.jithox@gmail.com. Health: /healthz and /readyz. Pricing: /pricing.