Webhooks & polling

Receive async results two ways: a signed webhook callback when a scan completes, or by polling GET /v1/receipts/{id}.

Two ways to get an async result

After you submit to POST /v1/receipts/async, the result is not in the immediate 202 response — only the scan id is. You can either (1) supply a callbackUrl and let Tagjet POST the finished result to your server, or (2) poll GET /v1/receipts/{id} until its status is terminal. Both are first-class; pick whichever fits your infrastructure.

Option A — webhook callback

Pass a callbackUrl when you submit the async scan (a multipart field, or a JSON body field). When processing finishes, Tagjet sends a single POST to that URL containing the verbose scan result as the JSON body. The callback URL must be a public http(s) endpoint — requests to private/internal hosts are blocked.

bash
curl https://api.tagjet.app/v1/receipts/async \
  -H "apikey: tj_live_your_key_here" \
  -F "file=@receipt.jpg" \
  -F "callbackUrl=https://your-app.example.com/hooks/tagjet"

What the callback request looks like

The POST carries Content-Type: application/json and two Tagjet headers: X-Tagjet-Event: scan.completed identifies the event, and X-Tagjet-Signature carries an HMAC-SHA256 signature of the exact request body, formatted as sha256=<hex>. The body itself is the same shape as GET /v1/receipts/{id} (verbose: id, status, data with confidences, and so on). Delivery is retried a few times on non-2xx responses, so make your handler idempotent (key off the scan id).

http
POST /hooks/tagjet HTTP/1.1
Content-Type: application/json
X-Tagjet-Event: scan.completed
X-Tagjet-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

{ "id": "0e0b1f1a-...", "status": "DONE", "overallConfidence": 0.93, "data": { ... } }

Verifying the signature

Compute HMAC-SHA256 over the raw request body using your webhook signing secret, hex-encode it, prefix it with sha256=, and compare against the X-Tagjet-Signature header with a constant-time comparison. Reject the request if they do not match.

javascript
import crypto from 'node:crypto'

function verify(rawBody, header, secret) {
  const digest = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  const expected = `sha256=${digest}`
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header))
}

Option B — polling

If you cannot expose a public callback URL, simply poll GET /v1/receipts/{id} on a back-off interval until status is terminal (DONE, or a failure with an error object). This needs no extra setup beyond the id you already received from the 202 response.

python
import time, requests

headers = {"apikey": "tj_live_your_key_here"}
scan_id = "0e0b1f1a-..."  # from the 202 response

while True:
    scan = requests.get(
        f"https://api.tagjet.app/v1/receipts/{scan_id}", headers=headers
    ).json()
    if scan["status"] == "DONE":
        print(scan["data"]["totalAmount"])
        break
    time.sleep(2)

Which to use

Prefer webhooks when you control a public endpoint — they avoid wasted polling requests and give you results the moment they are ready. Use polling for local development, serverless setups without a stable inbound URL, or quick scripts. You can also do both: supply a callbackUrl and still fall back to GET /v1/receipts/{id} if a delivery is missed.