jofotara-kit
v0.3.0
Published
Document builder, safe submission client, XML validator, local mock server and AI agent skill for JoFotara (Jordan ISTD e-invoicing) integrations.
Maintainers
Readme
jofotara-kit
The JoFotara sandbox that doesn't exist — plus a document builder, a safe submission client, a validator and an AI agent skill.
JoFotara (فوترة, Jordan's ISTD e-invoicing system) has no test environment. The only way to
see if your XML is right is to send a real invoice with real credentials and create a real
tax record. jofotara-kit lets you get the shape right locally first:
build— turns plain sale data into a valid invoice or return: tax-inclusive prices, line and order discounts, exempt and zero-rated lines, buyer rules, and partial returns that add up to the invoice exactly. From TypeScript/JavaScript, or from any language through JSON.send— submits documents and sorts every answer into accepted, rejected, unknown or not sent, so a timeout is never mistaken for a rejection and resent as a duplicate tax record.validate— lints JoFotara UBL 2.1 XML against the rules of the official ISTD technical guide (v1.4): type names, tax categoriesS/Z/Oand allowed VAT rates, buyer IDs, totals formulas, return references and the extra fields sales returns need. Every finding cites the manual page and says how to fix it.serve— a local mock ofPOST /core/invoices/: same path, sameClient-Id/Secret-Keyheaders, same{"invoice": base64}body,EINV_STATUS/EINV_RESULTS/EINV_QR-shaped responses. It remembers what it accepted, so duplicates, returns against unknown invoices and over-returns across several partial returns are rejected, line by line.template— prints sales and income invoices and (partial) returns, cash and receivable, in the manual's shape.- Agent skill — teaches Claude Code, Cursor, Devin, Codex & co. how JoFotara works and makes them validate their own output instead of guessing.
Not affiliated with the Income and Sales Tax Department. Passing
jofotara-kitmeans "passes known rules", not "accepted by ISTD".
Quick start
npx jofotara-kit template invoice > invoice.xml
npx jofotara-kit validate invoice.xml
npx jofotara-kit serve --port 8080Then point your integration's base URL at http://127.0.0.1:8080 instead of
https://backend.jofotara.gov.jo. Nothing else changes.
curl -s http://127.0.0.1:8080/core/invoices/ \
-H "Client-Id: test" -H "Secret-Key: test" -H "Content-Type: application/json" \
-d "$(npx jofotara-kit template invoice --body)"To use it from your code, tests or CI, install it (Node.js 22.18 or later):
npm install jofotara-kitBuild documents
Describe the sale; the builder does the JoFotara math and writes XML that passes every rule.
import { buildInvoice, buildCreditNote } from 'jofotara-kit';
const invoice = buildInvoice({
id: 'INV-1001', icv: 57, // your number and your invoice counter
seller: { taxNumber: '…', name: '…', incomeSourceSequence: '…' },
pricesIncludeTax: true, // shelf prices; VAT is taken out exactly
lines: [
{ name: 'Coffee', quantity: 3, unitPrice: 2.5, taxRate: 16, discount: { percent: 10 } },
{ name: 'Bread', quantity: 2, unitPrice: 0.75, taxRate: 0, taxCategory: 'Z' },
],
orderDiscount: { amount: 1 }, // spread over the lines for you
});
// invoice.xml, invoice.body (the request body to POST), invoice.document (every computed amount)
const ret = buildCreditNote({
original: storedInvoiceXml, // the XML you sent and stored
previousReturns: [storedReturnXml], // earlier returns against it
lines: [{ lineId: 1, quantity: 1 }], // or 'all' for everything not yet returned
reason: 'Customer return', id: 'RET-1001', icv: 58,
});What it takes care of:
- Money — exact arithmetic in micro-JOD, amounts written with six decimals. With
pricesIncludeTax, each line's amount plus VAT equals what the customer paid, exactly. - Tax categories —
Sat an allowed rate; a 0% line must sayZ(exempt) orO(zero-rated). Rates outside the manual's list are refused before any XML exists. - Buyers — a walk-in buyer (
TN0, "Cash customer") by default; a named buyer is required for receivables and cash sales above 10,000 JOD. - Returns — built from the original XML alone: original line numbers, names, prices, categories, buyer and type name are mirrored, sales returns get their extra fields, returned quantities are tracked across returns (over-returns are refused), and the last return of a line takes exactly what is left, so the returns always add up to the invoice.
- Errors — a
BuildErrorwhosecodeis the rule the input would break, e.g.JOF-STA-005for an over-return.
Keep the uuid of every document you send: a retry after a timeout must reuse the same XML.
readDocument(xml) reads any stored JoFotara document back into plain amounts.
Special sales (013/023) follow the manual's formulas (p.56-80) but have not been verified
against the live API yet; builds report this in warnings.
From any language
jofotara-kit build takes the same fields as JSON, so PHP, .NET, Python or Java code can shell out
to it:
npx jofotara-kit build invoice.json > INV-1001.xml
npx jofotara-kit build invoice.json --body # the request body instead of XML
npx jofotara-kit build invoice.json --json # {xml, body, document, warnings}A return names its original (and earlier returns) by path, relative to the JSON file:
{
"type": "credit-note",
"original": "INV-1001.xml",
"previousReturns": ["RET-1000.xml"],
"lines": [{ "lineId": 1, "quantity": 1 }],
"reason": "Customer return",
"id": "RET-1001",
"icv": 58
}A build error exits with status 1 and prints the rule id and the reason.
Send documents
The dangerous part of going live is not the XML — it is the answer. A timeout does not mean the invoice was rejected; resending a new document for the same sale can create a duplicate tax record. The client sorts every submission into one of four outcomes:
import { createClient, JOFOTARA_PRODUCTION_URL } from 'jofotara-kit';
const jofotara = createClient({
baseUrl: 'http://127.0.0.1:8080', // required: the local mock, or JOFOTARA_PRODUCTION_URL on purpose
clientId: process.env.JOFOTARA_CLIENT_ID!,
secretKey: process.env.JOFOTARA_SECRET_KEY!,
});
const outcome = await jofotara.submit(invoice); // XML, request body, or a build result| outcome.status | When | What to do |
|---|---|---|
| accepted | HTTP success and an explicit success status and a QR code | Store outcome.qr and outcome.raw; print the QR on the invoice. |
| rejected | Explicit rejection (NOT_SUBMITTED, ERROR, an error list) and no sign of acceptance — also when it arrives with HTTP 200 | Show outcome.errors, fix, send again. |
| unknown | Timeout, dropped connection, unreadable or HTML response, 5xx, success without a QR, contradictory answers | Do not send a new document for this sale. Check the portal; to retry, send this exact XML again (same UUID). |
| not-sent | The document failed validation, or the server could not be reached at all (DNS, connection refused) | Nothing was recorded; fix and send. |
The client validates before sending (validateBeforeSending: false turns that off), never
retries on its own, and never puts the secret key in an outcome. classifyResponse(status, body)
applies the same rules to a response you received with your own HTTP code.
From the command line, documents are sent in order and sending stops at the first one that is not accepted, so a return is never sent after its invoice failed:
npx jofotara-kit serve & # the local mock
npx jofotara-kit send INV-1001.xml RET-1001.xml # goes to the mock by defaultsend --production goes to https://backend.jofotara.gov.jo and takes the credentials only from
the JOFOTARA_CLIENT_ID and JOFOTARA_SECRET_KEY environment variables. Exit status: 0 accepted,
1 rejected or not sent, 3 unknown.
MCP server
Give any MCP client (Claude Code, Cursor, Devin, Windsurf, …) direct access to the validator:
{
"mcpServers": {
"jofotara": { "command": "npx", "args": ["-y", "jofotara-kit", "mcp"] }
}
}Tools: build_invoice, build_credit_note, validate_invoice, get_template, list_rules, explain_rule.
Install the agent skill
npx skills add balwahidi/jofotara-kitOr copy skills/jofotara into your agent's skills folder
(.claude/skills/, .devin/skills/, .cursor/skills/, …).
Commands
| Command | What it does |
|---|---|
| build [input.json] [--body \| --json] | Build an invoice or return from JSON (see Build documents). Reads stdin if no file. Exit 1 on build errors. |
| send <files...> [--production \| --base-url URL] [--json] [--timeout MS] [--no-validate] | Send documents in order and classify each answer (see Send documents). Mock by default; credentials from the environment. |
| validate [files...] [--json] | Validate XML, a {"invoice": base64} request body, or bare base64. Reads stdin if no files. Exit 1 on errors. |
| serve [--port] [--host] [--client-id] [--secret-key] [--reject-status] | Run the mock. With --client-id/--secret-key only those credentials pass. --reject-status 200 tests "HTTP 200 but rejected" handling. |
| template <name> [--body] | Print a sample XML, or its JSON request body. Names below. |
| rules [--json] | List every rule. |
| mcp | MCP server over stdio. |
Templates:
| Name | Type | Document |
|---|---|---|
| invoice | 388 / 012 | Sales invoice, cash (S, Z and O lines) |
| credit-note | 381 / 012 | Partial sales return |
| receivable-invoice | 388 / 022 | Sales invoice, receivable (named buyer) |
| receivable-credit-note | 381 / 022 | Partial return of a receivable sales invoice |
| income-invoice | 388 / 011 | Income invoice, cash (no VAT) |
| income-credit-note | 381 / 011 | Partial income return |
| income-receivable-invoice | 388 / 021 | Income invoice, receivable (named buyer) |
| income-receivable-credit-note | 381 / 021 | Partial return of a receivable income invoice |
Mock extras: GET /_kit/invoices lists accepted documents, DELETE /_kit/invoices resets.
Mock responses add a JOFOTARA_KIT key with full findings and fixes; everything else mirrors
the real response shape. The mock QR decodes to JOFOTARA-KIT MOCK|NOT A TAX DOCUMENT|….
Use in CI
# have your test suite write the XML your integration generates to out/, then:
npx jofotara-kit validate out/*.xmlOr as a library:
import { validate } from 'jofotara-kit';
const report = validate(xml);
if (!report.ok) throw new Error(report.findings.map((f) => `${f.rule}: ${f.message}`).join('\n'));Where the rules come from
Every rule states its source:
| Source | Meaning |
|---|---|
| manual p.N | Stated in the ISTD technical guide for the e-invoicing API, v1.4 (2023-12-01), page N. |
| verified | Observed on the live API and contributed back with a redacted response. |
| inferred | Follows from UBL 2.1 or accounting common sense; not in the manual. |
Run npx jofotara-kit rules for the full list.
Scope
The builder, the validator and the mock cover all six document families in the manual, in JOD:
income (011/021), general sales (012/022) and special sales (013/023), each as new
invoice (388) and return (381) — full, partial and multiple returns. Special sales are not yet
verified against the live API. Templates cover income and general sales.
Contributing
The most valuable contribution is a real JoFotara response — especially a rejection the
kit didn't predict — with the XML (redacted: tax numbers, names, income source sequence) and
the response body. That is how inferred rules become verified. Never share your Client-Id
or Secret-Key.
npm install
npm test
npm run buildLicense
MIT
