AI-categorize uncategorized expenses
curl --request POST \
--url https://api.contazen.ro/v1/expenses/ai-categorize-batch \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"max": 123
}'import requests
url = "https://api.contazen.ro/v1/expenses/ai-categorize-batch"
payload = { "max": 123 }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({max: 123})
};
fetch('https://api.contazen.ro/v1/expenses/ai-categorize-batch', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.contazen.ro/v1/expenses/ai-categorize-batch",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'max' => 123
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.contazen.ro/v1/expenses/ai-categorize-batch"
payload := strings.NewReader("{\n \"max\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.contazen.ro/v1/expenses/ai-categorize-batch")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"max\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.contazen.ro/v1/expenses/ai-categorize-batch")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"max\": 123\n}"
response = http.request(request)
puts response.read_body{
"processed": 123,
"categorized": 123,
"skipped": 123,
"errors": 123,
"remaining": 123,
"batch_size": 123,
"cap": 123
}Expenses
AI-categorize expenses
Sweep every uncategorized expense in the firm and assign a category from the existing list using AI classification.
POST
/
expenses
/
ai-categorize-batch
AI-categorize uncategorized expenses
curl --request POST \
--url https://api.contazen.ro/v1/expenses/ai-categorize-batch \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"max": 123
}'import requests
url = "https://api.contazen.ro/v1/expenses/ai-categorize-batch"
payload = { "max": 123 }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({max: 123})
};
fetch('https://api.contazen.ro/v1/expenses/ai-categorize-batch', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.contazen.ro/v1/expenses/ai-categorize-batch",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'max' => 123
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.contazen.ro/v1/expenses/ai-categorize-batch"
payload := strings.NewReader("{\n \"max\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.contazen.ro/v1/expenses/ai-categorize-batch")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"max\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.contazen.ro/v1/expenses/ai-categorize-batch")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"max\": 123\n}"
response = http.request(request)
puts response.read_body{
"processed": 123,
"categorized": 123,
"skipped": 123,
"errors": 123,
"remaining": 123,
"batch_size": 123,
"cap": 123
}When to use
After bulk-importing expenses (OCR, supplier sync, e-Factura SPV) thecategory_id is often null. This endpoint sweeps every such row and assigns a category from the firm’s existing list — system defaults plus custom categories, merged. Existing categorizations are never overwritten.
Call repeatedly until remaining = 0. Each call processes up to cap expenses (a server-side constant returned in the response), so a large backlog can be drained in controlled passes rather than a single long-running request.
Permissions
Requireswrite permission on expenses.
Request
integer
Optional cap on the number of expenses to process this call. The server enforces its own ceiling (
BATCH_MAX_PER_CALL), so very large values are clamped silently. Omit for “process up to the server cap.”Response
{
"data": {
"processed": 50,
"categorized": 47,
"skipped": 2,
"errors": 1,
"remaining": 1242,
"batch_size": 10,
"cap": 50
}
}
| Field | Meaning |
|---|---|
processed | Expenses inspected this call. |
categorized | Expenses that received a category_id. |
skipped | Classifier declined to assign a category (low confidence or no obvious match in the firm’s list). |
errors | Per-row failures — logged server-side, retryable on next call. |
remaining | Uncategorized expenses still queued. Stop when this hits 0. |
batch_size | Expenses per classifier call (informational). |
cap | Hard ceiling per invocation (informational). |
Driver pattern
async function categorizeAll(client) {
while (true) {
const { data } = await client.post('/expenses/ai-categorize-batch');
if (data.remaining === 0) return;
// Optional: brief pause between passes.
await new Promise(r => setTimeout(r, 500));
}
}
Errors
- 401 — missing / invalid bearer token
- 403 — API key lacks
writepermission on expenses - 502
ai_unavailable— classifier call failed; safe to retry - 503
ai_not_configured— AI categorization is not enabled on this deployment
Authorizations
Use your API key (sk_live_xxx or sk_test_xxx)
Body
application/json
Optional override for the per-call cap. The server still
enforces its own BATCH_MAX_PER_CALL ceiling, so very
large values are silently clamped.
Response
Batch run summary
Expenses inspected this call.
Expenses that received a category.
Classifier declined to assign a category (low confidence or no match).
Per-row failures (logged server-side).
Uncategorized expenses still in the queue. Call again until 0.
Expenses per classifier call (server constant).
Hard ceiling per invocation (server constant).
Was this page helpful?