Decision layer

Optional, enabled on request: expense categorisation, spending-policy checks, VAT-reclaim eligibility and a calibrated review score on top of a scan.

What it adds

Extraction tells you what is printed on a receipt. The decision layer judges it: how likely the extraction is to need a human, what kind of document it actually is, and whether each key field matches the text it came from. Supply your own context and it also returns which of your expense categories the receipt belongs to, whether it satisfies your spending policy, and whether input VAT is reclaimable in a given country.

It is off by default and enabled on request

This is not part of any plan and cannot be switched on by upgrading. It involves processing the text of your receipts through an additional engine, so it is enabled per account, deliberately, after we have agreed it in writing. Contact support to request it. You can ask which engine processes your data, including a self-hosted option that keeps the text inside Tagjet infrastructure – tell us your requirement and we will configure your account accordingly.

Turning it on for a key

Once your account is enabled, the Keys & usage page shows a Decisions switch on each API key. It starts off on every key, including keys you create afterwards, so a key used for an unrelated integration keeps sending nothing. Sending the decisions field on a key that does not have it enabled is accepted and ignored, so you can ship your integration before the switch is flipped.

Request

Add a decisions object to any JSON scan request. Every part of it is optional. Policy statements are phrased so that true means compliant. Category lists should stay under about 20 entries – accuracy drops on long lists, and a short list with a clear label beats a long one.

bash
curl https://api.tagjet.app/v1/receipts/verbose \
  -H "apikey: tj_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "<base64>",
    "contentType": "image/jpeg",
    "wantLineItems": true,
    "decisions": {
      "categories": [
        { "code": "MEALS", "label": "meals and entertainment" },
        { "code": "TRAVEL", "label": "travel, fuel and transport" }
      ],
      "policies": [
        { "code": "NO_ALCOHOL", "statement": "No alcohol was purchased." }
      ],
      "vatReclaimCountry": "LV",
      "classifyLines": true
    }
  }'

Response

The result gains a decisions block alongside data and quality. Fields are omitted when the question was not asked or could not be answered – treat an absent field as unknown, never as a negative answer.

json
{
  "decisions": {
    "calibrated": true,
    "needsReviewScore": 0.04,
    "needsReview": false,
    "documentType": "RECEIPT",
    "fieldChecks": [
      { "field": "totalAmount", "value": "24.90", "verified": true, "confidence": 0.98 }
    ],
    "expenseCategory": "MEALS",
    "expenseCategoryConfidence": 0.93,
    "vatReclaimable": true,
    "policyChecks": [
      { "code": "NO_ALCOHOL", "passed": true, "confidence": 0.97 }
    ],
    "policyCompliant": true,
    "lineClassifications": [
      { "index": 0, "description": "Coffee", "kind": "GOODS", "confidence": 0.96 }
    ],
    "notes": ["..."]
  }
}

Check calibrated before you auto-approve

When calibrated is true the confidences are genuine probabilities: a 0.97 means roughly a 3% chance of being wrong, so a threshold you tune against real receipts keeps meaning the same thing. If the layer has to fall back to a secondary engine, calibrated comes back false and the numbers are self-reported instead – same shape, different meaning. Read the flag before driving an auto-approval rule off a score, and route the fallback results to review rather than through it.

Undetermined is a real answer

A line whose kind cannot be established confidently comes back with kind set to null and the confidence still reported. That is deliberate: a wrong GOODS or DISCOUNT label silently changes what a total reconciles to, so the layer declines rather than guesses. Route those lines to a human instead of defaulting them.

It never fails your scan

The layer is advisory. If it is unavailable the scan still succeeds and the decisions block is simply absent from the response – your integration should handle that case the same way it handles any optional field. It also does not extend the normal processing time budget.