API
Base URL https://api.pintlint.com. JSON out, always.
Start
Send a UBL invoice or credit note to /v1/validate. XML runs through the official PINT AE 1.0.4 schematron. JSON runs through our source-data rules.
curl https://api.pintlint.com/v1/validate \
-H "X-API-Key: $PINTLINT_KEY" \
-H "Content-Type: application/xml" \
--data-binary @invoice.xmlimport os, requests
with open("invoice.xml", "rb") as f:
r = requests.post(
"https://api.pintlint.com/v1/validate",
headers={"X-API-Key": os.environ["PINTLINT_KEY"], "Content-Type": "application/xml"},
data=f,
)
result = r.json()
print(result["valid"])
for e in result["errors"]:
print(e["rule"], e["path"], e["fix"])import { readFile } from "node:fs/promises";
const r = await fetch("https://api.pintlint.com/v1/validate", {
method: "POST",
headers: { "X-API-Key": process.env.PINTLINT_KEY, "Content-Type": "application/xml" },
body: await readFile("invoice.xml"),
});
const result = await r.json();
for (const e of result.errors) console.log(e.rule, e.path, e.fix);Keys and limits
Put your key in the X-API-Key header. Authorization: Bearer works too. Get a free key at pintlint.com/signin.
- 60 requests a minute per key. Over that you get 429 with Retry-After.
- 1 MB per request.
- No key: /v1/public/check, 20 checks a day per IP. It powers the validator on the home page.
Findings
Every error and warning has the same shape. rule links to pintlint.com/rules/<rule>. source is official for schematron rules, ported for official rules we run on JSON, and thorncliff for our own checks.
{
"rule": "IBR-132-AE",
"severity": "error",
"source": "official",
"path": "/Invoice/AccountingSupplierParty[1]/Party[1]/PartyTaxScheme[1]",
"message": "VAT identifier [IBT-031 ...] should be TRN ... 15 digits, starting with 1, ending with 03",
"fix": "A TRN is 15 digits, starts with 1 and ends with 03. Copy it from the VAT certificate."
}
valid is true when there are no errors. Warnings don't fail an invoice.
POST /v1/validate
Validate a UBL XML or JSON invoice.
curl https://api.pintlint.com/v1/validate \
-H "X-API-Key: $PINTLINT_KEY" \
-H "Content-Type: application/xml" \
--data-binary @invoice.xmlimport os, requests
with open("invoice.xml", "rb") as f:
r = requests.post(
"https://api.pintlint.com/v1/validate",
headers={"X-API-Key": os.environ["PINTLINT_KEY"], "Content-Type": "application/xml"},
data=f,
)
result = r.json()
print(result["valid"])
for e in result["errors"]:
print(e["rule"], e["path"], e["fix"])import { readFile } from "node:fs/promises";
const r = await fetch("https://api.pintlint.com/v1/validate", {
method: "POST",
headers: { "X-API-Key": process.env.PINTLINT_KEY, "Content-Type": "application/xml" },
body: await readFile("invoice.xml"),
});
const result = await r.json();
for (const e of result.errors) console.log(e.rule, e.path, e.fix);Response fields
| Field | Type | Notes |
|---|---|---|
| valid | boolean | |
| format | json | ubl | |
| document_type | Invoice | CreditNote | |
| errors | array of object | |
| warnings | array of object | |
| spec | string | |
| disclaimer | string |
Status codes
| Code | Meaning | |
|---|---|---|
| 200 | Result | |
| 400 | Bad body | |
| 401 | Missing or bad key | |
| 413 | Over 1 MB | |
| 422 | Not UBL | |
| 429 | Rate limited | |
POST /v1/fix
Fix a JSON invoice.
curl https://api.pintlint.com/v1/fix?apply_suggestions=true \
-H "X-API-Key: $PINTLINT_KEY" \
-H "Content-Type: application/json" \
--data-binary @invoice.jsonimport os, requests
with open("invoice.json", "rb") as f:
r = requests.post(
"https://api.pintlint.com/v1/fix?apply_suggestions=true",
headers={"X-API-Key": os.environ["PINTLINT_KEY"], "Content-Type": "application/json"},
data=f,
)
result = r.json()
print(result["changes"])
print(result["needs_input"])import { readFile } from "node:fs/promises";
const r = await fetch("https://api.pintlint.com/v1/fix?apply_suggestions=true", {
method: "POST",
headers: { "X-API-Key": process.env.PINTLINT_KEY, "Content-Type": "application/json" },
body: await readFile("invoice.json"),
});
const result = await r.json();
console.log(result.changes, result.needs_input);Query
| Name | In | Notes |
|---|---|---|
| apply_suggestions | query | Also apply changes that normally need review. |
Response fields
| Field | Type | Notes |
|---|---|---|
| corrected | object | |
| invoice_number | string | |
| issue_date | string | |
| due_date | string | |
| currency | string | |
| transaction_type_code | string | |
| seller | object | |
| customer | object | |
| line_items | array of object | |
| totals | object | |
| changes | array of object | |
| needs_review | array of object | |
| needs_input | array of object | |
| valid_after_fix | boolean | |
| remaining_errors | array of object |
Status codes
| Code | Meaning | |
|---|---|---|
| 200 | Fix result | |
| 401 | Missing or bad key | |
| 422 | UBL not supported yet | |
| 429 | Rate limited | |
POST /v1/public/check
Free check (no key). Powers the validator on pintlint.com. Validates, and for JSON also fixes. 20 checks per IP per day, 500 KB max. Use a key for anything automated.
curl https://api.pintlint.com/v1/public/check \
-H "Content-Type: application/xml" \
--data-binary @invoice.xmlimport os, requests
with open("invoice.xml", "rb") as f:
r = requests.post(
"https://api.pintlint.com/v1/public/check",
headers={"Content-Type": "application/xml"},
data=f,
)
result = r.json()
print(result["valid"])
for e in result["errors"]:
print(e["rule"], e["path"], e["fix"])import { readFile } from "node:fs/promises";
const r = await fetch("https://api.pintlint.com/v1/public/check", {
method: "POST",
headers: { "Content-Type": "application/xml" },
body: await readFile("invoice.xml"),
});
const result = await r.json();
for (const e of result.errors) console.log(e.rule, e.path, e.fix);Status codes
| Code | Meaning | |
|---|---|---|
| 200 | Validate result, plus `fix` for JSON and `checks_left_today` | |
| 429 | Daily or burst limit | |
GET /v1/rules
List rules.
curl https://api.pintlint.com/v1/rulesimport requests
r = requests.get("https://api.pintlint.com/v1/rules")
print(r.json())const r = await fetch("https://api.pintlint.com/v1/rules");
console.log(await r.json());Status codes
| Code | Meaning | |
|---|---|---|
| 200 | Rules | |
JSON invoice
The shape /v1/validate and /v1/fix accept. All fields optional; missing ones become findings.
| Field | Type | Notes |
|---|---|---|
| invoice_number | string | |
| issue_date | string | |
| due_date | string | |
| currency | string | |
| transaction_type_code | string | |
| seller | object | |
| name | string | |
| trn | string | |
| address | object | |
| customer | object | |
| name | string | |
| vat_number | string | |
| legal_registration | string | |
| address | object | |
| line_items | array of object | |
| totals | object | |
| tax_exclusive | number | |
| tax | number | |
| payable | number |
JSON covers 33 rules today. Credit notes, allowances, charges, reverse charge and non-AED invoices need UBL for now. Which rules run on JSON.
Errors
Non-2xx responses look like this:
{ "error": { "code": "rate_limited", "message": "60 requests a minute per key. Slow down." } }
| Code | Meaning |
|---|---|
| 400 | Body isn't JSON or XML. |
| 401 | Missing or unknown key. |
| 402 | Monthly quota used up on Free, spend cap reached, or a failed payment. |
| 413 | Body too large. |
| 422 | XML that isn't a UBL Invoice or CreditNote, or UBL sent to /v1/fix. |
| 429 | Rate or daily limit. |