Unicraft API Open Unicraft →

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

  1. The company's administrator opens Company → Integrations in Unicraft, makes a key and copies it. It is shown only once.
  2. Every call goes to https://unicraft.ge/api/v1/… with the key in the Authorization header.
  3. 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.

ScopeWhat the key may do
accounting:readRead the projects, the feed and single records. Every key has it.
accounting:writeAlso mark records as sent, failed or skipped (/v1/sync) and say what was paid in your program (/v1/paid). A connector needs it.
people:readAlso 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.

Endpoints

GET/v1/me

The key's company, the key's name and its scopes. A quick check that the key works.

GET/v1/projects

The 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 } ] }
GET/v1/changes?after=0&limit=100

after: 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
}
GET/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.

POST/v1/sync

What your program did with records - see Marking sent. Needs accounting:write.

What 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.

entityWhat it isIts own fields
projectA 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
purchaseA 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
requestAn 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_paymentPay 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_paymentA 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
actA 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_inMoney 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
rentalAn 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_monthA 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_monthOne 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_advanceCash 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.

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" } ] }
entityWhat happens
purchaseThe whole purchase (every line with the same purchase_id) is marked paid on paid_on, if it was still owed.
requestMarked 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, rentalA 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": "…" } }.

StatuscodeMeaning
400bad_after, bad_id, bad_json, bad_body, bad_item, too_long, refusedThe call is not right - the message says what to change.
401no_key, bad_keyNo key, or one that is not valid or was revoked.
403scopeThe key may not do this.
404no_route, no_entity, not_foundNo such address, kind of record or record.
429too_manyOver 600 calls a minute - wait and try again.
500serverSomething 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].