PintLint

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.xml
import 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.

Auth: X-API-Key · Body: application/json, application/xml

curl https://api.pintlint.com/v1/validate \
  -H "X-API-Key: $PINTLINT_KEY" \
  -H "Content-Type: application/xml" \
  --data-binary @invoice.xml
import 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

FieldTypeNotes
validboolean
formatjson | ubl
document_typeInvoice | CreditNote
errorsarray of object
warningsarray of object
specstring
disclaimerstring

Status codes

CodeMeaning
200Result
400Bad body
401Missing or bad key
413Over 1 MB
422Not UBL
429Rate limited

POST /v1/fix

Fix a JSON invoice.

Auth: X-API-Key · Body: application/json

curl https://api.pintlint.com/v1/fix?apply_suggestions=true \
  -H "X-API-Key: $PINTLINT_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @invoice.json
import 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

NameInNotes
apply_suggestionsqueryAlso apply changes that normally need review.

Response fields

FieldTypeNotes
correctedobject
invoice_numberstring
issue_datestring
due_datestring
currencystring
transaction_type_codestring
sellerobject
customerobject
line_itemsarray of object
totalsobject
changesarray of object
needs_reviewarray of object
needs_inputarray of object
valid_after_fixboolean
remaining_errorsarray of object

Status codes

CodeMeaning
200Fix result
401Missing or bad key
422UBL not supported yet
429Rate 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.

Auth: No key · Body: application/json, application/xml

curl https://api.pintlint.com/v1/public/check \
  -H "Content-Type: application/xml" \
  --data-binary @invoice.xml
import 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

CodeMeaning
200Validate result, plus `fix` for JSON and `checks_left_today`
429Daily or burst limit

GET /v1/rules

List rules.

Auth: No key

curl https://api.pintlint.com/v1/rules
import 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

CodeMeaning
200Rules

JSON invoice

The shape /v1/validate and /v1/fix accept. All fields optional; missing ones become findings.

FieldTypeNotes
invoice_numberstring
issue_datestring
due_datestring
currencystring
transaction_type_codestring
sellerobject
namestring
trnstring
addressobject
customerobject
namestring
vat_numberstring
legal_registrationstring
addressobject
line_itemsarray of object
totalsobject
tax_exclusivenumber
taxnumber
payablenumber

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." } }
CodeMeaning
400Body isn't JSON or XML.
401Missing or unknown key.
402Monthly quota used up on Free, spend cap reached, or a failed payment.
413Body too large.
422XML that isn't a UBL Invoice or CreditNote, or UBL sent to /v1/fix.
429Rate or daily limit.