NodinACC integration API v1
One API, organised by business event. Source apps (NodinPMS first, then POS, HR, Pay, and outside apps) send events; NodinACC acknowledges each one and posts them as balanced journals once per closed business day.
Endpoint: POST /functions/v1/nodinacc-ingest
Auth: request header x-nodinacc-key carrying the connector key (one per connector per company, created with create_acc_connector; only its hash is stored, the plain key is shown once).
Envelope
| Field | Type | Required | Notes |
|---|---|---|---|
| api_version | string | yes | "1" |
| event_type | enum | yes | sale, payment, refund_correction, deposit, supplier_bill, payroll_run, fee_commission |
| company | string | yes | NodinACC company id; must match the connector |
| source_app | string | yes | e.g. nodinpms; must match the connector |
| object_type | string | yes | e.g. reservation_sale, payment |
| object_id | string | yes | id in the source app |
| version | int >= 1 | yes | increment when the source changes the object |
| unique_key | string | yes | company:source_app:object_type:object_id:version, must equal the parts |
| occurred_at | RFC3339 | yes | |
| business_date | YYYY-MM-DD | no | defaults to the UTC date of occurred_at |
| currency | ISO 4217 | yes | |
| payload | object | yes | see below |
| reversal_of | string | refund only | unique_key of the event being reversed |
| metadata | object | no | free-form |
Payloads
| event_type | Required fields |
|---|---|
| sale | net, tax, gross (gross = net + tax), lines[] of {code, amount, description?} |
| payment | related_object_id, payment_method, amount |
| refund_correction | amount, reason; envelope reversal_of |
| deposit | reservation_id, amount, status (received / applied / refunded), payment_method when received |
| supplier_bill | supplier_id, invoice_number, invoice_date, net, tax, gross, lines[] |
| payroll_run | period_start, period_end, gross, deductions, net (net = gross - deductions) |
| fee_commission | counterparty, fee_type, amount |
Line codes and the well-known codes (receivable, guest_ledger, output_vat, input_vat, payable, customer_deposit, salary_expense, salaries_payable, payroll_deductions, payment:<method>, fee:<type>) are mapped to ledger accounts per company in acc_account_mappings.
Acknowledgement
{ "entry_id": null, "status": "accepted", "error": null, "posted_at": null, "event_id": "..." }
| status | HTTP | Meaning |
|---|---|---|
| accepted | 202 | stored; posts with the business day |
| duplicate | 200 | same unique_key seen before; original acknowledgement returned |
| rejected | 401 / 422 | error.code and, for validation, error.fields[] |
entry_id and posted_at are filled once the day is posted (post_business_day_events).
Error codes: unauthorized, invalid_envelope, unsupported_version, unsupported_event_type, unknown_company, validation_error, unknown_account_mapping, unbalanced_entry, invalid_reversal, period_closed, version_conflict.
Rules
- The same unique_key is idempotent: stored once, acknowledged every time.
- Corrections are new events (refund_correction with reversal_of); entries are never mutated. The reversal posts an offsetting journal linked to the original.
- reversal_of must point to an event that was received; it posts only after the original is posted.
- Events post as one batch per company and business day. A day can be re-run; already posted events are skipped. Events with no mapping fail individually with unknown_account_mapping and the rest still post.
- Breaking changes go to a new major version path.
Examples
Room sale:
{ "api_version": "1", "event_type": "sale", "company": "c1", "source_app": "nodinpms",
"object_type": "reservation_sale", "object_id": "booking-1", "version": 1,
"unique_key": "c1:nodinpms:reservation_sale:booking-1:1",
"occurred_at": "2026-10-01T10:00:00+07:00", "currency": "THB",
"payload": { "net": 1000, "tax": 70, "gross": 1070,
"lines": [ { "code": "room_revenue", "amount": 1000, "description": "Deluxe, 1 night" } ] } }
Payment:
{ "api_version": "1", "event_type": "payment", "company": "c1", "source_app": "nodinpms",
"object_type": "payment", "object_id": "payment-1", "version": 1,
"unique_key": "c1:nodinpms:payment:payment-1:1",
"occurred_at": "2026-10-01T10:05:00+07:00", "currency": "THB",
"payload": { "related_object_id": "booking-1", "payment_method": "promptpay", "amount": 1070 } }
Refund as reversal:
{ "api_version": "1", "event_type": "refund_correction", "company": "c1", "source_app": "nodinpms",
"object_type": "refund", "object_id": "refund-1", "version": 1,
"unique_key": "c1:nodinpms:refund:refund-1:1", "reversal_of": "c1:nodinpms:payment:payment-1:1",
"occurred_at": "2026-10-02T09:00:00+07:00", "currency": "THB",
"payload": { "amount": 1070, "reason": "Cancelled within free window" } }