Unicraft API
Read a construction company's money as its people record it in Unicraft - purchases, rentals, daily workers' and the timesheet's wages, payments, acts, money in and cash advances - and say back what your program did with each record. Built first to connect the company's accounting program, so nobody types the same payment twice.
Getting started
- The company's administrator opens Company → Integrations in Unicraft, makes a key and copies it. It is shown only once.
- Every call goes to
https://unicraft.ge/api/v1/…with the key in theAuthorizationheader. - Read the feed from the start (
after=0): you get the company's whole history, oldest first, then everything new as it happens.
curl https://unicraft.ge/api/v1/me \
-H "Authorization: Bearer uck_your_key"
{
"company": { "id": "cc39…", "name": "Your Construction LLC" },
"key": { "id": "eddd…", "name": "ORIS connector" },
"scopes": ["accounting:read", "accounting:write"]
}
Keys
A key belongs to one company and opens only that company's records. Send it as Authorization: Bearer uck_… (or X-Api-Key: uck_…). Keep it on your server, never in a web page.
| Scope | What the key may do |
|---|---|
accounting:read | Read the projects, the feed and single records. Every key has it. |
accounting:write | Also mark records as sent, failed or skipped (/v1/sync) and say what was paid in your program (/v1/paid). A connector needs it. |
people:read | Also read workers' personal ID numbers (timesheet_month's worker.personal_no), for payroll. Without it they come as null. |
A key can make up to 600 calls a minute. The administrator can revoke a key at any time; from then on it gets 401. A company has up to 10 keys - give each program its own.
The feed
Every purchase, rental, month of daily workers, timesheet month, payment, act, money in and cash advance that is added, changed or deleted gets the next number (seq) in the company's feed. You keep the last number you have and ask for what came after it.
- Each change carries the record as it is now (
data), not as it was. A record changed five times appears five times, each time with its latest state - handle each one by itsid, so seeing it again only updates what you made before. opisupsert(added or changed) ordelete. A deleted record'sdataisnull; so is that of a record deleted after the change was made.- A change is handed out once it is a minute old, so that a save still finishing can never land behind a number you have already passed. Expect what is recorded to reach you a minute or so later.
- A record also comes again when something it shows changes elsewhere: a cash advance when something is bought with it, a request when its goods arrive, a rental counted by days every night (its cost grows each day), a month of daily workers when a log, a timesheet day or a payment for it changes.
- Match records to your own by
id(and a subcontractor, supplier or project by itsidwhere it has one), never by name: names are copied as they were when the change was made. - Ask again until
moreisfalse, then poll every few minutes.
Endpoints
/v1/meThe key's company, the key's name and its scopes. A quick check that the key works.
/v1/projectsThe company's projects - to match each to your own (for example an ORIS cost centre).
{ "projects": [ { "id": "bb2e…", "name": "Vake Residence", "name_ka": "ვაკე რეზიდენსი", "name_ru": null,
"currency": "GEL", "start_date": "2026-03-01", "end_date": null, "closed_on": null } ] }
/v1/changes?after=0&limit=100after: the last seq you have (0 for the start). limit: 1 to 500, 100 if not given.
{
"changes": [
{ "seq": 19, "entity": "purchase", "id": "3d04…", "op": "upsert", "at": "2026-10-24T12:39:15Z",
"project_id": "bb2e…",
"data": { "entity": "purchase", "id": "3d04…", "date": "2026-10-24", "item": "Cement M400",
"quantity": 12, "unit": "bag", "unit_price": 16.5, "amount": 198, "currency": "GEL",
"supplier": { "name": "Beton Service LLC", "name_ka": "შპს ბეტონ სერვისი", "code": "205123456" },
"vat": "included", "vat_rate": 18, "net": 167.8, "vat_amount": 30.2,
"waybill_no": "0712345678", "paid_on": "2026-10-24", "due_on": null,
"project": { "id": "bb2e…", "name": "Vake Residence", "currency": "GEL" }, … } }
],
"next": 19,
"more": false
}
/v1/records/{entity}/{id}One record as it is now - the same shape as a change's data. 404 if it is gone or not the company's.
/v1/syncWhat your program did with records - see Marking sent. Needs accounting:write.
/v1/paidWhat was paid in your program, marked paid in Unicraft - see Paid in your program. Needs accounting:write.
Records
Every record has entity, id, project (id, name, name_ka, name_ru, currency) and currency. Dates are YYYY-MM-DD; amounts are numbers in the project's currency. Names come in English, Georgian and Russian where they were written so.
| entity | What it is | Its own fields |
|---|---|---|
project | A project made, renamed or deleted. Deleted, everything in it is gone with it - its own records do not each come as deletions. | name, name_ka, name_ru, currency, start_date, end_date, closed_on |
purchase | A line bought for a project: materials, tools, services. Paid at once, on credit (due_on), or from a cash advance. | purchase_id (lines bought together), date, kind, item, quantity, unit, unit_price, amount, supplier (name, code: its identification code), vat, vat_rate, net, vat_amount (see VAT), waybill_no, due_on, paid_on (null: still owed), paid_from (cash_advance or null), cash_advance_id, request_id, note |
request | An approved material request, paid to the supplier of the chosen offer (its goods become purchases as they arrive). | number, status, date, supplier (name, code), paid_on, amount, vat, vat_rate, net, vat_amount, invoice_no, waybill_nos |
site_payment | Pay for the daily workers or the guards for a month, or for an equipment rental. | kind, month (YYYY-MM), paid_on, amount, rental (equipment, supplier), note |
subcontractor_payment | A payment to a subcontractor on one of their timetable items, with the retention held back - or retention given back. | kind (interim or retention_release), paid_on, amount (paid in cash), retention (held), gross, subcontractor, item, act_id (the Form 2 act it pays, if any), note |
act | A Form 2 act: the client's (money to receive) or a subcontractor's (money to pay). | party (client or subcontractor), number, period_from, period_to, date, total, retention, retention_pct, is_final, signed_on, paid_on (paid, or received), money_in_id, subcontractor |
money_in | Money received for a project: from the client, a buyer of a flat, a loan. | kind, date, amount, from, certificate_no, retention (true: retention coming back), note |
rental | An equipment rental: what is owed to its supplier as the days go by (paid, it is a site_payment). | equipment, supplier, date (start), daily_rate, counted (days: a set number of days from the start; timesheet: the days the machine is on the timesheet, a half day at half), days, months ([{ month, days, amount }] up to today), cost_to_date, note |
labour_month | A project's month of daily workers (kind: labour) or guards (guard): what is owed for it, counted as the Accounting page counts it - the daily logs' head counts times their day rate, or the timesheet where the site keeps one. Paid, it is a site_payment of the same kind and month. This is the wages to book. | kind, month (YYYY-MM), days (worker-days), amount (owed up to today), paid |
timesheet_month | One worker's month on the timesheet (ტაბელი): who earned what, for payroll and its income tax. It is a breakdown of the labour_month - do not book it as a cost a second time. Its id stays the same for that worker and month; it comes again whenever a day changes. | month (YYYY-MM), worker (name, personal_no, trade, day_rate), days (a half day counts 0.5), amount |
cash_advance | Cash given to someone to buy with (საქვეანგარიშო). What they buy with it are purchases with paid_from: "cash_advance". | date, amount, purpose, report_by, holder (name), spent, settled_on, settled_amount (+ came back, − paid back to the holder), note |
VAT. vat is included (the amount includes 18% VAT: net is before VAT, vat_amount the VAT to claim back), none (the seller charges no VAT) or null (nobody has said - net and vat_amount are null too). The amount is always what was paid.
A purchase with amount 0 has no price yet - the office prices it later, and it comes through the feed again when it does. An accounting connector should skip it until then.
Marking sent
After your program handles records, tell Unicraft. The company's accountant sees it beside each line on the Accounting page: Sent to ORIS, or the error in full, so nothing is lost quietly.
curl -X POST https://unicraft.ge/api/v1/sync \
-H "Authorization: Bearer uck_your_key" -H "Content-Type: application/json" \
-d '{ "system": "oris",
"items": [
{ "entity": "purchase", "id": "3d04…", "status": "sent", "external_id": "31" },
{ "entity": "act", "id": "9a1c…", "status": "error", "message": "Account [3110 7] is closed" }
] }'
{ "marked": 2, "of": 2 }
system names your program (oris, fina, balance or your own). status is sent, error or skipped (deliberately left out - for example a purchase with no price yet, or a kind your program does not keep; the accountant sees it as left out, not as sent). Keep your own id in external_id - for example the ORIS transaction - to update or delete it when the record changes. Marking a record again replaces its mark. Up to 500 items a call; records that are not the company's are left out of marked.
Paid in your program
When a payment is made in the accounting program, tell Unicraft and it is marked paid there too - nobody marks it again on the Accounting page.
curl -X POST https://unicraft.ge/api/v1/paid \
-H "Authorization: Bearer uck_your_key" -H "Content-Type: application/json" \
-d '{ "system": "oris",
"items": [
{ "entity": "purchase", "id": "3d04…", "paid_on": "2026-10-25", "external_id": "PAY-118" },
{ "entity": "labour_month", "id": "71be…", "paid_on": "2026-10-25", "amount": 2400, "external_id": "PAY-119" }
] }'
{ "results": [ { "entity": "purchase", "id": "3d04…", "result": "paid" },
{ "entity": "labour_month", "id": "71be…", "result": "paid" } ] }
| entity | What happens |
|---|---|
purchase | The whole purchase (every line with the same purchase_id) is marked paid on paid_on, if it was still owed. |
request | Marked paid as the accountant would: at amount, or the chosen offer's total. Its goods already received are marked paid with it; procurement is told. |
labour_month, rental | A payment of amount (or all that is still owed) is recorded. Give external_id (your payment's number): the same one sent again is not paid twice. |
paid_on is today if not given; amount is optional. Each item answers with result: paid, already_paid, not_approved (a request not approved yet), not_found (not the company's), future_date or bad_amount. The payment comes back to you in the feed (the record changed, or a new site_payment) - your program made it, so mark it skipped.
Errors
An error is JSON: { "error": { "code": "bad_key", "message": "…" } }.
| Status | code | Meaning |
|---|---|---|
| 400 | bad_after, bad_id, bad_json, bad_body, bad_item, too_long, refused | The call is not right - the message says what to change. |
| 401 | no_key, bad_key | No key, or one that is not valid or was revoked. |
| 403 | scope | The key may not do this. |
| 404 | no_route, no_entity, not_found | No such address, kind of record or record. |
| 429 | too_many | Over 600 calls a minute - wait and try again. |
| 500 | server | Something went wrong on our side - try again. |
A connector, in short
What an accounting program's connector does, every few minutes:
let after = loadLastSeq(); // 0 the first time
for (;;) {
const r = await get(`/v1/changes?after=${after}&limit=200`);
const marks = [];
for (const c of r.changes) {
try {
if (!c.data) removeEntry(c.entity, c.id); // deleted in Unicraft
else if (c.data.amount === 0) { marks.push({ entity: c.entity, id: c.id, status: 'skipped' }); continue; }
else {
const own = upsertEntry(c.data); // e.g. an ORIS transaction, by c.id
marks.push({ entity: c.entity, id: c.id, status: 'sent', external_id: String(own) });
}
} catch (e) {
marks.push({ entity: c.entity, id: c.id, status: 'error', message: e.message });
}
}
if (marks.length) await post('/v1/sync', { system: 'oris', items: marks });
saveLastSeq(after = r.next);
if (!r.more) break;
}
Which accounts a record goes to - suppliers, the bank, the cash desk, materials - is the company's own choice, set once in the connector. A purchase already in your program from an rs.ge waybill is the same purchase: match it by waybill_no and the supplier's code rather than booking it again. Questions, or building a connector for your program: [email protected].