Developers · Integration guide

SLF Data API: integration guide and reference

Draft integration guide and reference for the planned SLF Data API, which is not live: through it, a company's own system would send the documents it has already issued or received to SLF Accounting for recording in its books.

Version 0.9.2-draft · updated 2026-11-04

Draft specification, version 0.9.2-draft (4 November 2026). The SLF Data API is in preparation and not live yet: until launch, every request to the base URLs below gets a 503 service_unavailable error, and no keys are issued. It is planned to open in two releases, with supplier bills and supplier payments in the second (see Two releases). Names, fields and behaviour may change before launch. Companies interested in early access can register interest at https://slf-accounting.co/contact?topic=api-access. This reference describes data, not tax treatment. Nothing here is tax or legal advice; SLF Accounting reviews what you send.

ServerBase URL
Production, for live keys (planned — not live)https://api.slf-accounting.co/v1
Sandbox, for test keys (planned — not live)https://api-sandbox.slf-accounting.co/v1

Overview

This is a draft specification. The API is not live, no keys are issued, and names, fields and behaviour may change before launch. Nothing here is a commitment to launch the API or to a launch date. Changes are listed in the Changelog.

The SLF Data API is planned and not live. When it opens, a company whose books SLF Accounting keeps will be able to connect its own system (an ERP, a point-of-sale, billing or e-commerce platform) to SLF through it. Your system would send SLF the documents it has already issued or received: customers, sales documents, adjustment notes, receipts, supplier bills, supplier payments with withholding tax, and scans. SLF would record them in your company's books, instead of receiving them on paper or in spreadsheets.

The API is designed to record documents. It does not issue them, number them or print them, it does not file tax returns, and it does not give tax advice. Document numbers come from your own system.

Throughout this reference, "contact SLF" means contacting your SLF accountant. Quote the request_id of the request concerned.

What the draft covers

  • Read your company's details and the bank accounts SLF has on record for it.
  • Create customers and update their contact details.
  • Record sales documents: quotations, invoices, tax invoices (ใบกำกับภาษี), tax invoices that are also the receipt (cash sales), credit notes (ใบลดหนี้) and debit notes (ใบเพิ่มหนี้).
  • Record receipts for payments your customers made, including tax they withheld.
  • Record supplier bills, supplier adjustment notes and supplier payments, including tax you withheld (50 ทวิ). Second release.
  • Attach PDFs and scans to those records.
  • Read a month's totals as SLF has recorded them, to reconcile with your own system.

Two releases

The API is planned to open in two releases. Once the first release is live, the paths of the second answer 404 not_found until it opens, and POST /attachments refuses object_type bill or payment with reference_not_found.

ReleaseWhat it covers
FirstYour company and bank accounts; customers; sales documents of every type, including credit and debit notes, replacement tax invoices (replaces_id) and POST /sales-documents/{id}/void; receipts, including POST /receipts/{id}/void; attachments to sales documents and receipts; GET /periods/{month}.
SecondSupplier bills (POST /bills, GET /bills, GET /bills/{id}, POST /bills/{id}/void), supplier credit and debit notes (POST /bills/{id}/notes), supplier payments (POST /payments, GET /payments, GET /payments/{id}, POST /payments/{id}/reverse), and attachments to bills and payments.

What it does not do

  • It does not issue, number, print or send documents. You issue them in your system and send SLF a record of what you issued.
  • It does not file VAT or withholding tax returns.
  • It does not decide how a transaction is taxed. SLF Accounting reviews what you send.
  • It has no batch endpoint and no webhooks in v1.

Who can use it

When the API opens, it will be for companies whose books SLF Accounting keeps. Each key will belong to one company: one tax ID and one branch. If your company is interested in early access, register interest. Registering does not give access yet.

Quickstart: a first integration, step by step

A walk-through of the draft design. Until launch every /v1 request to the sandbox gets a 503 service_unavailable error and no keys are issued, so these commands return that error today; the responses show what this draft specifies. Each request and response below is the same example shown under its endpoint further down.

This walk-through records one sale from start to finish in the sandbox: check the key, create the customer, record the invoice, record the payment, attach the scan, then check the month. The commands use curl; any HTTP client works the same way.

Step 1. Get a test key and keep it out of your code

When the API opens, SLF issues your company a test key (starting slf_test_) for the sandbox and later a live key (starting slf_live_) for production; see Environments and keys. Put the key in an environment variable or a secret store, never in source code.

export SLF_API_KEY="slf_test_…"   # the key SLF gave you

Step 2. Check the key

Read the company the key belongs to. If this returns your company's name and tax ID, the key and the connection work. A 401 unauthenticated means the key is missing or wrong.

curl 'https://api-sandbox.slf-accounting.co/v1/company' \
  -H "Authorization: Bearer $SLF_API_KEY"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "name": "Example Trading Co., Ltd.",
  "name_local": "บริษัท ตัวอย่าง จำกัด",
  "tax_id": "0999000000019",
  "branch_no": "00000",
  "vat_registered": true
}

Also call GET /bank-accounts to see your company's accounts and their bnk_ ids: you will refer to them when you record which account a payment went to. Accounts can be added, edited and deleted by SLF or in your company's client portal, so an id can disappear and the account behind an id can change. Read the list again before you use an id rather than storing the ids once; see Bank accounts.

Step 3. Create the customer

Send the customer once, with your own customer id as external_id, and store the cus_ id that comes back next to your customer record. Every POST carries a new Idempotency-Key; resend the same key only when you retry the same request after a timeout (see Idempotency and duplicates).

curl 'https://api-sandbox.slf-accounting.co/v1/customers' \
  -X POST \
  -H "Authorization: Bearer $SLF_API_KEY" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "external_id": "CUST-00412",
  "name": "Sample Retail Co., Ltd.",
  "name_local": "บริษัท ตัวอย่างค้าปลีก จำกัด",
  "country": "TH",
  "tax_id": "0999000000027",
  "branch_no": "00000",
  "address": "88 Example Road, Pathum Wan, Bangkok 10330",
  "email": "accounts@sample-retail.example",
  "phone": "+66 2 000 0000",
  "notes": "Send statements to the accounts team."
}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
  "id": "cus_7Hc4Nf2Pq9Xz",
  "external_id": "CUST-00412",
  "name": "Sample Retail Co., Ltd.",
  "name_local": "บริษัท ตัวอย่างค้าปลีก จำกัด",
  "country": "TH",
  "tax_id": "0999000000027",
  "branch_no": "00000",
  "address": "88 Example Road, Pathum Wan, Bangkok 10330",
  "email": "accounts@sample-retail.example",
  "phone": "+66 2 000 0000",
  "notes": "Send statements to the accounts team.",
  "created_at": "2026-10-15T09:12:44+07:00"
}

Step 4. Record the invoice you issued

When your system issues a document, send it with the number, date, lines and totals exactly as printed. SLF recomputes the totals from the lines; a difference beyond rounding is refused with totals_mismatch and the expected figures, so you can see which side is wrong. reference is optional: use it for your job, project or order number to group documents.

curl 'https://api-sandbox.slf-accounting.co/v1/sales-documents' \
  -X POST \
  -H "Authorization: Bearer $SLF_API_KEY" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "type": "invoice",
  "external_id": "SO-2026-10-0187",
  "number": "INV-2026-0187",
  "issue_date": "2026-10-15",
  "due_date": "2026-11-14",
  "customer_id": "cus_7Hc4Nf2Pq9Xz",
  "currency": "THB",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Please pay by bank transfer within 30 days.",
  "reference": "PRJ-2026-031",
  "lines": [
    {
      "name": "Accounting system setup",
      "description": "Chart of accounts, opening balances and user training",
      "quantity": "1",
      "unit_price": "12500.00",
      "vat_category": "standard",
      "wht_rate": "3",
      "item_code": "SVC-SETUP",
      "charge_role": "own_supply"
    },
    {
      "name": "Monthly support",
      "description": "October to December 2026",
      "quantity": "3",
      "unit_price": "2150.75",
      "vat_category": "standard",
      "wht_rate": "3",
      "item_code": "SVC-SUPPORT",
      "charge_role": "own_supply"
    },
    {
      "name": "Government registration fee paid on your behalf",
      "quantity": "1",
      "unit_price": "1500.00",
      "vat_category": "none",
      "wht_rate": "0",
      "charge_role": "disbursement",
      "supplier_invoice_billed_to": "customer",
      "pass_through_cost": "1500.00"
    }
  ],
  "totals": {
    "subtotal": "20452.25",
    "vat_total": "1326.66",
    "total": "21778.91",
    "wht_total": "568.57",
    "net_to_pay": "21210.34"
  }
}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
  "id": "doc_4Fp9Km2Qx7Ld",
  "type": "invoice",
  "external_id": "SO-2026-10-0187",
  "number": "INV-2026-0187",
  "issue_date": "2026-10-15",
  "due_date": "2026-11-14",
  "valid_until": null,
  "customer_id": "cus_7Hc4Nf2Pq9Xz",
  "bill_to_address": null,
  "currency": "THB",
  "fx_rate": null,
  "fx_date": null,
  "payment_terms": null,
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Please pay by bank transfer within 30 days.",
  "reference": "PRJ-2026-031",
  "related_invoice_id": null,
  "replaces_id": null,
  "original_document_ids": null,
  "reason_code": null,
  "reason_text": null,
  "original_value": null,
  "correct_value": null,
  "lines": [
    {
      "name": "Accounting system setup",
      "description": "Chart of accounts, opening balances and user training",
      "quantity": "1",
      "unit_price": "12500.00",
      "vat_category": "standard",
      "wht_rate": "3",
      "item_code": "SVC-SETUP",
      "account_code": null,
      "charge_role": "own_supply",
      "supplier_invoice_billed_to": null,
      "pass_through_cost": null,
      "pre_vat_amount": "12500.00",
      "vat_rate": "7",
      "vat_amount": "875.00",
      "wht_amount": "375.00"
    },
    {
      "name": "Monthly support",
      "description": "October to December 2026",
      "quantity": "3",
      "unit_price": "2150.75",
      "vat_category": "standard",
      "wht_rate": "3",
      "item_code": "SVC-SUPPORT",
      "account_code": null,
      "charge_role": "own_supply",
      "supplier_invoice_billed_to": null,
      "pass_through_cost": null,
      "pre_vat_amount": "6452.25",
      "vat_rate": "7",
      "vat_amount": "451.66",
      "wht_amount": "193.57"
    },
    {
      "name": "Government registration fee paid on your behalf",
      "description": null,
      "quantity": "1",
      "unit_price": "1500.00",
      "vat_category": "none",
      "wht_rate": "0",
      "item_code": null,
      "account_code": null,
      "charge_role": "disbursement",
      "supplier_invoice_billed_to": "customer",
      "pass_through_cost": "1500.00",
      "pre_vat_amount": "1500.00",
      "vat_rate": "0",
      "vat_amount": "0.00",
      "wht_amount": "0.00"
    }
  ],
  "totals": {
    "subtotal": "20452.25",
    "vat_total": "1326.66",
    "total": "21778.91",
    "wht_total": "568.57",
    "net_to_pay": "21210.34"
  },
  "attachments": [],
  "status": "open",
  "paid_amount": "0.00",
  "credited_amount": "0.00",
  "outstanding": "21778.91",
  "voided_at": null,
  "void_reason": null,
  "replaced_by_id": null,
  "warnings": [],
  "created_at": "2026-10-15T14:02:11+07:00"
}

Store the doc_ id. outstanding is what the customer still owes.

Step 5. Record the payment

When the customer pays, record a receipt against the invoice: the money that arrived and any tax the customer withheld. Here the customer withheld 3% on the service lines, so amount_received plus wht_amount equals the invoice total and the invoice becomes paid.

curl 'https://api-sandbox.slf-accounting.co/v1/receipts' \
  -X POST \
  -H "Authorization: Bearer $SLF_API_KEY" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "external_id": "RCV-2026-10-0233",
  "number": "RE-2026-0233",
  "document_id": "doc_4Fp9Km2Qx7Ld",
  "payment_date": "2026-10-30",
  "amount_received": "21210.34",
  "wht_amount": "568.57",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Customer withheld 3% on the service lines; 50 ทวิ certificate received."
}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
  "id": "rcp_3Np8Gz5Kd1Vt",
  "external_id": "RCV-2026-10-0233",
  "number": "RE-2026-0233",
  "document_id": "doc_4Fp9Km2Qx7Ld",
  "payment_date": "2026-10-30",
  "amount_received": "21210.34",
  "wht_amount": "568.57",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Customer withheld 3% on the service lines; 50 ทวิ certificate received.",
  "gross_amount": "21778.91",
  "vat_amount": "1326.66",
  "currency": "THB",
  "fx_rate": null,
  "attachments": [],
  "warnings": [],
  "voided_at": null,
  "void_reason": null,
  "created_at": "2026-10-30T17:08:26+07:00"
}

Step 6. Attach the scan

Upload the PDF of the invoice and link it to the document. Upload files after the record exists; the same file on the same record is stored once.

curl 'https://api-sandbox.slf-accounting.co/v1/attachments' \
  -X POST \
  -H "Authorization: Bearer $SLF_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F 'file=@INV-2026-0187.pdf' \
  --form-string 'object_type=sales_document' \
  --form-string 'object_id=doc_4Fp9Km2Qx7Ld' \
  --form-string 'kind=document_copy'

The response is the attachment record with its att_ id, sha256 and size; see Attachments.

Step 7. Check the month

After the month ends, read the month as SLF has recorded it and compare the totals with your own system. If they differ, find the missing or extra documents in the lists (filter by date), or contact SLF.

curl 'https://api-sandbox.slf-accounting.co/v1/periods/2026-09' \
  -H "Authorization: Bearer $SLF_API_KEY"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "month": "2026-09",
  "status": "vat_filed",
  "filings": [
    {
      "form": "pp30",
      "filed": true
    },
    {
      "form": "pnd3",
      "filed": true
    },
    {
      "form": "pnd53",
      "filed": true
    },
    {
      "form": "pnd1",
      "filed": false
    }
  ],
  "sales": {
    "document_count": 64,
    "taxable_value": "412350.00",
    "zero_rated_value": "38900.00",
    "exempt_value": "0.00",
    "unclassified_value": "0.00",
    "vat": "28864.50"
  },
  "purchases": {
    "document_count": 41,
    "taxable_value": "158200.00",
    "vat": "11074.00"
  },
  "out_of_scope_value": "12750.00",
  "quarantined": {
    "count": 0,
    "value": "0.00"
  },
  "withholding": {
    "pnd3_amount": "1250.00",
    "pnd53_amount": "4830.00"
  }
}

What to build next

  • Retries: on a timeout, a 429 or a 5xx, resend the same request with the same Idempotency-Key; on any other 4xx, fix the request and send it with a new key. See Errors and retries.
  • Supplier side, in the second release: record supplier documents with POST /bills and what you paid with POST /payments, including any tax you withheld.
  • Corrections: credit and debit notes, and void-and-reissue, follow fixed rules. Read Corrections before you build them.
  • Going live: agree a start date with SLF, switch to the live key and base URL, and send every document your system issues dated from that day; documents dated earlier are refused. See Environments and keys.

How the pieces fit

Not live yet (draft specification).

Each resource matches a kind of document you already handle. Records refer to each other by the ids the API returns.

Your documentWhere it goesNotes
Customer recordPOST /customersCreate once; then refer to it by id or by your external_id.
Quotation (ใบเสนอราคา)POST /sales-documents, type: quotationFor reference; creates nothing owed.
Invoice that is not a tax invoice (ใบแจ้งหนี้)type: invoicePaid with receipts.
Tax invoice (ใบกำกับภาษี)type: tax_invoicePaid with receipts.
Tax invoice that is also the receipt (cash sale)type: tax_invoice_receiptRecorded as paid; no separate receipt.
Credit note (ใบลดหนี้) or debit note (ใบเพิ่มหนี้)type: credit_note or debit_noteRefers to one or more original tax invoices, or to the receipt of a paid invoice whose lines carry VAT.
Receipt (ใบเสร็จรับเงิน)POST /receiptsAgainst an invoice, tax invoice or debit note.
Document received from a supplierPOST /billsSecond release. Tax invoice, receipt, invoice, import entry, foreign invoice or other.
Supplier credit or debit notePOST /bills/{id}/notesSecond release. Recorded against the bill it adjusts.
Payment to suppliers, with withholding tax certificate (50 ทวิ)POST /paymentsSecond release. Allocated to one or more bills.
PDF or scanPOST /attachmentsAttached to a sales document or receipt; to a bill or payment from the second release.
Month totalsGET /periods/{month}Read-only; for reconciliation.

A typical flow

These steps describe how the API is designed to be used once it is live. Nothing can be sent yet.

  1. Call GET /company to check your key, and GET /bank-accounts to get the bnk_ ids of the accounts SLF has on record (read the list again before you use an id).
  2. Create each customer once with POST /customers and store the returned cus_ id with your own customer record.
  3. When your system issues a sales document, record it with POST /sales-documents: the number, date, lines and totals exactly as printed.
  4. When a customer pays, record a receipt with POST /receipts, including any tax the customer withheld.
  5. If a document needs correcting, record a credit or debit note, or void it and record a replacement (see Corrections).
  6. When a supplier document arrives, record it with POST /bills (second release).
  7. When you pay suppliers, record the payment with POST /payments, allocated to bills, with any tax you withheld (second release).
  8. Attach the PDF or scan of each document with POST /attachments.
  9. After the month ends, compare GET /periods/{month} with your own system's totals and contact SLF about any difference.

Every POST needs an Idempotency-Key header, and every record you create needs your external_id. Together they make retries safe; see Idempotency and duplicates.

Services: invoice first, tax invoice when paid

If your system issues an invoice with VAT lines and the tax invoice only when the customer pays, as is common for services:

  1. Record the invoice. It books no VAT.
  2. Record a receipt for each payment. A receipt against an invoice whose lines carry VAT books that VAT in the month of its payment_date; its vat_amount shows the share. A receipt for less than the document still owes carries the warning short_payment, which is expected for a part payment and needs no action. This follows the rule for services in s.78/1(1): VAT falls due when payment is received, unless a tax invoice was issued or the service was used first. It is not the rule for goods (see Tax notes): record a sale of goods as a tax_invoice or tax_invoice_receipt, or ask SLF.
  3. If your system prints a separate tax invoice for the payment, you may record it as a tax_invoice with related_invoice_id, once the invoice is paid in full. It books no VAT again, because the receipts already did.

Tax invoices issued for each part payment are not supported in the first release. A tax_invoice or tax_invoice_receipt without related_invoice_id is refused with tax_invoice_duplicates_receipt when the same customer has a receipt that is not voided, against an invoice with VAT lines, whose payment_date is in the same month as the new document's issue_date and whose gross_amount equals the new document's total: that receipt has already booked the VAT. The other way round, a receipt recorded after such a tax invoice, for the same amount and with its payment_date in the month of the tax invoice's issue_date, is accepted, because the money arrived, but carries the warning possible_double_vat for SLF to review.

To change the value of such a sale afterwards, the credit or debit note refers to the receipt (see Corrections). While a tax_invoice with related_invoice_id stands on the sale, no credit or debit note is accepted on it; contact SLF.

Fields by document type

FieldApplies to
valid_untilquotation
due_dateinvoice, tax_invoice, debit_note
related_invoice_idtax_invoice
replaces_idtax_invoice, tax_invoice_receipt
original_document_ids, reason_code, reason_text, original_value, correct_valuecredit_note, debit_note, and required for them
All other fieldsEvery type

A field sent for a type it does not apply to is refused with validation_failed (invalid_value). A company that is not VAT-registered can record only quotation and invoice; any other type is refused with validation_failed (invalid_value on /type).

Environments and keys

Not live yet (draft specification).

EnvironmentBase URLKey prefixWhat happens to the data
Sandbox (test keys)https://api-sandbox.slf-accounting.co/v1slf_test_Written to a separate test company that SLF sets up for yours. It starts with your company's details, its bank accounts (only the last four digits of each number), the account codes you may use and your foreign-currency settings, under different ids, and with no customers, documents or other transactions. Ids from the sandbox do not exist in production. Nothing reaches your books, SLF does not review sandbox records, and sandbox data is not backed up.
Production (live keys)https://api.slf-accounting.co/v1slf_live_Once the API is live: recorded in your company's books at SLF.

Both hostnames are planned and not live. Until launch, every request under /v1 gets a 503 service_unavailable error, and no keys are issued. The hostname itself (https://api.slf-accounting.co/) redirects to this guide.

Getting keys

When the API opens, keys are issued by SLF to companies whose books it keeps. Each key is bound to one company record at SLF, which has one tax ID and one branch number. If SLF corrects either, GET /company shows the new value and the key keeps working. Test keys and live keys are separate, and each works only on its own environment.

Everything a key records or reads belongs to that company record. All keys of it see the same customers and documents, and number and external_id must be unique within it.

SLF can issue a key, test or live, only when all of these are in place:

  • Your company is an active client of SLF, with a company profile at SLF set up for Thai tax documents.
  • SLF has your company's 13-digit tax ID and 5-digit branch number on record.
  • SLF holds your company as one record. In the first release, no key is issued to a company that SLF holds as several records under the same tax ID, such as one record per branch. If your company issues documents from several branches, ask SLF how they are set up.
  • Your company profile's VAT setting (vat_registered in GET /company) and SLF's own record of whether your company is VAT-registered are both set and agree.
  • SLF does not convert your company's foreign-currency documents to THB at a rate of its own (see Foreign currency in Conventions).
  • Your company has fewer than two keys for that environment, counting suspended keys but not revoked keys or old keys already replaced by a rotation. It also has fewer than three keys for that environment when the old keys still working in the 7 days after a rotation are counted too.

A live key also needs your company's written acceptance of SLF's API terms, and a review by SLF of sample documents from your system, in which SLF records which type each of your documents is. Your first live key also needs the load of your unpaid invoices described under Going live below.

Testing in the sandbox

To test period_filed and period_closed, ask SLF to mark a sandbox month as filed or closed. Sandbox records are never copied to production. If your bank accounts at SLF change, ask SLF to update the sandbox.

The date rules apply in the sandbox too (see Dates in Conventions). Ask SLF for the start date of your test key, and for the VAT registration date the test company has on record, if any, and date your test records accordingly.

Ask SLF for a reset when you need a clean start. A reset does not clear your test company in place: it gives you a new test company, with new ids and a new test key, and the old test keys stop working. SLF may also rebuild the whole sandbox at once; every test company then gets new ids and new test keys.

Going live

Agree a start date with SLF. From that date, send every document your system issues. Documents dated earlier stay with SLF as before and are not loaded through the API in v1: a sales document whose issue_date, or a receipt whose payment_date, is before the start date is refused with validation_failed (out_of_range). When the second release opens, agree with SLF from which date your system sends the documents it receives.

Ask SLF, too, which VAT registration date it has on record for your company: a tax_invoice or tax_invoice_receipt dated before it is refused with validation_failed (out_of_range). The API has no field that returns either date.

Your live key is issued on the start date or later, never before it. If it arrives after the start date, send the documents dated from the start date onward once it arrives. Before it issues your first live key, and on the start date or later, SLF loads the invoices you issued before the start date that are not paid yet, with your numbers, or confirms that there are none, so that every invoice dated before the start date is covered. When one of them is paid, find it with GET /sales-documents?number=… and record the receipt against its id. If SLF issues your company another live key later, its start date cannot be after the day your first live key was issued.

A tax invoice or cash sale that SLF has already entered by hand, in any month, is refused if you send it too (same number and the same total in THB), so that it is not counted twice. The refusal is 409 recorded_by_slf, with existing_id: null because SLF's entry is not a record the API returns; contact SLF.

{
  "error": {
    "code": "recorded_by_slf",
    "message": "SLF has already entered tax invoice TIV-2026-0401 (8560.00) by hand. Contact SLF.",
    "param": "/number",
    "details": [],
    "request_id": "req_4a7d2e9c1f6b3a85",
    "existing_id": null
  }
}

Looking after keys

  • A key is shown once, when SLF issues it. SLF stores only a hash of it and its last four characters, and cannot show the key again.
  • Send the key only in the Authorization header. Never put it in a URL, a log, a web page or a mobile app.
  • Use https:// only. Once the API is live, a request to /v1 over plain http:// is refused with 403 forbidden and is not redirected, because the key has already crossed the network unencrypted. If that happens, ask SLF to revoke the key.
  • To rotate a key, ask SLF for a new one. The old and the new key both work for an overlap period of 7 days, so you can switch without downtime; after that the old key gets 401 unauthenticated. One switch-over runs at a time: the new key cannot itself be replaced while the old one still works. If a new key goes astray, ask SLF to revoke it and issue another.
  • SLF can limit a key to reading, or to the IP addresses you name. A request outside those limits gets 403 forbidden. Tell SLF before the outgoing IP address of your system changes.
  • If a key may have leaked, ask SLF to revoke it. Revocation takes effect immediately.
  • A missing, unknown or revoked key, one SLF has suspended, or an old key past its overlap period returns 401 unauthenticated. A valid key used for something it may not do returns 403 forbidden.

Making requests

Not live yet (draft specification).

Send HTTPS requests to the base URL followed by the path of the operation, such as https://api-sandbox.slf-accounting.co/v1/customers. Request and response bodies are JSON in UTF-8, except file uploads and downloads. The API is served over HTTPS only: once it is live, a request over plain http:// is refused with 403 forbidden, never redirected.

HeaderDirectionWhenValue
AuthorizationRequestAlwaysBearer followed by a space and your key.
Content-TypeRequestWith a bodyapplication/json; charset=utf-8, or multipart/form-data for POST /attachments.
Idempotency-KeyRequestEvery POSTA unique value per request, 1 to 255 visible ASCII characters. A UUID works well.
Request-IdResponseAlwaysSLF's id for the request: an opaque string of up to 64 characters. Quote it when you contact SLF. A replayed response carries the id of the first request.
Idempotent-ReplayedResponseOn a replayed POSTtrue: the response is the stored response of an earlier request with the same Idempotency-Key; nothing new was recorded.
Retry-AfterResponseWith 429, sometimes 503Seconds to wait before retrying.

Example: create a customer

Illustrative only: until launch every /v1 request to the sandbox gets a 503 service_unavailable error, and no keys are issued. The response shows what this draft specifies.

curl https://api-sandbox.slf-accounting.co/v1/customers \
  -X POST \
  -H "Authorization: Bearer $SLF_API_KEY" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Idempotency-Key: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93" \
  -d '{
    "external_id": "CUST-00412",
    "name": "Sample Retail Co., Ltd.",
    "name_local": "บริษัท ตัวอย่างค้าปลีก จำกัด",
    "country": "TH",
    "tax_id": "0999000000027",
    "branch_no": "00000",
    "address": "88 Example Road, Pathum Wan, Bangkok 10330",
    "email": "accounts@sample-retail.example",
    "phone": "+66 2 000 0000",
    "notes": "Send statements to the accounts team."
  }'

Response:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Request-Id: req_8f2c1a9e5b7d4c30

{
  "id": "cus_7Hc4Nf2Pq9Xz",
  "external_id": "CUST-00412",
  "name": "Sample Retail Co., Ltd.",
  "name_local": "บริษัท ตัวอย่างค้าปลีก จำกัด",
  "country": "TH",
  "tax_id": "0999000000027",
  "branch_no": "00000",
  "address": "88 Example Road, Pathum Wan, Bangkok 10330",
  "email": "accounts@sample-retail.example",
  "phone": "+66 2 000 0000",
  "notes": "Send statements to the accounts team.",
  "created_at": "2026-10-15T09:12:44+07:00"
}

A 201 or 200 response means the record is stored. Keep the returned id with your own record; you need it to refer to the customer later. Read your key from the environment or a secret store, as in the example, rather than writing it into your code.

Example: record a sales document

# invoice.json holds the document as in the createSalesDocument example
curl https://api-sandbox.slf-accounting.co/v1/sales-documents \
  -X POST \
  -H "Authorization: Bearer $SLF_API_KEY" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Idempotency-Key: 7c3e9a1f-5b2d-4e8c-a6f0-3d9b1e7c4a25" \
  --data @invoice.json

Example: page through a list

curl "https://api-sandbox.slf-accounting.co/v1/sales-documents?issue_date_from=2026-10-01&issue_date_to=2026-10-31&limit=100&cursor=cur_8Tn3XqL5Vd" \
  -H "Authorization: Bearer $SLF_API_KEY"

Leave out cursor for the first page. Repeat with each next_cursor until it is null.

Example: upload an attachment

curl https://api-sandbox.slf-accounting.co/v1/attachments \
  -X POST \
  -H "Authorization: Bearer $SLF_API_KEY" \
  -H "Idempotency-Key: 0b6f3c1e-8a2d-4f7b-9c5e-1d4a7b2e9f60" \
  -F "file=@TIV-2026-0412.pdf;type=application/pdf" \
  -F "object_type=sales_document" \
  -F "object_id=doc_9Rt3Vb6Nz1Hs" \
  -F "kind=document_copy"

Do not set Content-Type yourself for an upload; your HTTP client adds it with the multipart boundary.

Conventions

Not live yet (draft specification).

TopicRule
Field namesLower case with underscores, such as issue_date. Ignore response fields you do not know; new ones can be added at any time. Unknown fields in requests are ignored.
Empty valuesOptional fields without a value are returned as null. So is a value SLF holds that does not fit the field's format, such as a currency on record that is not a three-letter code: it is returned as null rather than guessed. Before that check, a currency code is put in upper case, and a branch number SLF holds as 6 digits starting with 0 is given as its last 5 digits (000012 becomes 00012).
MoneyA string with exactly two decimals, such as "1070.00". Never a JSON number. Negative amounts are refused; use an adjustment note instead.
QuantitiesA decimal string with up to 4 decimals, greater than zero, such as "2.5".
Rateswht_rate is a percentage string from a fixed list: "0", "1", "2", "3", "5", "10", "15". You never send a VAT rate; it follows from vat_category.
DatesYYYY-MM-DD: the calendar date printed on the document, in Thailand. Buddhist-era years are refused: send 2026-10-15, not 2569-10-15. "Today" means today in Thailand: a tax_invoice or tax_invoice_receipt dated after today, or a receipt whose payment_date is after today, is refused with validation_failed (out_of_range), and so is a sales document or receipt dated before your start date (see Going live).
Timestampscreated_at and similar fields are RFC 3339 with an offset, such as 2026-10-15T14:02:11+07:00. Only the server sets them.
CurrencyISO 4217 code; THB if omitted. For any other currency, fx_rate (THB per one unit, up to 6 decimals) and fx_date are required. Amounts are in the document's own currency.
Thai tax IDs13 digits, no dashes or spaces. The last digit must be the correct check digit; otherwise the detail code is invalid_tax_id.
Branch numbers5 digits. 00000 is the head office.
IdsOpaque strings with a type prefix: cus_ customer, doc_ sales document, rcp_ receipt, bil_ bill, bnt_ supplier adjustment note, pay_ payment, att_ attachment, bnk_ bank account. Do not parse them. Their length varies.
TextPlain text. Line breaks are kept; HTML is not rendered.
AtomicityEach request is all-or-nothing: if any field, line or allocation is refused, nothing is recorded.
Records SLF enteredLists and GET return every record of that kind in your company's books at SLF, including records SLF entered itself, such as credit and debit notes. Those have external_id: null and cannot be voided or reversed through the API (void_not_allowed). Two kinds are not returned at all: pro-forma documents SLF recorded, and receipts SLF recorded against a quotation rather than an invoice; GET answers 404 not_found for them. Separately, SLF may have entered a document's VAT by hand: a VAT entry typed from the printed document, with no sales document record behind it. Such hand entries are never returned by lists or GET; the API refers to them only in recorded_by_slf, recorded_by_slf_posted and recorded_by_slf_filed.

Pagination

List operations take limit (1 to 100, default 25) and cursor. The response is a list envelope, { "data": [...], "next_cursor": ... }, ordered by created_at, newest first. Lists include voided and reversed records. When next_cursor is not null, pass it as cursor to get the next page; null means this is the last page.

A limit or cursor that cannot be used is refused with 422 validation_failed, with one entry in details for each problem, whose param is /limit or /cursor:

  • limit that is not a whole number (such as 2.5, 1e2 or an empty value), or that is given more than once: invalid_format.
  • limit that is a whole number outside 1 to 100: out_of_range.
  • cursor that is empty, longer than 512 characters or given more than once, or that is not a next_cursor this list returned to your company (for example a cursor that was edited, or one taken from another list): invalid_value. Leave cursor out for the first page.
{
  "data": [
    {
      "id": "cus_7Hc4Nf2Pq9Xz",
      "external_id": "CUST-00412",
      "name": "Sample Retail Co., Ltd."
    }
  ],
  "next_cursor": "cur_8Tn3XqL5Vd"
}

The record in this example is shortened; real list responses return complete records.

How the API checks totals

You send each line's quantity and unit_price, and the totals as printed. The API computes the line amounts and totals with the rules below and compares yours with its own. Your subtotal and wht_total may differ from the API's by up to 0.01 per line on the document (0.03 on a three-line document, for example), and your vat_total by up to 0.01 per line that carries VAT (standard or standard_incl). A larger difference is refused with totals_mismatch. Your total must equal your subtotal plus your vat_total exactly, and net_to_pay, if sent, your total minus your wht_total exactly. This is the API's own check, not a legal rule.

FigureHow the API computes it
Line pre-VAT amount (all categories except standard_incl)round2(quantity × unit_price)
Line VAT, standardround2(pre_vat × 7 / 100)
Line VAT and pre-VAT amount, standard_inclgross = round2(quantity × unit_price); VAT = round2(gross × 7 / 107); pre-VAT = gross − VAT
Line VAT, zero, exempt, none0.00
Line withholding taxround2(pre_vat × wht_rate / 100)
subtotal, vat_total, wht_totalSums of the rounded line figures (subtotal covers all categories)
totalsubtotal + vat_total
net_to_paytotal − wht_total

round2 rounds the exact decimal value to the nearest 0.01, halves rounded up: 14.50 × 7 / 100 = 1.015 becomes 1.02. The API computes in exact decimals, not in binary floating point.

Example: a cash sale with VAT-inclusive prices (standard_incl).

LineGrossVATPre-VAT
Printer paper A4, 3 boxes × 430.001290.00round2(1290.00 × 7 / 107) = 84.391205.61
Toner cartridge, 1 × 2890.002890.00round2(2890.00 × 7 / 107) = 189.072700.93
Document totalstotal 4180.00vat_total 273.46subtotal 3906.54

What is recorded

Within the tolerance, the document is recorded with the API's line figures, except that the VAT you printed stands: your vat_total is recorded, and any difference between it and the sum of the API's line VAT is put on the last line that carries VAT. On a standard_incl line the difference moves between that line's VAT and its pre-VAT amount, and the line's gross stays the same. The response returns the recorded figures in lines and totals, and receipts share out this recorded VAT (see vat_amount on Receipt). Your subtotal and wht_total are not recorded as sent: the recorded ones are the sums of the API's line figures, and the recorded total is that subtotal plus your vat_total. If your subtotal differs from the API's, the recorded total, and so outstanding, differ from your printed total by the same amount; check totals in the response before you record a receipt for the full amount.

Example: a tax_invoice with 12 lines of 1 × 14.50, all standard. Each line's VAT is round2(1.015) = 1.02, so the API's lines add up to 12.24; the tolerance for vat_total is 0.12 (12 lines × 0.01).

Your system printsYour vat_totalRecordedWarning
VAT on the document's value: 174.00 × 7 / 10012.18vat_total 12.18; the last line's vat_amount is 0.96 (1.02 − 0.06)None
VAT per line, rounded line by line and added up12.24vat_total 12.24vat_arithmetic_review: 12.24 is 0.06 away from 7% of 174.00

SLF also runs an arithmetic check on the VAT a record books when it is recorded, whether a document such as a tax_invoice or a receipt against an invoice with VAT lines: among other things, that the VAT is within 0.05 of 7% of the value it is charged on. A record that does not pass is still recorded, but its VAT is held back from the month's figures until SLF resolves it (quarantined in GET /periods/{month}). A sales document that does not pass carries the warning vat_arithmetic_review; a receipt carries no warning for it, so watch quarantined. This check, too, is SLF's own, not a legal rule.

Foreign currency

Send amounts in the document's own currency, with the exchange rate you used: fx_rate is THB per one unit of the currency, and fx_date is the date of that rate. A tax_invoice, tax_invoice_receipt, credit_note or debit_note in a foreign currency is accepted only if SLF holds the Director-General's approval for your company (see Tax notes); otherwise it is refused with foreign_currency_tax_invoice_not_allowed. Example: an invoice in US dollars to a customer abroad.

{
  "type": "invoice",
  "external_id": "SO-2026-10-0192",
  "number": "INV-2026-0192",
  "issue_date": "2026-10-27",
  "due_date": "2026-11-26",
  "customer_id": "cus_3Wd8Lm5Tb2Kr",
  "currency": "USD",
  "fx_rate": "32.4512",
  "fx_date": "2026-10-27",
  "payment_terms": "tt",
  "bank_account_id": "bnk_9Ze3Lp6Qr2Dn",
  "lines": [
    {
      "name": "Teak serving tray, large",
      "quantity": "120",
      "unit_price": "18.75",
      "vat_category": "zero",
      "item_code": "TRAY-L"
    },
    {
      "name": "Teak serving tray, small",
      "quantity": "200",
      "unit_price": "9.40",
      "vat_category": "zero",
      "item_code": "TRAY-S"
    }
  ],
  "totals": {
    "subtotal": "4130.00",
    "vat_total": "0.00",
    "total": "4130.00"
  }
}

Both lines are zero-rated, so the response carries the warning zero_rated_review for each of them (see Warnings and review).

For some companies, SLF converts foreign-currency documents to THB for VAT at a rate of its own rather than your fx_rate, under a rule set up for that company (for example a rate taken from the bank statement, or a month-end rate). For such a company, the THB figures SLF records, and so GET /periods/{month}, can differ from your amounts × your fx_rate, and a tax_invoice or tax_invoice_receipt in a foreign currency is refused with fx_basis_unavailable. In the first release SLF does not issue keys to these companies; ask SLF whether this applies to yours.

Idempotency and duplicates

Not live yet (draft specification).

Networks fail, and a request can succeed even when your system never sees the response. Two mechanisms make it safe to send again.

The Idempotency-Key header

  • Every POST must carry an Idempotency-Key header of 1 to 255 visible ASCII characters. Without one, or with a longer value or other characters, the request is refused with 400 idempotency_key_missing.
  • A key belongs to the API key that sent it. "The same request" means the same method, path and body. JSON bodies are compared after parsing, so whitespace and the order of fields do not matter. A body that is not valid JSON, or is nested more than 64 levels deep, is compared byte for byte.
  • For POST /attachments, the body is compared by the file's SHA-256 and file name and by the object_type, object_id and kind fields; the multipart boundary is ignored.
  • Within 24 hours, the same request with the same key returns the first response again, with the same status, body and Request-Id, plus the header Idempotent-Replayed: true, and creates nothing new.
  • The same key with a different body is refused with 409 idempotency_key_reused. That refusal is not stored, so the first response stays the one that is replayed.
  • Responses are stored for 24 hours, 4xx refusals included (invalid_json too). Not stored: 429 and 5xx responses; 401 and 403; 409 idempotency_key_reused; and refusals made before the request body is read, which are 400 idempotency_key_missing, 404 or 405 for a path or method the API does not have, 413 for a request over the size limit, and 415 for an unsupported Content-Type. A 415 for an uploaded file whose content is not an allowed type is stored like other 4xx refusals.
  • A request that ended in 429 or a 5xx error is not stored, so retrying it with the same key runs it again. If the first attempt did complete (for example, the connection dropped after SLF recorded it), the retry returns the stored response.
  • If a request with the same key is still being processed, the second one is refused with 409 idempotency_key_in_use. Wait a few seconds and retry with the same key.
  • Generate a new key for each new request, store it with the outgoing record, and reuse it only to retry that same request.

Your own ids

Every record you create, except attachments, carries your own external_id: your system's id for it. It must be unique per resource type within your company, including records you voided or reversed: a voided record keeps its external_id, so a record you send again after voiding needs a new one. A second create with an external_id that already exists is refused with 409 duplicate_external_id, and error.existing_id holds the id of the record already there. This protects you after the 24-hour idempotency window too.

{
  "error": {
    "code": "duplicate_external_id",
    "message": "A sales document with external_id SO-2026-10-0187 already exists.",
    "param": "/external_id",
    "details": [],
    "request_id": "req_5b8e2d1c9f4a7e60",
    "existing_id": "doc_4Fp9Km2Qx7Ld"
  }
}

Other duplicate checks

CheckResponseApplies to
Same number as another invoice, tax invoice, cash sale, credit note or debit note that is not voided, including a pro-forma document SLF recorded (quotations are checked only against quotations)409 duplicate_numberSales documents
Same number and the same total in THB as a VAT entry SLF made by hand, in any month409 recorded_by_slftax_invoice, tax_invoice_receipt
No related_invoice_id, and the total equals the gross_amount of a receipt that is not voided, for the same customer, against an invoice with VAT lines, whose payment_date is in the same month as this document's issue_date422 tax_invoice_duplicates_receipttax_invoice, tax_invoice_receipt
Same number as another receipt that is not voided409 duplicate_numberReceipts
SLF made a VAT entry by hand for the document being paid (its number and total), in a month whose VAT return SLF has not filed, and has posted that entry to your books409 recorded_by_slf_postedReceipts that book VAT
Same tax_id and branch_no409 customer_existsCustomers
Same supplier tax_id (or, without one, the same supplier name) and bill_number409 duplicate_billBills (second release)
Same file (by SHA-256) on the same record200 with the existing attachmentAttachments

A 409 for a duplicate carries existing_id when the record already there is one the API returns: fetch that record, compare it with yours, and store its id instead of creating another. existing_id is null for recorded_by_slf and recorded_by_slf_posted, whose entries SLF made by hand, and for a duplicate_number whose number belongs to a pro-forma document SLF recorded; contact SLF about those. If a request matches more than one duplicate check, duplicate_external_id is returned first. After it come, for a sales document, duplicate_number, recorded_by_slf and tax_invoice_duplicates_receipt, in that order; for a receipt, duplicate_number, then recorded_by_slf_posted; for a customer or a bill, customer_exists or duplicate_bill.

Errors and retries

Not live yet (draft specification).

Errors use standard HTTP status codes and one JSON shape. code is stable and meant for your code; message is for people and may change. The full list is under Error codes.

{
  "error": {
    "code": "validation_failed",
    "message": "2 fields are invalid.",
    "param": null,
    "details": [
      {
        "param": "/tax_id",
        "code": "invalid_tax_id",
        "message": "The check digit does not match.",
        "expected": null,
        "received": null
      },
      {
        "param": "/email",
        "code": "invalid_format",
        "message": "Not an email address.",
        "expected": null,
        "received": null
      }
    ],
    "request_id": "req_3c9e1f7a2b5d8c04",
    "existing_id": null
  }
}
FieldMeaning
codeThe error code.
messageA short explanation for people.
paramJSON Pointer to the field concerned, such as /lines/2/vat_category, or null. Here and in details, a query parameter is written as / followed by its name, such as /limit or /cursor.
detailsFor validation_failed, mismatch errors and vat_category_unexplained: one entry per problem, each with param, code and message. Mismatch entries also give expected and received. Empty for other errors.
request_idThe same value as the Request-Id header.
existing_idFor a 409 duplicate refusal, the id of the record already there, when it is one the API returns. null for a duplicate_number whose number belongs to a pro-forma document SLF recorded, for recorded_by_slf and recorded_by_slf_posted, and for every other error.

Detail codes

Detail codeMeaning
requiredA required field is missing or empty.
invalid_formatThe value has the wrong format, such as a date that is not YYYY-MM-DD or money without two decimals.
invalid_valueThe value is not allowed here, such as an unknown enum value.
out_of_rangeThe value is too small or too large, such as a quantity of zero.
too_longThe text is longer than the limit.
invalid_tax_idA Thai tax ID does not have 13 digits or its check digit is wrong.
mismatchA figure differs from the one the API computed; see expected and received.

When to retry

What happenedWhat to do
Network error or timeout (no response)Retry with the same Idempotency-Key, with exponential backoff.
429 rate_limitedWait the number of seconds in Retry-After, then retry with the same key.
500 or 503Retry with the same key, with exponential backoff (for example after 1, 2, 4 and 8 seconds, then once a minute). If it persists, contact SLF with the request_id. Before launch every /v1 request gets 503 service_unavailable, and retrying does not help.
After launch, 503 between 20:00 and 05:59 Bangkok timeSLF's nightly maintenance window: its system may restart briefly. Wait the number of seconds in Retry-After, then retry with the same key.
409 idempotency_key_in_useThe first request with this key is still running. Wait a few seconds, then retry with the same key.
401 or 403Stop and contact SLF; retrying unchanged will not help. A 403 on a write because your company profile's VAT setting and SLF's own record of your VAT registration are not both set and in agreement is temporary: keep the refused records and send them again once SLF tells you they agree. 401 and 403 responses are not stored, so the same Idempotency-Key works.
Any other 4xxDo not resend unchanged. Fix the request and send it with a new key.

A 409 duplicate_external_id on a retry usually means your first attempt succeeded. Fetch the record in existing_id and carry on.

Warnings and review

Not live yet (draft specification).

Some records are accepted but flagged for SLF to review. The response is a normal 201 and includes warnings, each with a code, a message and the param it concerns. Warnings never block a record and you do not need to resend it. If your company is VAT-registered, flagged records that book VAT are listed for SLF when it prepares that month's VAT return; SLF looks at other flagged records when it reviews your books. SLF contacts you if something needs to change.

{
  "id": "doc_6Xn1Bq5Ty3Gv",
  "number": "INV-2026-0192",
  "status": "open",
  "warnings": [
    {
      "code": "zero_rated_review",
      "message": "Line 1 uses the 0% rate. SLF will check that the conditions for it are met.",
      "param": "/lines/0/vat_category"
    },
    {
      "code": "zero_rated_review",
      "message": "Line 2 uses the 0% rate. SLF will check that the conditions for it are met.",
      "param": "/lines/1/vat_category"
    }
  ]
}

The response above is shortened to the fields that matter here. A receipt for part of what a document owes is flagged the same way (this response is shortened too; the remark is stored as you sent it):

{
  "id": "rcp_6Kq2Wd9Mz4Tb",
  "number": "RE-2026-0241",
  "document_id": "doc_9Rt3Vb6Nz1Hs",
  "payment_date": "2026-10-31",
  "amount_received": "20000.00",
  "wht_amount": "0.00",
  "remark": "First payment; balance to follow.",
  "gross_amount": "20000.00",
  "vat_amount": "0.00",
  "warnings": [
    {
      "code": "short_payment",
      "message": "Received 20000.00 against 25966.76 outstanding: short by 5966.76.",
      "param": "/amount_received"
    }
  ]
}
WarningWhen it appears
disbursement_marked_upA disbursement line's amount is above its pass_through_cost, on a document of a company that is not VAT-registered or in a currency other than THB. Otherwise such a line is refused (error disbursement_marked_up).
disbursement_billed_to_selfA disbursement line has supplier_invoice_billed_to: self.
vat_category_ignoredYour company is not VAT-registered, so a line's vat_category was recorded as none.
zero_rated_reviewA line uses vat_category: zero.
vat_arithmetic_reviewThe VAT a sales document books did not pass SLF's arithmetic check; it is held back from the month's figures until SLF resolves it.
wht_rate_reviewThe tax withheld on a receipt does not match the rates on the document's lines.
short_paymentA receipt's gross_amount is less than what the document still owed.
recorded_by_slf_filedA receipt books VAT that SLF had already entered by hand, in a month whose VAT return it has filed.
possible_double_vatA receipt books VAT for a customer who has a tax_invoice that is not voided, of the same amount, without related_invoice_id, dated in the month of the receipt's payment_date.

Warnings appear on sales documents and receipts. They are stored with the record and returned by later GET requests. The full list is under Warning codes.

Corrections

Not live yet (draft specification).

Recorded documents are not edited through the API. You correct them as you would correct paper documents: with an adjustment note, or by voiding them and recording a replacement. Customers are the only records you can update (PATCH /customers/{id}).

Which correction to use

SituationWhat to record
The value of a sale changes after a tax invoice was issued, for a reason in s.82/9 of the Revenue Code (an increase) or in s.82/10 or DG VAT Notice No. 82 (a decrease); the grounds are the debit_note_reason and credit_note_reason valuesA credit note (credit_note, ใบลดหนี้) or a debit note (debit_note, ใบเพิ่มหนี้).
A tax invoice has wrong material particulars, such as the buyer's name or address or the seller's tax ID, or its VAT amount is not the value × the rate (other than rounding)Cancel and reissue: record the replacement, then void the original (Revenue Department Order ป.86/2542, clauses 10, 24 and 25).
A tax invoice has the wrong buyer tax ID, or the wrong VAT rateAsk SLF before you record anything.
A quotation or an invoice that is not a tax invoice is wrong or cancelledVoid it, and record a new one if needed.
A receipt was recorded by mistakeVoid it with POST /receipts/{id}/void.
Your customer withheld tax the document's lines did not expect (wht_exceeds_expected)For an invoice, void its receipts and then the invoice, and record it again with the right wht_rate (and a new external_id). For a tax invoice, contact SLF.
Your system already issued a document with a disbursement line above its cost, and the API refused it (disbursement_marked_up)Contact SLF about that document. From then on, have your system show such a charge as two lines: the cost as the disbursement line, and the difference as a separate own_supply line.
A supplier bill or supplier payment was recorded by mistake (second release)Void the bill (POST /bills/{id}/void) or reverse the payment (POST /payments/{id}/reverse).
A supplier credit or debit note was recorded by mistake (second release)Contact SLF. Supplier notes cannot be voided through the API.
A file was attached to the wrong recordContact SLF. Attachments cannot be deleted through the API in v1.

A void or reversal is refused with void_not_allowed while receipts, payments or adjustment notes are recorded against the record, or once SLF has posted it to your books. Void the receipts (or reverse the payments) first; otherwise contact SLF. Each void operation lists its own conditions.

Voiding quotations and invoices

For a quotation or an invoice, voiding is a record of your cancellation. Send POST /sales-documents/{id}/void with a reason. The document keeps its number and stays visible with status: void.

Cancelling and reissuing a tax invoice

When a tax invoice's material particulars are wrong and it is cancelled on request, clause 25 of Revenue Department Order ป.86/2542 sets the procedure:

  1. The original is retrieved, stamped ยกเลิก (cancelled) or crossed out, and kept with its copy.
  2. The replacement has a new number but the same date as the original.
  3. The replacement carries the remark เป็นการยกเลิกและออกใบกำกับภาษีฉบับใหม่แทนฉบับเดิมเลขที่ … เล่มที่ … (cancelling and replacing the original, number … book …).
  4. The cancellation is noted in the output tax report of the month in which the replacement is issued.

In the API:

  1. Record the replacement with POST /sales-documents: a new number, issue_date equal to the original's date, and replaces_id set to the original's id.
  2. Void the original with POST /sales-documents/{id}/void, giving a reason and the replacement's id as replacement_id.
{
  "type": "tax_invoice",
  "external_id": "SO-2026-10-0415",
  "number": "TIV-2026-0415",
  "issue_date": "2026-10-05",
  "customer_id": "cus_7Hc4Nf2Pq9Xz",
  "bill_to_address": "88 Example Road, Pathum Wan, Bangkok 10330",
  "replaces_id": "doc_1Gh6Tc9Rw2Ne",
  "remark": "เป็นการยกเลิกและออกใบกำกับภาษีฉบับใหม่แทนฉบับเดิมเลขที่ TIV-2026-0398 เล่มที่ 1",
  "lines": [
    {
      "name": "Bookkeeping software licence, 12 months",
      "quantity": "1",
      "unit_price": "18000.00",
      "vat_category": "standard",
      "item_code": "LIC-12M"
    }
  ],
  "totals": {
    "subtotal": "18000.00",
    "vat_total": "1260.00",
    "total": "19260.00"
  }
}
{
  "reason": "Buyer's address was wrong. Replaced by TIV-2026-0415.",
  "replacement_id": "doc_8Jd2Sv6Wm4Pa"
}

Until the original is voided, both documents count. If the void is refused, contact SLF with both ids; do not void the replacement. Receipts do not move to the replacement: void them first and record them again against the replacement. A tax invoice with a credit or debit note recorded against it cannot be voided through the API; contact SLF.

Clause 26 of the same order has a different procedure for computerised systems that cannot back-date the replacement. If that applies to your system, ask SLF before recording anything. If the original's date falls in a month whose VAT return SLF has already filed, the replacement is refused with period_filed; contact SLF.

Credit and debit notes

A credit note (ใบลดหนี้) or debit note (ใบเพิ่มหนี้) records a change in value after a tax invoice was issued. Record it with POST /sales-documents and type: credit_note or type: debit_note.

  • original_document_ids: the originals, for the same customer and not voided. Each is a tax_invoice or tax_invoice_receipt, or a receipt (rcp_) recorded against an invoice whose lines carry VAT, which is the record that booked that sale's VAT. One note may cover more than one original (Order ป.80/2542, clause 4), but not tax invoices and receipts together. A note may not be dated before its originals.
  • A note on a receipt changes no document's outstanding, and a credit note on a receipt is accepted only once that invoice is paid in full. A credit note on a tax invoice reduces that tax invoice's outstanding (see credited_amount); a debit note is paid with receipts of its own.
  • reason_code: a value from credit_note_reason or debit_note_reason. Credit notes have no other reason; a debit note with other_announced needs reason_text to name the cause.
  • reason_text: the short explanation printed on the note.
  • original_value and correct_value: the value before VAT shown on the original or originals, and the correct value. The difference between them must equal the note's subtotal (note_difference_mismatch).
  • lines: the change in value, as positive amounts. A credit note may not take the originals below zero (note_exceeds_original). With more than one original, the note's value is applied to them in the order listed, each up to the value still remaining on it.

A note must show the particulars required by s.86/9 (debit notes) or s.86/10 (credit notes) of the Revenue Code and by DG VAT Notices No. 194 and 195. They include the original's number (and book number, if any), the original value, the correct value, the difference, the VAT on the difference, a short reason, the buyer's tax ID, and the head office or branch of both you and the buyer; see Tax notes for the full list. The API collects the values it needs; printing the note is your system's job.

Example: two chairs from tax invoice TIV-2026-0412 were returned as defective.

{
  "type": "credit_note",
  "external_id": "SO-CN-2026-10-0031",
  "number": "CN-2026-0031",
  "issue_date": "2026-10-22",
  "customer_id": "cus_7Hc4Nf2Pq9Xz",
  "original_document_ids": [
    "doc_9Rt3Vb6Nz1Hs"
  ],
  "reason_code": "goods_returned",
  "reason_text": "2 chairs returned: gas lift defective",
  "original_value": "29168.00",
  "correct_value": "24268.00",
  "lines": [
    {
      "name": "Office chair, model A (returned)",
      "quantity": "2",
      "unit_price": "2450.00",
      "vat_category": "standard",
      "item_code": "CH-A"
    }
  ],
  "totals": {
    "subtotal": "4900.00",
    "vat_total": "343.00",
    "total": "5243.00"
  }
}

Credit and debit notes cannot be voided through the API (note_cannot_be_voided). Contact SLF if one was recorded by mistake.

This summary is not tax advice.

Filed periods

Not live yet (draft specification).

Once SLF has filed a return for a month, the records below that would change that return are refused with 422 period_filed. Once SLF has closed an accounting period, the records below dated in it are refused with 422 period_closed. Both are SLF's controls to protect filed returns and closed books. They are not legal rules.

RecordMonth taken fromRefused with period_filedRefused with period_closed
tax_invoice, tax_invoice_receipt, credit_note, debit_noteissue_dateCreate and void, once the VAT return is filedCreate and void
Receipt against an invoice whose lines carry VAT (it books that VAT)payment_dateCreate and void, once the VAT return is filedCreate and void
Receipt against a tax_invoice or debit_note (the VAT is booked by that document)payment_dateNeverCreate and void
Supplier payment with withholding (second release)pay_dateCreate, once the withholding return of its return_form is filedCreate and reverse
Supplier payment without withholding (second release)pay_dateNeverCreate and reverse
quotation, invoice; supplier bill and supplier note (second release)—NeverNever

Second release: supplier bills are not refused by period_filed; SLF decides which VAT return claims their input VAT. Reversing a supplier payment does not take its withholding out of a withholding return SLF has already filed; SLF handles that with you.

Check a month with GET /periods/{month}: its status is open, vat_filed or closed, and filings shows each return.

A replacement tax invoice dated in a filed month (see Corrections) is refused too. Contact SLF; it handles those corrections with you.

{
  "error": {
    "code": "period_filed",
    "message": "SLF has already filed the VAT return for 2026-09. Contact SLF.",
    "param": "/issue_date",
    "details": [],
    "request_id": "req_6f3b9e2a1c7d4b58",
    "existing_id": null
  }
}

Tax notes

Not live yet (draft specification).

This section collects the Thai tax statements behind the API's fields, each with the official source it rests on. Field descriptions repeat them where they apply.

VAT categories

  • standard: the current standard rate, 7% including local tax. The rate in s.80 of the Revenue Code is 10%, reduced by royal decree. The price excludes VAT.
  • standard_incl: the same rate; the price includes VAT.
  • zero: 0%, only where s.80/1 applies, for example qualifying exports. SLF reviews every zero line.
  • exempt: exempt under s.81.
  • none: not subject to VAT on this document: disbursements recovered, or a company that is not VAT-registered.

Sources: Revenue Code s.80, s.80/1 and s.81; the Revenue Department's list of royal decrees, where the latest decree extending the reduced rate is No. 807 (B.E. 2569); and its news release ปชส. 18/2569 on the rate of 7% including local tax.

When VAT falls due (tax point)

  • Services, s.78/1(1): when payment is received, or earlier if a tax invoice is issued or the service is used before payment, for the part concerned. This is why a receipt against an invoice with VAT lines books its VAT in the month of its payment_date.
  • Goods, s.78(1): on delivery, or earlier if ownership passes, payment is received or a tax invoice is issued before delivery, for the part concerned. Record a sale of goods as a tax_invoice or tax_invoice_receipt, which books VAT by its issue_date.

Source: s.78 and s.78/1 of the Revenue Code. The other cases in those sections (s.78(2) to (5) and s.78/1(2) to (4)): ask SLF.

Disbursements (เงินทดรองจ่าย)

In ruling กค 0811/12237 (2 December 1999), the Revenue Department answered a company that had paid advertising costs on another company's behalf: recovering that amount from the other company was not a sale of goods or a service under s.77/2, so the company could not issue a tax invoice for it. A ruling answers the facts put to it. In the API, mark such lines charge_role: disbursement, supplier_invoice_billed_to: customer, vat_category: none. Whether a charge is a disbursement, and how it is shown on your documents: confirm with SLF.

Credit note reasons

credit_note_reason follows s.82/10 of the Revenue Code, DG VAT Notice No. 82 and Order ป.80/2542, clause 2(3). There is no other value.

ValueMeaning
goods_nonconformingPrice reduced because goods did not meet the agreed specification, were damaged or short.
goods_returnedGoods returned because defective, not matching sample or description.
goods_return_exchangeGoods returned or exchanged under a trade agreement: between VAT-registered businesses, or between you and a buyer if done within a reasonable time.
service_deficientService price reduced because the service was deficient, short or not as agreed.
price_overchargedPrice calculated higher than it should have been.
service_contract_cancelled_deficientService contract cancelled because the service was deficient or not as agreed.
service_contract_cancelled_not_providedService contract cancelled because the service was not provided.
deposit_refundRefund of an advance payment, deposit, guarantee or booking money under a trade agreement.
compensationCompensation or similar the seller must pay the buyer by law.

Debit note reasons

debit_note_reason follows s.82/9 of the Revenue Code.

ValueMeaning
goods_quantity_exceededMore goods delivered than agreed.
goods_underchargedGoods price calculated lower than it should have been.
service_exceededService provided beyond what was agreed.
service_underchargedService price calculated lower than it should have been.
other_announcedAnother cause set by the Director-General. reason_text must name it; SLF reviews.

One credit or debit note may refer to more than one original tax invoice (Order ป.80/2542, clause 4). Sources: s.82/9 and s.82/10; DG VAT Notice No. 82; Order ป.80/2542.

What a credit or debit note must show

Credit and debit notes must show the particulars required by s.86/10 and s.86/9 and by DG VAT Notices No. 195 and 194. They include: the words ใบลดหนี้ or ใบเพิ่มหนี้; your name, address and tax ID; the buyer's name and address; the date of the note; the original's number (and book number, if any); the value shown on the original, the correct value, the difference and the VAT on the difference; a short reason; the buyer's tax ID; and the head office or branch of both you and the buyer. Sources: s.86/9 and s.86/10; DG VAT Notice No. 194; DG VAT Notice No. 195.

Correcting a tax invoice

A change in value for a reason in s.82/9 (an increase), or in s.82/10 or DG VAT Notice No. 82 (a decrease), is recorded with a debit or credit note. Wrong material particulars (the buyer's name or address, the seller's tax ID), or a VAT amount that is not value × rate other than rounding, are corrected by cancelling and reissuing under Order ป.86/2542, clauses 10, 24 and 25; clause 26 covers computerised systems that cannot back-date. For a wrong buyer tax ID or a wrong VAT rate, ask SLF. Source: Order ป.86/2542.

Foreign-currency tax invoices

A tax invoice must be in Thai currency unless the Director-General of the Revenue Department has approved a foreign currency (s.86/4 of the Revenue Code; Order ป.86/2542, clause 8). Sections 86/9 and 86/10 apply the same rule to debit and credit notes. The API therefore accepts a tax_invoice, tax_invoice_receipt, credit_note or debit_note in a currency other than THB only if SLF holds that approval for your company (foreign_currency_tax_invoice_not_allowed). Sources: s.86/4, s.86/9 and s.86/10; Order ป.86/2542.

Withholding tax

  • return_form is set from payee_type: pnd3 (ภ.ง.ด.3) for an individual, pnd53 (ภ.ง.ด.53) for a company. It is SLF's working label, and SLF confirms the return when it reviews the entry. The return depends on the income and the payee, and some withholding belongs on another return: tax withheld under s.50(1) on income under s.40(1) and (2) of the Revenue Code (s.40(2) lists commission, ค่านายหน้า) goes on ภ.ง.ด.1; tax withheld under s.50(2) on income under s.40(3) and (4) (s.40(4)(ก) lists interest, ดอกเบี้ย) goes on ภ.ง.ด.2; and tax withheld under s.70 from a company organised under foreign law that does not carry on business in Thailand is reported on ภ.ง.ด.54. Ask SLF before you record withholding on commission or interest paid to an individual, or on a payment to a company abroad. Sources: withholding tax returns; s.40 and s.50; s.70; ภ.ง.ด.54.
  • 50 ทวิ is the withholding tax certificate under s.50 bis of the Revenue Code (s.50 bis). If you issued it yourself, send its number as certificate_number.
  • The API does not say which rate applies to which income. Send the rate you actually withheld; SLF reviews it.

This reference describes data, not tax treatment. Nothing here is tax or legal advice; SLF Accounting reviews what you send.

Limits and versioning

Not live yet (draft specification).

LimitValue
Requests60 per minute per key (draft value). Above it: 429 rate_limited with Retry-After.
Short burstsWhen many requests reach SLF in the same second, from any company, some may get 503 service_unavailable with Retry-After, even within your per-minute limit. Retry with the same Idempotency-Key.
JSON request body1 MB
File upload10 MB; PDF, JPEG, PNG or WebP, recognised from the file's content. HEIC is not accepted; convert such photos to JPEG.
Uploads per company1 GB or 1,000 files per day (Bangkok time), whichever comes first. Above it: 429 rate_limited with Retry-After until midnight.
Maintenance windowOnce the API is live, every night from 20:00 to 05:59 Bangkok time. SLF's system may restart briefly; requests then get 503 service_unavailable with Retry-After.
Lines per sales document1 to 500
Lines per supplier bill (second release)0 to 500
Allocations per supplier payment (second release)1 to 100
Withholding entries per supplier payment (second release)Up to 50
Page sizelimit 1 to 100, default 25
Idempotency window24 hours
Records per requestOne. There is no batch endpoint in v1.

Versioning

  • The version is part of the path: /v1.
  • Adding fields, enum values or endpoints is not a breaking change. Your code must ignore fields it does not know and cope with enum values it has not seen.
  • Removing or renaming anything goes into a new version, /v2.
  • Until launch, the draft itself can change, including in ways that would break code written against it. Every change is listed in the Changelog.

Not in v1

  • Webhooks.
  • Batch requests.
  • Editing recorded documents. Only customers can be updated.
  • Discounts. There are no discount fields. Send each line at its net unit price after any line discount, provided the printed line amount equals quantity × unit_price. A document with a discount printed as a negative line or at document level cannot be recorded in v1; contact SLF.
  • One receipt for several documents. A receipt covers one document. If your printed receipts can cover several documents, contact SLF before you connect; sending one as several receipts with the same number is refused with duplicate_number.
  • Refunds and overpayments. A receipt cannot be more than the document's outstanding. Contact SLF about money to be paid back to a customer, including after a credit note.
  • Bank charges and short payments. Record the amount that arrived as amount_received; the receipt carries the warning short_payment, and the document stays partially_paid until SLF settles the difference with you.
  • Tax invoices issued for each part payment, in the first release. A tax_invoice or tax_invoice_receipt that repeats the VAT of a receipt already recorded is refused with tax_invoice_duplicates_receipt (see How the pieces fit); contact SLF if your system issues them.
  • Exchange differences. A receipt uses its document's fx_rate; SLF books any exchange difference.
  • Loading past documents. See Going live in Environments and keys.

Endpoints

Company

Not live yet (draft specification). The company your key belongs to.

GET /v1/company

Get your company

Returns the company the key belongs to. Use it to check that your key works and belongs to the company you expect. vat_registered is the VAT setting of your company's profile at SLF, which someone at your company can also change in the client portal. If it is false, every line's vat_category is recorded as none. Every write is refused with 403 forbidden unless this setting and SLF's own record of your company's VAT registration are both set and agree; they can disagree, for example, after the setting is changed in the client portal. Reads, including this one, keep working. Keep the refused records, contact SLF, and send them again once SLF tells you the two agree.

Responses

200 Your company. · Company

{
  "name": "Example Trading Co., Ltd.",
  "name_local": "บริษัท ตัวอย่าง จำกัด",
  "tax_id": "0999000000019",
  "branch_no": "00000",
  "vat_registered": true
}

Objects

Company Company

The company your key belongs to.

FieldTypeRequiredDescription
namestringread-onlyThe name SLF records for the company. It is not necessarily the registered name; SLF records the registered name in Thai as name_local. Example: Example Trading Co., Ltd..
name_localstringread-onlyThe company's registered name in Thai, as SLF has it on record. Example: บริษัท ตัวอย่าง จำกัด.
tax_idstring
13 digits
read-onlyThe company's tax ID. Example: 0999000000019.
branch_nostring
5 digits
read-onlyThe branch number SLF has on record for the company the key belongs to. 00000 is the head office. Example: 00000.
vat_registeredbooleanread-onlyThe VAT setting of your company's profile at SLF: whether the company is VAT-registered, or null when the profile has no VAT setting. Someone at your company can also change it in the client portal. If false, every sales line's vat_category is recorded as none. Writes are refused with 403 forbidden unless this setting and SLF's own record of your VAT registration are both set and agree; send the refused records again once SLF tells you they agree. Example: true.

Bank accounts

Not live yet (draft specification). Your company's bank accounts as SLF has recorded them. The API only reads them: accounts are added, edited and deleted by SLF or in your company's client portal.

GET /v1/bank-accounts

List bank accounts

Returns the accounts SLF has on record for your company. Use an account's id as bank_account_id on sales documents, receipts and payments. Only the last four digits of each account number are returned. The list can change between two reads: an account deleted by SLF or in your client portal is no longer listed and its id is gone, and an account edited there keeps its id with the new details. Read the list again before you use an id, and check that bank_name and account_number_last4 still show the account you mean. An id that is no longer listed is refused on writes with 422 reference_not_found.

Parameters
NameInTypeRequiredDescription
limitqueryintegeroptionalNumber of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25.
cursorquerystring
≤ 512 characters
optionalThe next_cursor value from the previous page, unchanged. Omit it for the first page; an empty value is refused. For the refusals, see Pagination in Conventions. Example: cur_8Tn3XqL5Vd.
Responses

200 A page of bank accounts. · list of BankAccount

{
  "data": [
    {
      "id": "bnk_5Qm2Rk8Lt3Vw",
      "bank_name": "Example Bank Public Company Limited",
      "account_name": "Example Trading Co., Ltd.",
      "account_number_last4": "4821",
      "currency": "THB",
      "type": null
    },
    {
      "id": "bnk_9Ze3Lp6Qr2Dn",
      "bank_name": "Example Bank Public Company Limited",
      "account_name": "Example Trading Co., Ltd.",
      "account_number_last4": "7730",
      "currency": "USD",
      "type": null
    }
  ],
  "next_cursor": null
}

Objects

BankAccount Bank account

A bank account of your company as SLF has recorded it.

FieldTypeRequiredDescription
idstring
bnk_ id
read-onlyUse it as bank_account_id. Example: bnk_5Qm2Rk8Lt3Vw.
bank_namestringread-onlyThe bank, as SLF has recorded it for this account. It may also name the branch, such as Example Bank, Main branch. Example: Example Bank Public Company Limited.
account_namestringread-onlyThe name the account is held in. Example: Example Trading Co., Ltd..
account_number_last4string
4 digits
read-onlyThe last four digits of the account number. The full number is never returned. Example: 4821.
currencystring
ISO 4217
read-onlyThe account's currency. Example: THB.
typestring · enum bank_account_typeread-onlypersonal when SLF has recorded the account as held in a person's name; otherwise null, which does not say whose name the account is in (see account_name). Example: personal.

Customers

Not live yet (draft specification). The customers your sales documents are issued to. Create each customer once and refer to it by id (or your external_id) on sales documents.

POST /v1/customers

Create a customer

Creates a customer. Only external_id and name are required. For a customer in Thailand (country TH) with a tax_id, the tax ID's check digit is validated and branch_no defaults to 00000. A customer with the same tax_id and branch_no as an existing one is refused with 409 customer_exists, and existing_id points to the customer already recorded. For a customer outside Thailand, tax_id is optional and its format is not checked.

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
Request body

application/json · Customer

{
  "external_id": "CUST-00412",
  "name": "Sample Retail Co., Ltd.",
  "name_local": "บริษัท ตัวอย่างค้าปลีก จำกัด",
  "country": "TH",
  "tax_id": "0999000000027",
  "branch_no": "00000",
  "address": "88 Example Road, Pathum Wan, Bangkok 10330",
  "email": "accounts@sample-retail.example",
  "phone": "+66 2 000 0000",
  "notes": "Send statements to the accounts team."
}
Responses

201 The customer was created. · Customer

{
  "id": "cus_7Hc4Nf2Pq9Xz",
  "external_id": "CUST-00412",
  "name": "Sample Retail Co., Ltd.",
  "name_local": "บริษัท ตัวอย่างค้าปลีก จำกัด",
  "country": "TH",
  "tax_id": "0999000000027",
  "branch_no": "00000",
  "address": "88 Example Road, Pathum Wan, Bangkok 10330",
  "email": "accounts@sample-retail.example",
  "phone": "+66 2 000 0000",
  "notes": "Send statements to the accounts team.",
  "created_at": "2026-10-15T09:12:44+07:00"
}

409 A customer with this tax_id and branch_no, or this external_id, already exists. · Error

{
  "error": {
    "code": "customer_exists",
    "message": "A customer with tax ID 0999000000027, branch 00000 already exists.",
    "param": "/tax_id",
    "details": [],
    "request_id": "req_7d1a4c8e2f6b9a35",
    "existing_id": "cus_7Hc4Nf2Pq9Xz"
  }
}

422 One or more fields are invalid. · Error

{
  "error": {
    "code": "validation_failed",
    "message": "2 fields are invalid.",
    "param": null,
    "details": [
      {
        "param": "/tax_id",
        "code": "invalid_tax_id",
        "message": "The check digit does not match.",
        "expected": null,
        "received": null
      },
      {
        "param": "/email",
        "code": "invalid_format",
        "message": "Not an email address.",
        "expected": null,
        "received": null
      }
    ],
    "request_id": "req_3c9e1f7a2b5d8c04",
    "existing_id": null
  }
}
Errors specific to this endpoint

duplicate_external_id, customer_exists. Common errors: see Error codes.

GET /v1/customers

List customers

Returns your customers, newest first. Filter by tax ID and branch, or by your own external_id, to find a customer you may have created before.

Parameters
NameInTypeRequiredDescription
tax_idquerystringoptionalOnly customers with this tax ID. Example: 0999000000027.
branch_noquerystringoptionalOnly customers with this branch number. Use together with tax_id. Example: 00000.
external_idquerystringoptionalOnly the customer with this external_id. Example: CUST-00412.
limitqueryintegeroptionalNumber of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25.
cursorquerystring
≤ 512 characters
optionalThe next_cursor value from the previous page, unchanged. Omit it for the first page; an empty value is refused. For the refusals, see Pagination in Conventions. Example: cur_8Tn3XqL5Vd.
Responses

200 A page of customers. · list of Customer

{
  "data": [
    {
      "id": "cus_3Wd8Lm5Tb2Kr",
      "external_id": "CUST-00977",
      "name": "Example Imports Pte. Ltd.",
      "name_local": null,
      "country": "SG",
      "tax_id": null,
      "branch_no": null,
      "address": "10 Example Street, Singapore 000000",
      "email": "finance@example-imports.example",
      "phone": null,
      "notes": null,
      "created_at": "2026-10-26T16:40:03+07:00"
    },
    {
      "id": "cus_7Hc4Nf2Pq9Xz",
      "external_id": "CUST-00412",
      "name": "Sample Retail Co., Ltd.",
      "name_local": "บริษัท ตัวอย่างค้าปลีก จำกัด",
      "country": "TH",
      "tax_id": "0999000000027",
      "branch_no": "00000",
      "address": "88 Example Road, Pathum Wan, Bangkok 10330",
      "email": "accounts@sample-retail.example",
      "phone": "+66 2 000 0000",
      "notes": "Send statements to the accounts team.",
      "created_at": "2026-10-15T09:12:44+07:00"
    }
  ],
  "next_cursor": null
}

GET /v1/customers/{id}

Get a customer

Returns one customer.

Parameters
NameInTypeRequiredDescription
idpathstringrequiredThe customer's id (cus_). Example: cus_….
Responses

200 The customer. · Customer

{
  "id": "cus_7Hc4Nf2Pq9Xz",
  "external_id": "CUST-00412",
  "name": "Sample Retail Co., Ltd.",
  "name_local": "บริษัท ตัวอย่างค้าปลีก จำกัด",
  "country": "TH",
  "tax_id": "0999000000027",
  "branch_no": "00000",
  "address": "88 Example Road, Pathum Wan, Bangkok 10330",
  "email": "accounts@sample-retail.example",
  "phone": "+66 2 000 0000",
  "notes": "Send statements to the accounts team.",
  "created_at": "2026-10-15T09:12:44+07:00"
}

PATCH /v1/customers/{id}

Update a customer

Changes a customer's name, name_local, address, email, phone or notes, and adds a tax_id and branch_no to a customer that has none. Send only the fields you change; null clears an optional field. tax_id and branch_no cannot change once set: sending a different value is refused with immutable_field (contact SLF if a tax ID was recorded wrongly). A tax_id and branch_no already used by another customer are refused with customer_exists. country and external_id cannot be changed, and other fields in the body are ignored. Documents already recorded keep the details printed on them.

Parameters
NameInTypeRequiredDescription
idpathstringrequiredThe customer's id (cus_). Example: cus_….
Request body

application/json · CustomerUpdate

{
  "email": "ap@sample-retail.example",
  "phone": "+66 2 000 0001"
}
Responses

200 The updated customer. · Customer

{
  "id": "cus_7Hc4Nf2Pq9Xz",
  "external_id": "CUST-00412",
  "name": "Sample Retail Co., Ltd.",
  "name_local": "บริษัท ตัวอย่างค้าปลีก จำกัด",
  "country": "TH",
  "tax_id": "0999000000027",
  "branch_no": "00000",
  "address": "88 Example Road, Pathum Wan, Bangkok 10330",
  "email": "ap@sample-retail.example",
  "phone": "+66 2 000 0001",
  "notes": "Send statements to the accounts team.",
  "created_at": "2026-10-15T09:12:44+07:00"
}

409 Another customer already has this tax_id and branch_no. · Error

{
  "error": {
    "code": "customer_exists",
    "message": "A customer with tax ID 0999000000027, branch 00000 already exists.",
    "param": "/tax_id",
    "details": [],
    "request_id": "req_7d1a4c8e2f6b9a35",
    "existing_id": "cus_7Hc4Nf2Pq9Xz"
  }
}

422 A field is invalid or cannot be changed. · Error

{
  "error": {
    "code": "immutable_field",
    "message": "tax_id cannot be changed once set.",
    "param": "/tax_id",
    "details": [],
    "request_id": "req_0e5a2c8d4f7b1e93",
    "existing_id": null
  }
}
Errors specific to this endpoint

immutable_field, customer_exists. Common errors: see Error codes.

Objects

Customer Customer

A customer your sales documents are issued to.

FieldTypeRequiredDescription
idstring
cus_ id
read-onlySLF's id for the customer. Example: cus_7Hc4Nf2Pq9Xz.
external_idstring
≤ 100 characters
requiredYour system's id for the customer. Unique among your customers (duplicate_external_id). Required when you create the customer; null on customers SLF entered itself. Example: CUST-00412.
namestring
≤ 300 characters
requiredThe customer's name as printed on your documents. Example: Sample Retail Co., Ltd..
name_localstring
≤ 300 characters
optionalThe name in Thai, if different. Example: บริษัท ตัวอย่างค้าปลีก จำกัด.
countrystring
ISO 3166-1 alpha-2
optionalThe customer's country. Thai tax-ID validation applies only when it is TH. Default: TH. Example: TH.
tax_idstring
13 digits when country is TH
optionalThe customer's tax ID. For TH, 13 digits with a valid check digit (invalid_tax_id); for other countries, optional and not checked. Cannot change once set. Example: 0999000000027.
branch_nostring
5 digits
optionalThe customer's branch. 00000 is the head office. Defaults to 00000 when country is TH and tax_id is given; otherwise null unless you send it. Cannot change once set. Example: 00000.
addressstring
≤ 1000 characters
optionalThe address printed on your documents. Example: 88 Example Road, Pathum Wan, Bangkok 10330.
emailstring
≤ 254 characters
optionalBilling email address. Example: accounts@sample-retail.example.
phonestring
≤ 50 characters
optionalPhone number. Example: +66 2 000 0000.
notesstring
≤ 2000 characters
optionalYour notes about the customer.
created_atdate-timeread-onlyWhen the customer was created. Example: 2026-10-15T09:12:44+07:00.

CustomerUpdate Customer update

The fields PATCH /customers/{id} can change. Send only the fields you change.

FieldTypeRequiredDescription
namestring
≤ 300 characters
optionalNew name. Cannot be cleared.
name_localstring
≤ 300 characters
optionalNew name in Thai.
addressstring
≤ 1000 characters
optionalNew address.
emailstring
≤ 254 characters
optionalNew billing email address. Example: ap@sample-retail.example.
phonestring
≤ 50 characters
optionalNew phone number. Example: +66 2 000 0001.
notesstring
≤ 2000 characters
optionalNew notes.
tax_idstring
13 digits when country is TH
optionalOnly while the customer has no tax ID yet. A different value once one is set is refused with immutable_field; a tax ID and branch already used by another customer with customer_exists.
branch_nostring
5 digits
optionalOnly while the customer has no branch yet. A different value once one is set is refused with immutable_field. Defaults to 00000 when you add a Thai tax_id without it.

Sales documents

Not live yet (draft specification). Documents you issued to customers: quotations, invoices, tax invoices, cash-sale tax invoice/receipts, credit notes and debit notes. You record them as printed; SLF does not number, print or send them.

POST /v1/sales-documents

Record a sales document

Records a sales document your system has already issued. Send it exactly as printed: your number, the issue_date, the lines and the totals. The API computes the line amounts and totals and refuses a difference beyond its tolerance (totals_mismatch); within it, the vat_total you printed is recorded (see How the API checks totals). A document whose VAT does not pass SLF's arithmetic check is recorded with the warning vat_arithmetic_review. Disbursement lines (charge_role: disbursement) are recorded as printed and never split; a line priced above its pass_through_cost is refused with disbursement_marked_up, unless your company is not VAT-registered or the document is not in THB. If your company is VAT-registered, a document in which no line carries VAT or says why it does not is refused with vat_category_unexplained. Credit and debit notes also need original_document_ids, reason_code, reason_text, original_value and correct_value (see Corrections); an original that is voided or belongs to another customer is refused with validation_failed. A tax_invoice_receipt is its own receipt and is recorded as paid. A document dated before your start date is refused with validation_failed (out_of_range), and so is a tax_invoice or tax_invoice_receipt dated after today or before the VAT registration date SLF has on record for your company. A tax invoice or cash sale that SLF already entered by hand is refused with recorded_by_slf, and one that repeats the VAT of a receipt with tax_invoice_duplicates_receipt (see Idempotency and duplicates); a foreign-currency one may be refused with fx_basis_unavailable (see Conventions). Tax invoices, cash sales and adjustment notes dated in a month whose VAT return SLF has filed are refused with period_filed (see Filed periods). The example records an invoice with a disbursement line. Its withholding rates (3% on the service lines, none on the disbursement) are only example values, not rules; SLF reviews the rates.

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
Request body

application/json · SalesDocument

{
  "type": "invoice",
  "external_id": "SO-2026-10-0187",
  "number": "INV-2026-0187",
  "issue_date": "2026-10-15",
  "due_date": "2026-11-14",
  "customer_id": "cus_7Hc4Nf2Pq9Xz",
  "currency": "THB",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Please pay by bank transfer within 30 days.",
  "reference": "PRJ-2026-031",
  "lines": [
    {
      "name": "Accounting system setup",
      "description": "Chart of accounts, opening balances and user training",
      "quantity": "1",
      "unit_price": "12500.00",
      "vat_category": "standard",
      "wht_rate": "3",
      "item_code": "SVC-SETUP",
      "charge_role": "own_supply"
    },
    {
      "name": "Monthly support",
      "description": "October to December 2026",
      "quantity": "3",
      "unit_price": "2150.75",
      "vat_category": "standard",
      "wht_rate": "3",
      "item_code": "SVC-SUPPORT",
      "charge_role": "own_supply"
    },
    {
      "name": "Government registration fee paid on your behalf",
      "quantity": "1",
      "unit_price": "1500.00",
      "vat_category": "none",
      "wht_rate": "0",
      "charge_role": "disbursement",
      "supplier_invoice_billed_to": "customer",
      "pass_through_cost": "1500.00"
    }
  ],
  "totals": {
    "subtotal": "20452.25",
    "vat_total": "1326.66",
    "total": "21778.91",
    "wht_total": "568.57",
    "net_to_pay": "21210.34"
  }
}
Responses

201 The document was recorded. Check warnings. · SalesDocument

{
  "id": "doc_4Fp9Km2Qx7Ld",
  "type": "invoice",
  "external_id": "SO-2026-10-0187",
  "number": "INV-2026-0187",
  "issue_date": "2026-10-15",
  "due_date": "2026-11-14",
  "valid_until": null,
  "customer_id": "cus_7Hc4Nf2Pq9Xz",
  "bill_to_address": null,
  "currency": "THB",
  "fx_rate": null,
  "fx_date": null,
  "payment_terms": null,
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Please pay by bank transfer within 30 days.",
  "reference": "PRJ-2026-031",
  "related_invoice_id": null,
  "replaces_id": null,
  "original_document_ids": null,
  "reason_code": null,
  "reason_text": null,
  "original_value": null,
  "correct_value": null,
  "lines": [
    {
      "name": "Accounting system setup",
      "description": "Chart of accounts, opening balances and user training",
      "quantity": "1",
      "unit_price": "12500.00",
      "vat_category": "standard",
      "wht_rate": "3",
      "item_code": "SVC-SETUP",
      "account_code": null,
      "charge_role": "own_supply",
      "supplier_invoice_billed_to": null,
      "pass_through_cost": null,
      "pre_vat_amount": "12500.00",
      "vat_rate": "7",
      "vat_amount": "875.00",
      "wht_amount": "375.00"
    },
    {
      "name": "Monthly support",
      "description": "October to December 2026",
      "quantity": "3",
      "unit_price": "2150.75",
      "vat_category": "standard",
      "wht_rate": "3",
      "item_code": "SVC-SUPPORT",
      "account_code": null,
      "charge_role": "own_supply",
      "supplier_invoice_billed_to": null,
      "pass_through_cost": null,
      "pre_vat_amount": "6452.25",
      "vat_rate": "7",
      "vat_amount": "451.66",
      "wht_amount": "193.57"
    },
    {
      "name": "Government registration fee paid on your behalf",
      "description": null,
      "quantity": "1",
      "unit_price": "1500.00",
      "vat_category": "none",
      "wht_rate": "0",
      "item_code": null,
      "account_code": null,
      "charge_role": "disbursement",
      "supplier_invoice_billed_to": "customer",
      "pass_through_cost": "1500.00",
      "pre_vat_amount": "1500.00",
      "vat_rate": "0",
      "vat_amount": "0.00",
      "wht_amount": "0.00"
    }
  ],
  "totals": {
    "subtotal": "20452.25",
    "vat_total": "1326.66",
    "total": "21778.91",
    "wht_total": "568.57",
    "net_to_pay": "21210.34"
  },
  "attachments": [],
  "status": "open",
  "paid_amount": "0.00",
  "credited_amount": "0.00",
  "outstanding": "21778.91",
  "voided_at": null,
  "void_reason": null,
  "replaced_by_id": null,
  "warnings": [],
  "created_at": "2026-10-15T14:02:11+07:00"
}

409 A document with this external_id or this number already exists, or SLF already entered this tax invoice or cash sale by hand (recorded_by_slf). · Error

{
  "error": {
    "code": "duplicate_number",
    "message": "An invoice numbered INV-2026-0187 already exists.",
    "param": "/number",
    "details": [],
    "request_id": "req_1e6c3a9d7b2f5c81",
    "existing_id": "doc_4Fp9Km2Qx7Ld"
  }
}

422 The document is invalid, its totals do not match its lines, one of the rules above refuses it, or its month is filed or closed. · Error

{
  "error": {
    "code": "totals_mismatch",
    "message": "Totals do not match the lines.",
    "param": "/totals",
    "details": [
      {
        "param": "/totals/vat_total",
        "code": "mismatch",
        "message": "VAT total differs from the sum of line VAT.",
        "expected": "1326.66",
        "received": "1431.66"
      },
      {
        "param": "/totals/total",
        "code": "mismatch",
        "message": "Total differs from subtotal plus VAT total.",
        "expected": "21778.91",
        "received": "21883.91"
      }
    ],
    "request_id": "req_9a4f7c2e5d8b1a36",
    "existing_id": null
  }
}
Errors specific to this endpoint

duplicate_external_id, duplicate_number, recorded_by_slf, reference_not_found, totals_mismatch, note_difference_mismatch, note_original_not_tax_invoice, note_exceeds_original, disbursement_vat_category, disbursement_marked_up, vat_category_unexplained, tax_invoice_duplicates_receipt, period_filed, period_closed, foreign_currency_tax_invoice_not_allowed, fx_basis_unavailable, fx_rate_required, account_code_unknown. Common errors: see Error codes.

GET /v1/sales-documents

List sales documents

Returns your sales documents, newest first, with optional filters. Documents SLF entered itself are included, such as credit and debit notes; pro-forma documents SLF recorded are not.

Parameters
NameInTypeRequiredDescription
typequerystring · enum sales_document_typeoptionalOnly documents of this type. Example: tax_invoice.
customer_idquerystringoptionalOnly documents for this customer. Example: cus_7Hc4Nf2Pq9Xz.
external_idquerystringoptionalOnly the document with this external_id. Example: SO-2026-10-0412.
numberquerystringoptionalOnly documents with this printed number. Example: TIV-2026-0412.
referencequerystringoptionalOnly documents with this reference. Example: PRJ-2026-031.
statusquerystring · enum sales_document_statusoptionalOnly documents with this status, such as open to find what is still owed. Example: open.
issue_date_fromquerydateoptionalOnly documents issued on or after this date. Example: 2026-10-01.
issue_date_toquerydateoptionalOnly documents issued on or before this date. Example: 2026-10-31.
limitqueryintegeroptionalNumber of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25.
cursorquerystring
≤ 512 characters
optionalThe next_cursor value from the previous page, unchanged. Omit it for the first page; an empty value is refused. For the refusals, see Pagination in Conventions. Example: cur_8Tn3XqL5Vd.
Responses

200 A page of sales documents. The example holds a foreign-currency invoice, a credit note and the tax invoice it adjusts, whose outstanding the credit note reduced. · list of SalesDocument

{
  "data": [
    {
      "id": "doc_6Xn1Bq5Ty3Gv",
      "type": "invoice",
      "external_id": "SO-2026-10-0192",
      "number": "INV-2026-0192",
      "issue_date": "2026-10-27",
      "due_date": "2026-11-26",
      "valid_until": null,
      "customer_id": "cus_3Wd8Lm5Tb2Kr",
      "bill_to_address": null,
      "currency": "USD",
      "fx_rate": "32.4512",
      "fx_date": "2026-10-27",
      "payment_terms": "tt",
      "bank_account_id": "bnk_9Ze3Lp6Qr2Dn",
      "remark": null,
      "reference": null,
      "related_invoice_id": null,
      "replaces_id": null,
      "original_document_ids": null,
      "reason_code": null,
      "reason_text": null,
      "original_value": null,
      "correct_value": null,
      "lines": [
        {
          "name": "Teak serving tray, large",
          "description": null,
          "quantity": "120",
          "unit_price": "18.75",
          "vat_category": "zero",
          "wht_rate": "0",
          "item_code": "TRAY-L",
          "account_code": null,
          "charge_role": null,
          "supplier_invoice_billed_to": null,
          "pass_through_cost": null,
          "pre_vat_amount": "2250.00",
          "vat_rate": "0",
          "vat_amount": "0.00",
          "wht_amount": "0.00"
        },
        {
          "name": "Teak serving tray, small",
          "description": null,
          "quantity": "200",
          "unit_price": "9.40",
          "vat_category": "zero",
          "wht_rate": "0",
          "item_code": "TRAY-S",
          "account_code": null,
          "charge_role": null,
          "supplier_invoice_billed_to": null,
          "pass_through_cost": null,
          "pre_vat_amount": "1880.00",
          "vat_rate": "0",
          "vat_amount": "0.00",
          "wht_amount": "0.00"
        }
      ],
      "totals": {
        "subtotal": "4130.00",
        "vat_total": "0.00",
        "total": "4130.00",
        "wht_total": null,
        "net_to_pay": null
      },
      "attachments": [],
      "status": "open",
      "paid_amount": "0.00",
      "credited_amount": "0.00",
      "outstanding": "4130.00",
      "voided_at": null,
      "void_reason": null,
      "replaced_by_id": null,
      "warnings": [
        {
          "code": "zero_rated_review",
          "message": "Line 1 uses the 0% rate. SLF will check that the conditions for it are met.",
          "param": "/lines/0/vat_category"
        },
        {
          "code": "zero_rated_review",
          "message": "Line 2 uses the 0% rate. SLF will check that the conditions for it are met.",
          "param": "/lines/1/vat_category"
        }
      ],
      "created_at": "2026-10-27T15:20:48+07:00"
    },
    {
      "id": "doc_2Lk7Pw4Mc8Jq",
      "type": "credit_note",
      "external_id": "SO-CN-2026-10-0031",
      "number": "CN-2026-0031",
      "issue_date": "2026-10-22",
      "due_date": null,
      "valid_until": null,
      "customer_id": "cus_7Hc4Nf2Pq9Xz",
      "bill_to_address": null,
      "currency": "THB",
      "fx_rate": null,
      "fx_date": null,
      "payment_terms": null,
      "bank_account_id": null,
      "remark": null,
      "reference": null,
      "related_invoice_id": null,
      "replaces_id": null,
      "original_document_ids": [
        "doc_9Rt3Vb6Nz1Hs"
      ],
      "reason_code": "goods_returned",
      "reason_text": "2 chairs returned: gas lift defective",
      "original_value": "29168.00",
      "correct_value": "24268.00",
      "lines": [
        {
          "name": "Office chair, model A (returned)",
          "description": null,
          "quantity": "2",
          "unit_price": "2450.00",
          "vat_category": "standard",
          "wht_rate": "0",
          "item_code": "CH-A",
          "account_code": null,
          "charge_role": null,
          "supplier_invoice_billed_to": null,
          "pass_through_cost": null,
          "pre_vat_amount": "4900.00",
          "vat_rate": "7",
          "vat_amount": "343.00",
          "wht_amount": "0.00"
        }
      ],
      "totals": {
        "subtotal": "4900.00",
        "vat_total": "343.00",
        "total": "5243.00",
        "wht_total": null,
        "net_to_pay": null
      },
      "attachments": [],
      "status": "open",
      "paid_amount": "0.00",
      "credited_amount": "0.00",
      "outstanding": "0.00",
      "voided_at": null,
      "void_reason": null,
      "replaced_by_id": null,
      "warnings": [],
      "created_at": "2026-10-22T10:31:09+07:00"
    },
    {
      "id": "doc_9Rt3Vb6Nz1Hs",
      "type": "tax_invoice",
      "external_id": "SO-2026-10-0412",
      "number": "TIV-2026-0412",
      "issue_date": "2026-10-08",
      "due_date": "2026-11-07",
      "valid_until": null,
      "customer_id": "cus_7Hc4Nf2Pq9Xz",
      "bill_to_address": null,
      "currency": "THB",
      "fx_rate": null,
      "fx_date": null,
      "payment_terms": null,
      "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
      "remark": null,
      "reference": null,
      "related_invoice_id": null,
      "replaces_id": null,
      "original_document_ids": null,
      "reason_code": null,
      "reason_text": null,
      "original_value": null,
      "correct_value": null,
      "lines": [
        {
          "name": "Office chair, model A",
          "description": null,
          "quantity": "10",
          "unit_price": "2450.00",
          "vat_category": "standard",
          "wht_rate": "0",
          "item_code": "CH-A",
          "account_code": null,
          "charge_role": null,
          "supplier_invoice_billed_to": null,
          "pass_through_cost": null,
          "pre_vat_amount": "24500.00",
          "vat_rate": "7",
          "vat_amount": "1715.00",
          "wht_amount": "0.00"
        },
        {
          "name": "Desk lamp, LED",
          "description": null,
          "quantity": "12",
          "unit_price": "389.00",
          "vat_category": "standard",
          "wht_rate": "0",
          "item_code": "LAMP-LED",
          "account_code": null,
          "charge_role": null,
          "supplier_invoice_billed_to": null,
          "pass_through_cost": null,
          "pre_vat_amount": "4668.00",
          "vat_rate": "7",
          "vat_amount": "326.76",
          "wht_amount": "0.00"
        }
      ],
      "totals": {
        "subtotal": "29168.00",
        "vat_total": "2041.76",
        "total": "31209.76",
        "wht_total": null,
        "net_to_pay": null
      },
      "attachments": [
        {
          "id": "att_2Bh5Rx8Fk3Wq",
          "object_type": "sales_document",
          "object_id": "doc_9Rt3Vb6Nz1Hs",
          "kind": "document_copy",
          "file_name": "TIV-2026-0412.pdf",
          "content_type": "application/pdf",
          "size": 184233,
          "sha256": "9f2b5c1e7a4d8f0c3b6e9a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3a6d9c2f",
          "created_at": "2026-10-08T11:05:37+07:00"
        }
      ],
      "status": "open",
      "paid_amount": "0.00",
      "credited_amount": "5243.00",
      "outstanding": "25966.76",
      "voided_at": null,
      "void_reason": null,
      "replaced_by_id": null,
      "warnings": [],
      "created_at": "2026-10-08T11:04:52+07:00"
    }
  ],
  "next_cursor": null
}

GET /v1/sales-documents/{id}

Get a sales document

Returns one sales document, with its lines, totals, attachments, payment status and warnings. A pro-forma document SLF recorded answers 404 not_found.

Parameters
NameInTypeRequiredDescription
idpathstringrequiredThe sales document's id (doc_). Example: doc_….
Responses

200 The sales document. · SalesDocument

{
  "id": "doc_9Rt3Vb6Nz1Hs",
  "type": "tax_invoice",
  "external_id": "SO-2026-10-0412",
  "number": "TIV-2026-0412",
  "issue_date": "2026-10-08",
  "due_date": "2026-11-07",
  "valid_until": null,
  "customer_id": "cus_7Hc4Nf2Pq9Xz",
  "bill_to_address": null,
  "currency": "THB",
  "fx_rate": null,
  "fx_date": null,
  "payment_terms": null,
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": null,
  "reference": null,
  "related_invoice_id": null,
  "replaces_id": null,
  "original_document_ids": null,
  "reason_code": null,
  "reason_text": null,
  "original_value": null,
  "correct_value": null,
  "lines": [
    {
      "name": "Office chair, model A",
      "description": null,
      "quantity": "10",
      "unit_price": "2450.00",
      "vat_category": "standard",
      "wht_rate": "0",
      "item_code": "CH-A",
      "account_code": null,
      "charge_role": null,
      "supplier_invoice_billed_to": null,
      "pass_through_cost": null,
      "pre_vat_amount": "24500.00",
      "vat_rate": "7",
      "vat_amount": "1715.00",
      "wht_amount": "0.00"
    },
    {
      "name": "Desk lamp, LED",
      "description": null,
      "quantity": "12",
      "unit_price": "389.00",
      "vat_category": "standard",
      "wht_rate": "0",
      "item_code": "LAMP-LED",
      "account_code": null,
      "charge_role": null,
      "supplier_invoice_billed_to": null,
      "pass_through_cost": null,
      "pre_vat_amount": "4668.00",
      "vat_rate": "7",
      "vat_amount": "326.76",
      "wht_amount": "0.00"
    }
  ],
  "totals": {
    "subtotal": "29168.00",
    "vat_total": "2041.76",
    "total": "31209.76",
    "wht_total": null,
    "net_to_pay": null
  },
  "attachments": [
    {
      "id": "att_2Bh5Rx8Fk3Wq",
      "object_type": "sales_document",
      "object_id": "doc_9Rt3Vb6Nz1Hs",
      "kind": "document_copy",
      "file_name": "TIV-2026-0412.pdf",
      "content_type": "application/pdf",
      "size": 184233,
      "sha256": "9f2b5c1e7a4d8f0c3b6e9a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3a6d9c2f",
      "created_at": "2026-10-08T11:05:37+07:00"
    }
  ],
  "status": "open",
  "paid_amount": "0.00",
  "credited_amount": "5243.00",
  "outstanding": "25966.76",
  "voided_at": null,
  "void_reason": null,
  "replaced_by_id": null,
  "warnings": [],
  "created_at": "2026-10-08T11:04:52+07:00"
}

POST /v1/sales-documents/{id}/void

Void a sales document

Voids a sales document. For a quotation or invoice, this records your cancellation. To cancel and reissue a tax_invoice or tax_invoice_receipt, first record the replacement (new number, issue_date equal to the original's, replaces_id set to the original), then void the original with replacement_id; see Corrections for the Revenue Department procedure. Credit and debit notes cannot be voided through the API (note_cannot_be_voided). A voided document keeps its number and is returned with status: void. Voiding a tax invoice, cash sale or note dated in a filed month is refused with period_filed, and one dated in a closed period with period_closed. A document cannot be voided while receipts recorded against it are not voided (void them first), while a credit or debit note is recorded against it, or once SLF has posted it to your books (void_not_allowed).

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
idpathstringrequiredThe sales document's id (doc_). Example: doc_….
Request body

application/json · SalesDocumentVoidRequest

{
  "reason": "Buyer's address was wrong. Replaced by TIV-2026-0415.",
  "replacement_id": "doc_8Jd2Sv6Wm4Pa"
}
Responses

200 The voided document. · SalesDocument

{
  "id": "doc_1Gh6Tc9Rw2Ne",
  "type": "tax_invoice",
  "external_id": "SO-2026-10-0398",
  "number": "TIV-2026-0398",
  "issue_date": "2026-10-05",
  "due_date": null,
  "valid_until": null,
  "customer_id": "cus_7Hc4Nf2Pq9Xz",
  "bill_to_address": "8 Example Road, Pathum Wan, Bangkok 10330",
  "currency": "THB",
  "fx_rate": null,
  "fx_date": null,
  "payment_terms": null,
  "bank_account_id": null,
  "remark": null,
  "reference": null,
  "related_invoice_id": null,
  "replaces_id": null,
  "original_document_ids": null,
  "reason_code": null,
  "reason_text": null,
  "original_value": null,
  "correct_value": null,
  "lines": [
    {
      "name": "Bookkeeping software licence, 12 months",
      "description": null,
      "quantity": "1",
      "unit_price": "18000.00",
      "vat_category": "standard",
      "wht_rate": "0",
      "item_code": "LIC-12M",
      "account_code": null,
      "charge_role": null,
      "supplier_invoice_billed_to": null,
      "pass_through_cost": null,
      "pre_vat_amount": "18000.00",
      "vat_rate": "7",
      "vat_amount": "1260.00",
      "wht_amount": "0.00"
    }
  ],
  "totals": {
    "subtotal": "18000.00",
    "vat_total": "1260.00",
    "total": "19260.00",
    "wht_total": null,
    "net_to_pay": null
  },
  "attachments": [],
  "status": "void",
  "paid_amount": "0.00",
  "credited_amount": "0.00",
  "outstanding": "0.00",
  "voided_at": "2026-10-29T13:47:20+07:00",
  "void_reason": "Buyer's address was wrong. Replaced by TIV-2026-0415.",
  "replaced_by_id": "doc_8Jd2Sv6Wm4Pa",
  "warnings": [],
  "created_at": "2026-10-05T10:15:02+07:00"
}

409 The document is already voided (already_voided), or it cannot be voided now (void_not_allowed). · Error

{
  "error": {
    "code": "void_not_allowed",
    "message": "Receipt RE-2026-0233 is recorded against this invoice. Void the receipt first.",
    "param": null,
    "details": [],
    "request_id": "req_8d4f2b7e1a6c9d53",
    "existing_id": null
  }
}

422 The document cannot be voided through the API, or its month is filed or closed. · Error

{
  "error": {
    "code": "period_filed",
    "message": "This tax invoice is dated in 2026-09, whose VAT return SLF has already filed. Contact SLF.",
    "param": null,
    "details": [],
    "request_id": "req_3e8b1d6a9c2f5e47",
    "existing_id": null
  }
}
Errors specific to this endpoint

already_voided, void_not_allowed, note_cannot_be_voided, reference_not_found, period_filed, period_closed. Common errors: see Error codes.

Objects

SalesDocument Sales document

A quotation, invoice, tax invoice, cash-sale tax invoice/receipt, credit note or debit note you issued, recorded as printed.

FieldTypeRequiredDescription
idstring
doc_ id
read-onlySLF's id for the document. Example: doc_4Fp9Km2Qx7Ld.
typestring · enum sales_document_typerequiredWhat kind of document it is. Example: invoice.
external_idstring
≤ 100 characters
requiredYour system's id for the document. Unique among your sales documents, voided ones included (duplicate_external_id). Required when you create the document; null on documents SLF entered itself. Example: SO-2026-10-0187.
numberstring
≤ 40 characters
requiredThe number printed on the document, from your own system. It must be unique among your invoices, tax invoices, cash sales, credit notes and debit notes that are not voided, including pro-forma documents SLF recorded; quotations have their own numbers (duplicate_number). A tax invoice or cash sale that SLF already entered by hand under the same number and total is refused with recorded_by_slf. A voided document keeps its number on record. SLF does not number documents. Example: INV-2026-0187.
issue_datedaterequiredThe date printed on the document. Not before your start date. For a tax_invoice or tax_invoice_receipt, also not after today in Thailand, and not before the VAT registration date SLF has on record for your company. Otherwise validation_failed (out_of_range). Example: 2026-10-15.
due_datedateoptionalFor invoice, tax_invoice and debit_note: the payment due date. Not before issue_date. Example: 2026-11-14.
valid_untildateoptionalFor quotation: the last day the quotation is valid. After it, status becomes expired, unless the quotation is rejected or void. Example: 2026-11-30.
customer_idstringrequired unless customer_external_id is givenThe customer the document is issued to. Example: cus_7Hc4Nf2Pq9Xz.
customer_external_idstringrequired unless customer_id is givenInstead of customer_id: the external_id you gave the customer. Request only; responses always carry customer_id. Example: CUST-00412.
bill_to_addressstring
≤ 1000 characters
optionalThe buyer address printed on this document, if different from the customer's address.
currencystring
ISO 4217
optionalThe document's currency. All amounts on the document are in it. A tax_invoice, tax_invoice_receipt, credit_note or debit_note in a currency other than THB is accepted only if SLF holds the Director-General's approval for your company (foreign_currency_tax_invoice_not_allowed; see Tax notes). For some companies SLF converts to THB for VAT at a rate of its own (fx_basis_unavailable; see Conventions). Default: THB. Example: THB.
fx_ratedecimal string
up to 6 decimals
required when currency is not THBTHB per one unit of currency, as used by your system (fx_rate_required). Example: 32.4512.
fx_datedaterequired when currency is not THBThe date of fx_rate. Example: 2026-10-27.
payment_termsstring · enum payment_termsoptionalTrade payment terms printed on the document, if any. Example: tt.
bank_account_idstringoptionalThe bank account printed on the document for payment (a bnk_ id from GET /bank-accounts). Example: bnk_5Qm2Rk8Lt3Vw.
remarkstring
≤ 20000 characters
optionalThe remark printed on the document, as plain text. Example: Please pay by bank transfer within 30 days..
referencestring
≤ 60 characters
optionalYour own reference for grouping documents, such as a job, project or order number. It does not have to be printed on the document; several documents may share it, and lists can be filtered by it. Example: PRJ-2026-031.
related_invoice_idstringoptionalFor a tax_invoice issued for an earlier invoice that has been paid in full (for example, where the tax invoice is issued when payment is received): that invoice's id. The invoice must be for the same customer and currency, fully paid by receipts already recorded, with no credit or debit note on it or on its receipts, and with no other tax invoice issued for it. This tax invoice's total and vat_total must equal what those receipts recorded (their gross_amount and vat_amount), and its issue_date may not be earlier than the invoice or its last payment. It books no VAT again (the receipts did), is returned with status: paid, and cannot be the original of a credit or debit note. Otherwise it is refused with validation_failed (invalid_value on /related_invoice_id).
replaces_idstringoptionalFor a replacement tax invoice in a cancel-and-reissue: the id of the original it replaces. Its issue_date must equal the original's, and the original must be a tax_invoice or tax_invoice_receipt for the same customer that is not voided yet; otherwise validation_failed (invalid_value). See Corrections. Example: doc_1Gh6Tc9Rw2Ne.
original_document_idsarray of stringrequired for credit_note and debit_noteThe originals the note adjusts, for the same customer and not voided (otherwise validation_failed, invalid_value). Each is a tax_invoice or tax_invoice_receipt (doc_ id), or a receipt (rcp_ id) recorded against an invoice whose lines carry VAT, which is the record that booked that sale's VAT; anything else is refused with note_original_not_tax_invoice. One note may cover more than one original (Order ป.80/2542, clause 4), but not tax invoices and receipts together; the note's value is then applied to the originals in the order listed, each up to the value still remaining on it. A note may not be dated before its originals. A note on a receipt changes no document's outstanding, and a credit note on a receipt is accepted only once that invoice is paid in full. Example: doc_9Rt3Vb6Nz1Hs.
reason_codestring · enum credit_note_reason or debit_note_reasonrequired for credit_note and debit_noteWhy the value changed: a value of credit_note_reason for a credit note, of debit_note_reason for a debit note. Example: goods_returned.
reason_textstring
≤ 500 characters
required for credit_note and debit_noteThe short explanation of the reason printed on the note. For other_announced, it must name the cause. Example: 2 chairs returned: gas lift defective.
original_valuedecimal stringrequired for credit_note and debit_noteThe total value before VAT shown on the original document or documents. Example: 29168.00.
correct_valuedecimal stringrequired for credit_note and debit_noteThe correct value before VAT. The difference from original_value must equal the note's subtotal (note_difference_mismatch). Example: 24268.00.
linesarray of SalesDocumentLine
1 to 500 lines
requiredThe lines as printed. For a credit or debit note: the change in value, as positive amounts.
totalsDocumentTotalsrequiredThe totals as printed. Checked against the lines (totals_mismatch).
attachmentsarray of Attachmentread-onlyFiles attached with POST /attachments.
statusstring · enum sales_document_statusread-onlyPayment or validity status. Example: open.
paid_amountdecimal stringread-onlyThe sum of gross_amount of the receipts recorded against the document (cash received plus tax withheld). 0.00 for documents that cannot be receipted; equal to total for a tax_invoice_receipt and for a tax_invoice with related_invoice_id. Example: 0.00.
credited_amountdecimal stringread-onlyWhat credit notes recorded against this document took off it, VAT included. 0.00 if none. Credit notes on a receipt are not counted here. Example: 0.00.
outstandingdecimal stringread-onlytotal minus paid_amount minus credited_amount, never below 0.00. 0.00 for quotations, credit notes, voided documents and tax invoices with related_invoice_id. If credit notes take off more than is still owed, contact SLF about the refund; refunds are not recorded through the API in v1. Example: 21778.91.
voided_atdate-timeread-onlyWhen the document was voided, or null.
void_reasonstringread-onlyThe reason given when voiding, or null.
replaced_by_idstringread-onlyFor a voided tax invoice: the replacement's id, or null. Example: doc_8Jd2Sv6Wm4Pa.
warningsarray of Warningread-onlyPoints flagged for SLF review. See Warnings and review.
created_atdate-timeread-onlyWhen the document was recorded. Example: 2026-10-15T14:02:11+07:00.

SalesDocumentLine Sales document line

One line of a sales document, as printed.

FieldTypeRequiredDescription
namestring
≤ 300 characters
requiredThe item or service, as printed. Example: Accounting system setup.
descriptionstring
≤ 1000 characters
optionalFurther description printed under the name.
quantitydecimal string
> 0, up to 4 decimals
requiredQuantity. Example: 3.
unit_pricedecimal string
≥ 0, 2 decimals
requiredPrice per unit: excluding VAT, or including VAT for standard_incl. Example: 2150.75.
vat_categorystring · enum vat_categoryoptionalHow VAT applies to the line. If your company is not VAT-registered, it is recorded as none whatever you send (warning vat_category_ignored). A disbursement line must be none (disbursement_vat_category). If your company is VAT-registered, a none line that is not a disbursement billed to the customer is counted in unclassified_value until SLF classifies it, and a document in which no line carries VAT or is zero, exempt or such a disbursement is refused (vat_category_unexplained). Default: standard. Example: standard.
wht_ratestring · enum wht_rateoptionalThe rate your customer is expected to withhold from this line. It also caps the tax a receipt can record as withheld (wht_exceeds_expected), and a recorded document cannot be edited, so set it whenever your customer may withhold. Default: 0. Example: 3.
item_codestring
≤ 50 characters
optionalYour product or service code. SLF maps your codes to accounts in your books. A code SLF has not mapped yet is not refused; SLF maps it when it reviews the document. Example: SVC-SUPPORT.
account_codestring
≤ 20 characters
optionalAn active income account code from your company's chart of accounts at SLF. Any other code is refused (account_code_unknown). SLF gives you the codes you may use; there is no chart-of-accounts endpoint in v1. Usually left out, and SLF maps item_code instead.
charge_rolestring · enum charge_roleoptionalWhether the line is your own supply or a disbursement recovered from the customer. There is no default: if you leave it out, the line's role is recorded as not stated and returned as null. It matters for lines with vat_category: none: only a disbursement billed to the customer is counted in out_of_scope_value; for a VAT-registered company, any other none line is counted as unclassified until SLF classifies it. Example: own_supply.
supplier_invoice_billed_tostring · enum supplier_invoice_billed_torequired when charge_role is disbursementWho the supplier's document for the cost is addressed to. Example: customer.
pass_through_costdecimal stringrequired when charge_role is disbursementThe amount you paid on the customer's behalf. A line whose amount is above it is refused with disbursement_marked_up: show the cost as the disbursement line and the difference as a separate own_supply line. For a company that is not VAT-registered, or a document in a currency other than THB, such a line is recorded as sent and flagged with the warning disbursement_marked_up instead. Example: 1500.00.
pre_vat_amountdecimal stringread-onlyThe line's value before VAT, as computed by the API. When a VAT difference is put on a standard_incl line, that line's pre-VAT amount moves by the opposite amount, so its gross stays the same (see How the API checks totals). Example: 6452.25.
vat_ratestring
percentage
read-onlyThe VAT rate applied, following vat_category: 7 for standard and standard_incl, 0 otherwise. Example: 7.
vat_amountdecimal stringread-onlyThe line's VAT, as computed by the API. The last line that carries VAT also takes any difference between that sum and your printed vat_total (see How the API checks totals). Example: 451.66.
wht_amountdecimal stringread-onlyround2(pre_vat_amount × wht_rate / 100). Example: 193.57.

DocumentTotals Document totals

The totals printed on a sales document. The API computes its own from the lines and checks yours against them; see How the API checks totals.

FieldTypeRequiredDescription
subtotaldecimal stringrequiredSum of the lines' pre-VAT amounts, all VAT categories. Example: 20452.25.
vat_totaldecimal stringrequiredSum of the lines' VAT, as printed. Within 0.01 per line that carries VAT of the API's own sum, this figure is recorded; the difference goes on the last line that carries VAT. Example: 1326.66.
totaldecimal stringrequiredsubtotal plus vat_total. Example: 21778.91.
wht_totaldecimal stringoptionalSum of the lines' withholding tax, if printed. Example: 568.57.
net_to_paydecimal stringoptionaltotal minus wht_total, if printed. Example: 21210.34.

SalesDocumentVoidRequest Void request (sales document)

The body of POST /sales-documents/{id}/void.

FieldTypeRequiredDescription
reasonstring
≤ 500 characters
requiredWhy the document is voided. Example: Buyer's address was wrong. Replaced by TIV-2026-0415..
replacement_idstringoptionalWhen a tax invoice is cancelled and reissued: the id of the replacement, recorded first with replaces_id set to this document. Example: doc_8Jd2Sv6Wm4Pa.

Receipts

Not live yet (draft specification). Payments your customers made against an invoice, tax invoice or debit note, including any tax they withheld.

POST /v1/receipts

Record a receipt

Records a payment received against an invoice, tax_invoice or debit_note. amount_received is the money that arrived; wht_amount is the tax the customer withheld. Their sum, gross_amount, reduces the document's outstanding and may not exceed it (receipt_exceeds_outstanding). wht_amount may not exceed the withholding still expected from the document lines' wht_rate (wht_exceeds_expected); if it implies a different rate, the receipt is recorded with the warning wht_rate_review. A quotation, a tax_invoice_receipt (it is its own receipt), a credit note or a voided document cannot be receipted (receipt_not_allowed). The receipt takes the document's currency and exchange rate. A receipt that books VAT is checked against the month of its payment_date (period_filed), and any receipt dated in a closed period is refused (period_closed). A payment_date after today or before your start date is refused with validation_failed (out_of_range). Your receipt number must be unique among your receipts that are not voided (duplicate_number). A receipt for less than the document still owes is recorded with the warning short_payment, and your remark is stored unchanged. If SLF had entered by hand the VAT of the document being paid (an entry with its number and total), a receipt that books VAT depends on that entry's month. In a month whose VAT return SLF has filed, the receipt is recorded with the warning recorded_by_slf_filed, whether or not the entry is posted. In a month not yet filed, the receipt takes the entry's place, unless SLF has posted the entry to your books: then the receipt is refused with 409 recorded_by_slf_posted. A receipt that books VAT for a customer who has a tax_invoice that is not voided, of the same amount, without related_invoice_id, dated in the month of the receipt's payment_date, is recorded with the warning possible_double_vat.

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
Request body

application/json · Receipt

{
  "external_id": "RCV-2026-10-0233",
  "number": "RE-2026-0233",
  "document_id": "doc_4Fp9Km2Qx7Ld",
  "payment_date": "2026-10-30",
  "amount_received": "21210.34",
  "wht_amount": "568.57",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Customer withheld 3% on the service lines; 50 ทวิ certificate received."
}
Responses

201 The receipt was recorded. Check warnings. · Receipt

{
  "id": "rcp_3Np8Gz5Kd1Vt",
  "external_id": "RCV-2026-10-0233",
  "number": "RE-2026-0233",
  "document_id": "doc_4Fp9Km2Qx7Ld",
  "payment_date": "2026-10-30",
  "amount_received": "21210.34",
  "wht_amount": "568.57",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Customer withheld 3% on the service lines; 50 ทวิ certificate received.",
  "gross_amount": "21778.91",
  "vat_amount": "1326.66",
  "currency": "THB",
  "fx_rate": null,
  "attachments": [],
  "warnings": [],
  "voided_at": null,
  "void_reason": null,
  "created_at": "2026-10-30T17:08:26+07:00"
}

409 A receipt with this external_id or number already exists, or SLF has posted its own hand entry for this document's VAT in a month it has not filed (recorded_by_slf_posted). · Error

{
  "error": {
    "code": "duplicate_external_id",
    "message": "A receipt with external_id RCV-2026-10-0233 already exists.",
    "param": "/external_id",
    "details": [],
    "request_id": "req_7a3e9c1d5b8f2a64",
    "existing_id": "rcp_3Np8Gz5Kd1Vt"
  }
}

422 The receipt is invalid or not allowed for this document. · Error

{
  "error": {
    "code": "receipt_exceeds_outstanding",
    "message": "amount_received plus wht_amount (21778.91) is more than the outstanding 10000.00.",
    "param": "/amount_received",
    "details": [],
    "request_id": "req_2d7a5c9f3e1b8d42",
    "existing_id": null
  }
}
Errors specific to this endpoint

duplicate_external_id, duplicate_number, recorded_by_slf_posted, reference_not_found, receipt_not_allowed, receipt_exceeds_outstanding, wht_exceeds_expected, period_filed, period_closed. Common errors: see Error codes.

GET /v1/receipts

List receipts

Returns your receipts, newest first.

Parameters
NameInTypeRequiredDescription
document_idquerystringoptionalOnly receipts against this sales document. Example: doc_4Fp9Km2Qx7Ld.
external_idquerystringoptionalOnly the receipt with this external_id. Example: RCV-2026-10-0233.
payment_date_fromquerydateoptionalOnly receipts with a payment_date on or after this date. Example: 2026-10-01.
payment_date_toquerydateoptionalOnly receipts with a payment_date on or before this date. Example: 2026-10-31.
limitqueryintegeroptionalNumber of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25.
cursorquerystring
≤ 512 characters
optionalThe next_cursor value from the previous page, unchanged. Omit it for the first page; an empty value is refused. For the refusals, see Pagination in Conventions. Example: cur_8Tn3XqL5Vd.
Responses

200 A page of receipts. · list of Receipt

{
  "data": [
    {
      "id": "rcp_3Np8Gz5Kd1Vt",
      "external_id": "RCV-2026-10-0233",
      "number": "RE-2026-0233",
      "document_id": "doc_4Fp9Km2Qx7Ld",
      "payment_date": "2026-10-30",
      "amount_received": "21210.34",
      "wht_amount": "568.57",
      "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
      "remark": "Customer withheld 3% on the service lines; 50 ทวิ certificate received.",
      "gross_amount": "21778.91",
      "vat_amount": "1326.66",
      "currency": "THB",
      "fx_rate": null,
      "attachments": [],
      "warnings": [],
      "voided_at": null,
      "void_reason": null,
      "created_at": "2026-10-30T17:08:26+07:00"
    }
  ],
  "next_cursor": null
}

GET /v1/receipts/{id}

Get a receipt

Returns one receipt.

Parameters
NameInTypeRequiredDescription
idpathstringrequiredThe receipt's id (rcp_). Example: rcp_….
Responses

200 The receipt. · Receipt

{
  "id": "rcp_3Np8Gz5Kd1Vt",
  "external_id": "RCV-2026-10-0233",
  "number": "RE-2026-0233",
  "document_id": "doc_4Fp9Km2Qx7Ld",
  "payment_date": "2026-10-30",
  "amount_received": "21210.34",
  "wht_amount": "568.57",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Customer withheld 3% on the service lines; 50 ทวิ certificate received.",
  "gross_amount": "21778.91",
  "vat_amount": "1326.66",
  "currency": "THB",
  "fx_rate": null,
  "attachments": [],
  "warnings": [],
  "voided_at": null,
  "void_reason": null,
  "created_at": "2026-10-30T17:08:26+07:00"
}

POST /v1/receipts/{id}/void

Void a receipt

Voids a receipt recorded by mistake. The document's outstanding goes back up by the receipt's gross_amount. A receipt that booked VAT cannot be voided once its month is filed (period_filed), and no receipt can be voided once its payment_date falls in a closed period (period_closed). It is also refused with void_not_allowed once SLF has posted it to your books, while a credit or debit note is recorded against it or a credit note depends on it, and while a tax_invoice with related_invoice_id stands on its invoice. If the receipt took the place of a VAT entry SLF had made by hand, voiding it restores that entry.

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
idpathstringrequiredThe receipt's id (rcp_). Example: rcp_….
Request body

application/json · VoidRequest

{
  "reason": "Recorded against the wrong invoice."
}
Responses

200 The voided receipt. · Receipt

{
  "id": "rcp_3Np8Gz5Kd1Vt",
  "external_id": "RCV-2026-10-0233",
  "number": "RE-2026-0233",
  "document_id": "doc_4Fp9Km2Qx7Ld",
  "payment_date": "2026-10-30",
  "amount_received": "21210.34",
  "wht_amount": "568.57",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "remark": "Customer withheld 3% on the service lines; 50 ทวิ certificate received.",
  "gross_amount": "21778.91",
  "vat_amount": "1326.66",
  "currency": "THB",
  "fx_rate": null,
  "attachments": [],
  "warnings": [],
  "voided_at": "2026-10-31T09:03:12+07:00",
  "void_reason": "Recorded against the wrong invoice.",
  "created_at": "2026-10-30T17:08:26+07:00"
}

409 The receipt is already voided (already_voided), or it cannot be voided now (void_not_allowed). · Error

{
  "error": {
    "code": "already_voided",
    "message": "This record is already voided.",
    "param": null,
    "details": [],
    "request_id": "req_5c9a1e7d3b6f2a48",
    "existing_id": null
  }
}

422 The receipt is dated in a filed month or a closed period. · Error

{
  "error": {
    "code": "period_filed",
    "message": "This receipt booked VAT in a month whose VAT return SLF has already filed. Contact SLF.",
    "param": null,
    "details": [],
    "request_id": "req_9b2e6c4a8d1f3e75",
    "existing_id": null
  }
}
Errors specific to this endpoint

already_voided, void_not_allowed, period_filed, period_closed. Common errors: see Error codes.

Objects

Receipt Receipt

A payment received from a customer against an invoice, tax invoice or debit note.

FieldTypeRequiredDescription
idstring
rcp_ id
read-onlySLF's id for the receipt. Example: rcp_3Np8Gz5Kd1Vt.
external_idstring
≤ 100 characters
requiredYour system's id for the receipt. Unique among your receipts, voided ones included (duplicate_external_id). Required when you create the receipt; null on receipts SLF entered itself. Example: RCV-2026-10-0233.
numberstring
≤ 40 characters
requiredYour receipt number, as printed. Unique among your receipts that are not voided (duplicate_number). Example: RE-2026-0233.
document_idstringrequiredThe invoice, tax_invoice or debit_note being paid. Example: doc_4Fp9Km2Qx7Ld.
payment_datedaterequiredThe date the money arrived. Not after today in Thailand, and not before your start date (validation_failed, out_of_range). Example: 2026-10-30.
amount_receiveddecimal stringrequiredThe money that arrived, in the document's currency. Example: 21210.34.
wht_amountdecimal stringoptionalTax the customer withheld from this payment. Not more than the withholding still expected from the document's lines (wht_exceeds_expected). Default: 0.00. Example: 568.57.
bank_account_idstringoptionalThe account the money arrived in. Example: bnk_5Qm2Rk8Lt3Vw.
remarkstring
≤ 20000 characters
optionalYour remark, as plain text. Stored as you sent it: a short payment is flagged with the warning short_payment, not written into the remark.
gross_amountdecimal stringread-onlyamount_received plus wht_amount. This is what reduces the document's outstanding. Example: 21778.91.
vat_amountdecimal stringread-onlyThe VAT this payment booked: round2(vat_total × gross_amount / total) of the document, halves rounded up, where vat_total is the VAT recorded for the document (as printed; see How the API checks totals). The receipt that brings the document's outstanding to 0.00 takes the document's vat_total less the VAT of its earlier receipts, so the shares add up. 0.00 for a receipt against a tax_invoice or debit_note, whose VAT that document booked. Example: 1326.66.
currencystring
ISO 4217
read-onlyTaken from the document. Example: THB.
fx_ratedecimal stringread-onlyTaken from the document; null for THB.
attachmentsarray of Attachmentread-onlyFiles attached with POST /attachments.
warningsarray of Warningread-onlyPoints flagged for SLF review, such as wht_rate_review or short_payment.
voided_atdate-timeread-onlyWhen the receipt was voided, or null.
void_reasonstringread-onlyThe reason given when voiding, or null.
created_atdate-timeread-onlyWhen the receipt was recorded. Example: 2026-10-30T17:08:26+07:00.

VoidRequest Void or reverse request

The body of POST /receipts/{id}/void, POST /bills/{id}/void and POST /payments/{id}/reverse.

FieldTypeRequiredDescription
reasonstring
≤ 500 characters
requiredWhy the record is voided or reversed. Example: Recorded against the wrong invoice..

Supplier bills

Not live yet (draft specification). Second release (see Two releases): once the first release is live, these paths answer 404 not_found until the second opens. Documents you received from suppliers, and the credit and debit notes suppliers issued against them.

POST /v1/bills

Record a supplier bill

Second release (see Two releases): once the first release is live, this path answers 404 not_found until the second opens. Records a document you received from a supplier. Amounts are typed from the printed document and are not recomputed; the API only checks that total equals subtotal plus vat_amount (totals_mismatch). Lines are optional and help SLF classify the cost; their amounts are not added up against the header. A second bill with the same supplier tax_id (or, for a supplier without one, the same supplier name) and bill_number is refused with 409 duplicate_bill. If the bill is a cost you paid on a customer's behalf and is addressed to that customer, set on_behalf_of_customer_id.

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
Request body

application/json · Bill

{
  "external_id": "AP-2026-10-0058",
  "document_type": "tax_invoice",
  "supplier": {
    "name": "Demo Services Co., Ltd.",
    "tax_id": "0999000000035",
    "branch_no": "00000",
    "country": "TH"
  },
  "bill_number": "DS-2610-0058",
  "bill_date": "2026-10-25",
  "due_date": "2026-11-24",
  "currency": "THB",
  "subtotal": "15000.00",
  "vat_amount": "1050.00",
  "total": "16050.00",
  "lines": [
    {
      "description": "IT support, October 2026",
      "quantity": "1",
      "unit_price": "15000.00",
      "amount": "15000.00",
      "item_code": "DS-ITS-01"
    }
  ],
  "remark": "Monthly IT support contract",
  "reference": null
}
Responses

201 The bill was recorded. · Bill

{
  "id": "bil_5Tg9Hw2Lr6Mx",
  "external_id": "AP-2026-10-0058",
  "document_type": "tax_invoice",
  "supplier": {
    "name": "Demo Services Co., Ltd.",
    "tax_id": "0999000000035",
    "branch_no": "00000",
    "country": "TH"
  },
  "bill_number": "DS-2610-0058",
  "bill_date": "2026-10-25",
  "due_date": "2026-11-24",
  "currency": "THB",
  "fx_rate": null,
  "fx_date": null,
  "subtotal": "15000.00",
  "vat_amount": "1050.00",
  "total": "16050.00",
  "lines": [
    {
      "description": "IT support, October 2026",
      "quantity": "1",
      "unit_price": "15000.00",
      "amount": "15000.00",
      "item_code": "DS-ITS-01",
      "account_code": null
    }
  ],
  "on_behalf_of_customer_id": null,
  "remark": "Monthly IT support contract",
  "reference": null,
  "status": "open",
  "paid_amount": "0.00",
  "outstanding": "16050.00",
  "notes": [],
  "attachments": [],
  "voided_at": null,
  "void_reason": null,
  "created_at": "2026-10-26T09:41:15+07:00"
}

409 A bill with this external_id, or this supplier tax_id and bill_number, already exists. · Error

{
  "error": {
    "code": "duplicate_bill",
    "message": "A bill DS-2610-0058 from supplier 0999000000035 already exists.",
    "param": "/bill_number",
    "details": [],
    "request_id": "req_8c2e6a4f1d9b3c75",
    "existing_id": "bil_5Tg9Hw2Lr6Mx"
  }
}

422 The bill is invalid. · Error

{
  "error": {
    "code": "validation_failed",
    "message": "2 fields are invalid.",
    "param": null,
    "details": [
      {
        "param": "/tax_id",
        "code": "invalid_tax_id",
        "message": "The check digit does not match.",
        "expected": null,
        "received": null
      },
      {
        "param": "/email",
        "code": "invalid_format",
        "message": "Not an email address.",
        "expected": null,
        "received": null
      }
    ],
    "request_id": "req_3c9e1f7a2b5d8c04",
    "existing_id": null
  }
}
Errors specific to this endpoint

duplicate_external_id, duplicate_bill, reference_not_found, totals_mismatch, fx_rate_required, account_code_unknown. Common errors: see Error codes.

GET /v1/bills

List supplier bills

Second release (see Two releases): once the first release is live, this path answers 404 not_found until the second opens. Returns your supplier bills, newest first.

Parameters
NameInTypeRequiredDescription
external_idquerystringoptionalOnly the bill with this external_id. Example: AP-2026-10-0058.
supplier_tax_idquerystringoptionalOnly bills from the supplier with this tax ID. Example: 0999000000035.
referencequerystringoptionalOnly bills with this reference. Example: PRJ-2026-031.
statusquerystring · enum bill_statusoptionalOnly bills with this status, such as open to find what you still owe. Example: open.
bill_date_fromquerydateoptionalOnly bills with a bill_date on or after this date. Example: 2026-10-01.
bill_date_toquerydateoptionalOnly bills with a bill_date on or before this date. Example: 2026-10-31.
limitqueryintegeroptionalNumber of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25.
cursorquerystring
≤ 512 characters
optionalThe next_cursor value from the previous page, unchanged. Omit it for the first page; an empty value is refused. For the refusals, see Pagination in Conventions. Example: cur_8Tn3XqL5Vd.
Responses

200 A page of bills. · list of Bill

{
  "data": [
    {
      "id": "bil_5Tg9Hw2Lr6Mx",
      "external_id": "AP-2026-10-0058",
      "document_type": "tax_invoice",
      "supplier": {
        "name": "Demo Services Co., Ltd.",
        "tax_id": "0999000000035",
        "branch_no": "00000",
        "country": "TH"
      },
      "bill_number": "DS-2610-0058",
      "bill_date": "2026-10-25",
      "due_date": "2026-11-24",
      "currency": "THB",
      "fx_rate": null,
      "fx_date": null,
      "subtotal": "15000.00",
      "vat_amount": "1050.00",
      "total": "16050.00",
      "lines": [
        {
          "description": "IT support, October 2026",
          "quantity": "1",
          "unit_price": "15000.00",
          "amount": "15000.00",
          "item_code": "DS-ITS-01",
          "account_code": null
        }
      ],
      "on_behalf_of_customer_id": null,
      "remark": "Monthly IT support contract",
      "reference": null,
      "status": "open",
      "paid_amount": "0.00",
      "outstanding": "16050.00",
      "notes": [],
      "attachments": [],
      "voided_at": null,
      "void_reason": null,
      "created_at": "2026-10-26T09:41:15+07:00"
    }
  ],
  "next_cursor": null
}

GET /v1/bills/{id}

Get a supplier bill

Second release (see Two releases): once the first release is live, this path answers 404 not_found until the second opens. Returns one bill, with its payment status and any supplier notes recorded against it.

Parameters
NameInTypeRequiredDescription
idpathstringrequiredThe bill's id (bil_). Example: bil_….
Responses

200 The bill. · Bill

{
  "id": "bil_5Tg9Hw2Lr6Mx",
  "external_id": "AP-2026-10-0058",
  "document_type": "tax_invoice",
  "supplier": {
    "name": "Demo Services Co., Ltd.",
    "tax_id": "0999000000035",
    "branch_no": "00000",
    "country": "TH"
  },
  "bill_number": "DS-2610-0058",
  "bill_date": "2026-10-25",
  "due_date": "2026-11-24",
  "currency": "THB",
  "fx_rate": null,
  "fx_date": null,
  "subtotal": "15000.00",
  "vat_amount": "1050.00",
  "total": "16050.00",
  "lines": [
    {
      "description": "IT support, October 2026",
      "quantity": "1",
      "unit_price": "15000.00",
      "amount": "15000.00",
      "item_code": "DS-ITS-01",
      "account_code": null
    }
  ],
  "on_behalf_of_customer_id": null,
  "remark": "Monthly IT support contract",
  "reference": null,
  "status": "open",
  "paid_amount": "0.00",
  "outstanding": "16050.00",
  "notes": [],
  "attachments": [],
  "voided_at": null,
  "void_reason": null,
  "created_at": "2026-10-26T09:41:15+07:00"
}

POST /v1/bills/{id}/void

Void a supplier bill

Second release (see Two releases): once the first release is live, this path answers 404 not_found until the second opens. Voids a bill recorded by mistake. It stays visible with status: void. A bill cannot be voided while payments allocated to it are not reversed (reverse them first), or while a supplier note on it has been posted by SLF to your books (void_not_allowed).

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
idpathstringrequiredThe bill's id (bil_). Example: bil_….
Request body

application/json · VoidRequest

{
  "reason": "The supplier withdrew this bill."
}
Responses

200 The voided bill. · Bill

{
  "id": "bil_8Kd3Wq6Ln1Rz",
  "external_id": "AP-2026-10-0057",
  "document_type": "tax_invoice",
  "supplier": {
    "name": "Demo Services Co., Ltd.",
    "tax_id": "0999000000035",
    "branch_no": "00000",
    "country": "TH"
  },
  "bill_number": "DS-2610-0057",
  "bill_date": "2026-10-25",
  "due_date": "2026-11-24",
  "currency": "THB",
  "fx_rate": null,
  "fx_date": null,
  "subtotal": "15000.00",
  "vat_amount": "1050.00",
  "total": "16050.00",
  "lines": [
    {
      "description": "IT support, October 2026",
      "quantity": "1",
      "unit_price": "15000.00",
      "amount": "15000.00",
      "item_code": "DS-ITS-01",
      "account_code": null
    }
  ],
  "on_behalf_of_customer_id": null,
  "remark": "Monthly IT support contract",
  "reference": null,
  "status": "void",
  "paid_amount": "0.00",
  "outstanding": "0.00",
  "notes": [],
  "attachments": [],
  "voided_at": "2026-10-27T08:30:00+07:00",
  "void_reason": "The supplier withdrew this bill.",
  "created_at": "2026-10-26T09:41:15+07:00"
}

409 The bill is already voided (already_voided), or it cannot be voided now (void_not_allowed). · Error

{
  "error": {
    "code": "already_voided",
    "message": "This record is already voided.",
    "param": null,
    "details": [],
    "request_id": "req_5c9a1e7d3b6f2a48",
    "existing_id": null
  }
}
Errors specific to this endpoint

already_voided, void_not_allowed. Common errors: see Error codes.

POST /v1/bills/{id}/notes

Record a supplier credit or debit note

Second release (see Two releases): once the first release is live, this path answers 404 not_found until the second opens. Records a credit or debit note your supplier issued against a bill. amount is the change before VAT and vat_amount the VAT on it, both as printed; reason is the supplier's reason, as free text. The note changes the bill's outstanding. A credit note may not be more than the bill's total less the supplier credit notes already recorded against it (validation_failed, out_of_range), and no note is accepted on a voided bill.

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
idpathstringrequiredThe bill's id (bil_). Example: bil_….
Request body

application/json · BillNote

{
  "external_id": "AP-CN-2026-11-0007",
  "type": "credit_note",
  "number": "DS-CN-0007",
  "date": "2026-11-02",
  "amount": "1000.00",
  "vat_amount": "70.00",
  "reason": "Two support visits were not provided"
}
Responses

201 The note was recorded. · BillNote

{
  "id": "bnt_1Qz6Ck3Pv8Ys",
  "bill_id": "bil_2Cw8Np4Vs7Kf",
  "external_id": "AP-CN-2026-11-0007",
  "type": "credit_note",
  "number": "DS-CN-0007",
  "date": "2026-11-02",
  "amount": "1000.00",
  "vat_amount": "70.00",
  "reason": "Two support visits were not provided",
  "created_at": "2026-11-02T14:22:40+07:00"
}

409 A supplier note with this external_id already exists. · Error

{
  "error": {
    "code": "duplicate_external_id",
    "message": "A supplier note with external_id AP-CN-2026-11-0007 already exists.",
    "param": "/external_id",
    "details": [],
    "request_id": "req_6a2d9f4c1e8b3a70",
    "existing_id": "bnt_1Qz6Ck3Pv8Ys"
  }
}

422 The note is invalid. · Error

{
  "error": {
    "code": "validation_failed",
    "message": "2 fields are invalid.",
    "param": null,
    "details": [
      {
        "param": "/tax_id",
        "code": "invalid_tax_id",
        "message": "The check digit does not match.",
        "expected": null,
        "received": null
      },
      {
        "param": "/email",
        "code": "invalid_format",
        "message": "Not an email address.",
        "expected": null,
        "received": null
      }
    ],
    "request_id": "req_3c9e1f7a2b5d8c04",
    "existing_id": null
  }
}
Errors specific to this endpoint

duplicate_external_id. Common errors: see Error codes.

Objects

Bill Supplier bill

A document you received from a supplier, with amounts typed as printed.

FieldTypeRequiredDescription
idstring
bil_ id
read-onlySLF's id for the bill. Example: bil_5Tg9Hw2Lr6Mx.
external_idstring
≤ 100 characters
requiredYour system's id for the bill. Unique among your bills, voided ones included (duplicate_external_id). Required when you create the bill; null on bills SLF entered itself. Example: AP-2026-10-0058.
document_typestring · enum bill_document_typerequiredWhat the supplier gave you. Example: tax_invoice.
supplierSupplierrequiredThe supplier, as printed on the document.
bill_numberstring
≤ 40 characters
requiredThe supplier's document number. Example: DS-2610-0058.
bill_datedaterequiredThe date printed on the supplier's document. Example: 2026-10-25.
due_datedateoptionalWhen the bill is due. Example: 2026-11-24.
currencystring
ISO 4217
optionalThe bill's currency. Default: THB. Example: THB.
fx_ratedecimal string
up to 6 decimals
required when currency is not THBTHB per one unit of currency (fx_rate_required).
fx_datedaterequired when currency is not THBThe date of fx_rate.
subtotaldecimal stringrequiredValue before VAT, as printed. Example: 15000.00.
vat_amountdecimal stringrequiredVAT as printed; 0.00 if none. Example: 1050.00.
totaldecimal stringrequiredTotal as printed. Must equal subtotal plus vat_amount (totals_mismatch). Example: 16050.00.
linesarray of BillLine
0 to 500 lines
optionalThe bill's lines, if you have them. They help SLF classify the cost.
on_behalf_of_customer_idstringoptionalWhen the bill is a cost you paid on a customer's behalf and is addressed to that customer (the cost side of a disbursement): that customer's id.
remarkstring
≤ 20000 characters
optionalYour remark, as plain text.
referencestring
≤ 60 characters
optionalYour own reference for grouping documents, such as a job, project or order number. It does not have to be printed on the document; several documents may share it, and lists can be filtered by it. Use the same value as on the sales documents it belongs with to see income and costs together. Example: PRJ-2026-031.
statusstring · enum bill_statusread-onlyPayment status. Example: open.
paid_amountdecimal stringread-onlyCash paid plus tax withheld, from payment allocations to this bill. Example: 0.00.
outstandingdecimal stringread-onlytotal minus paid_amount, less supplier credit notes and plus supplier debit notes recorded against the bill. Example: 16050.00.
notesarray of BillNoteread-onlySupplier credit and debit notes recorded against the bill.
attachmentsarray of Attachmentread-onlyFiles attached with POST /attachments.
voided_atdate-timeread-onlyWhen the bill was voided, or null.
void_reasonstringread-onlyThe reason given when voiding, or null.
created_atdate-timeread-onlyWhen the bill was recorded. Example: 2026-10-26T09:41:15+07:00.

Supplier Supplier

The supplier as printed on a bill. Suppliers are not a separate resource in v1.

FieldTypeRequiredDescription
namestring
≤ 300 characters
requiredThe supplier's name. Example: Demo Services Co., Ltd..
tax_idstring
13 digits when country is TH
optionalThe supplier's tax ID. For TH, the check digit is validated (invalid_tax_id). Together with bill_number it identifies the bill (duplicate_bill); without a tax ID, the supplier's name and bill_number identify it. Example: 0999000000035.
branch_nostring
5 digits
optionalThe supplier's branch, as printed. 00000 is the head office. Example: 00000.
countrystring
ISO 3166-1 alpha-2
optionalThe supplier's country. Default: TH. Example: TH.

BillLine Supplier bill line

One line of a supplier's document. Line amounts are not added up against the bill's header.

FieldTypeRequiredDescription
descriptionstring
≤ 1000 characters
requiredThe line as printed. Example: IT support, October 2026.
quantitydecimal string
> 0, up to 4 decimals
optionalQuantity. Example: 1.
unit_pricedecimal string
≥ 0
optionalPrice per unit. Example: 15000.00.
amountdecimal stringrequiredThe line amount as printed. Example: 15000.00.
item_codestring
≤ 50 characters
optionalThe supplier's item code, if printed. SLF maps it to an account. Example: DS-ITS-01.
account_codestring
≤ 20 characters
optionalAn active account code from your company's chart of accounts at SLF (account_code_unknown). SLF gives you the codes you may use. Usually left out.

BillNote Supplier adjustment note

A credit or debit note your supplier issued against a bill.

FieldTypeRequiredDescription
idstring
bnt_ id
read-onlySLF's id for the note. Example: bnt_1Qz6Ck3Pv8Ys.
bill_idstringread-onlyThe bill the note adjusts (from the path). Example: bil_2Cw8Np4Vs7Kf.
external_idstring
≤ 100 characters
requiredYour system's id for the note. Unique among your supplier notes (duplicate_external_id). Required when you create the note; null on notes SLF entered itself. Example: AP-CN-2026-11-0007.
typestring · enum bill_note_typerequiredCredit or debit note. Example: credit_note.
numberstring
≤ 40 characters
requiredThe supplier's note number. Example: DS-CN-0007.
datedaterequiredThe date printed on the note. Example: 2026-11-02.
amountdecimal stringrequiredThe change in value before VAT, as printed. Example: 1000.00.
vat_amountdecimal stringrequiredThe VAT on that change, as printed; 0.00 if none. Example: 70.00.
reasonstring
≤ 500 characters
requiredThe supplier's reason, as free text. Example: Two support visits were not provided.
created_atdate-timeread-onlyWhen the note was recorded. Example: 2026-11-02T14:22:40+07:00.

Supplier payments

Not live yet (draft specification). Second release (see Two releases): once the first release is live, these paths answer 404 not_found until the second opens. Payments you made to suppliers, allocated to bills, with the tax you withheld and the withholding tax certificates (50 ทวิ) you issued.

POST /v1/payments

Record a supplier payment

Second release (see Two releases): once the first release is live, this path answers 404 not_found until the second opens. Records a payment you made. Each allocation gives the cash paid towards one bill and the tax you withheld from it, in the bill's currency; together they reduce the bill's outstanding, and an allocation larger than the outstanding is refused with validation_failed (out_of_range). When you withheld tax, list it in withholding, one entry per payee and income type, as on your 50 ทวิ certificate. Withholding entries are in THB, so a payment with withholding may be allocated only to bills in THB (validation_failed, invalid_value). The withheld amounts in allocations and in withholding must add up to the same total (withholding_mismatch), and each entry's wht_amount must equal round2(gross_amount × rate / 100) (totals_mismatch). return_form is set from payee_type, and SLF confirms it on review. A payment with withholding dated in a month whose withholding return of that return_form SLF has filed is refused with period_filed; any payment dated in a closed period with period_closed. Rates are what you actually withheld; the API does not check them against income types, and SLF reviews them. The example withholds 3% on a service fee; the number is only an example, not a rule.

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
Request body

application/json · Payment

{
  "external_id": "AP-PAY-2026-10-0031",
  "pay_date": "2026-10-31",
  "method": "transfer",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "reference": "TRF-20261031-0007",
  "allocations": [
    {
      "bill_id": "bil_5Tg9Hw2Lr6Mx",
      "amount": "15600.00",
      "wht_amount": "450.00"
    }
  ],
  "withholding": [
    {
      "payee_name": "Demo Services Co., Ltd.",
      "payee_tax_id": "0999000000035",
      "payee_branch_no": "00000",
      "payee_address": "1 Example Road, Khlong Toei, Bangkok 10110",
      "payee_type": "company",
      "income_type": "service",
      "income_description": null,
      "gross_amount": "15000.00",
      "rate": "3",
      "wht_amount": "450.00",
      "certificate_number": "WHT-2026-0101"
    }
  ]
}
Responses

201 The payment was recorded. · Payment

{
  "id": "pay_7Mv4Ys1Jn9Bc",
  "external_id": "AP-PAY-2026-10-0031",
  "pay_date": "2026-10-31",
  "method": "transfer",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "reference": "TRF-20261031-0007",
  "allocations": [
    {
      "bill_id": "bil_5Tg9Hw2Lr6Mx",
      "amount": "15600.00",
      "wht_amount": "450.00"
    }
  ],
  "withholding": [
    {
      "payee_name": "Demo Services Co., Ltd.",
      "payee_tax_id": "0999000000035",
      "payee_branch_no": "00000",
      "payee_address": "1 Example Road, Khlong Toei, Bangkok 10110",
      "payee_type": "company",
      "income_type": "service",
      "income_description": null,
      "gross_amount": "15000.00",
      "rate": "3",
      "wht_amount": "450.00",
      "certificate_number": "WHT-2026-0101",
      "return_form": "pnd53"
    }
  ],
  "attachments": [],
  "state": "recorded",
  "reversed_at": null,
  "reverse_reason": null,
  "created_at": "2026-10-31T16:55:02+07:00"
}

409 A payment with this external_id already exists. · Error

{
  "error": {
    "code": "duplicate_external_id",
    "message": "A payment with external_id AP-PAY-2026-10-0031 already exists.",
    "param": "/external_id",
    "details": [],
    "request_id": "req_1c7e4a9d2f6b8e53",
    "existing_id": "pay_7Mv4Ys1Jn9Bc"
  }
}

422 The payment is invalid, its withholding does not add up, or its month is filed or closed. · Error

{
  "error": {
    "code": "withholding_mismatch",
    "message": "allocations withhold 450.00 in total but withholding lists 405.00.",
    "param": "/withholding",
    "details": [
      {
        "param": "/withholding",
        "code": "mismatch",
        "message": "Sum of withholding[].wht_amount.",
        "expected": "450.00",
        "received": "405.00"
      }
    ],
    "request_id": "req_4b1d8f3a6c2e9d17",
    "existing_id": null
  }
}
Errors specific to this endpoint

duplicate_external_id, reference_not_found, withholding_mismatch, totals_mismatch, period_filed, period_closed. Common errors: see Error codes.

GET /v1/payments

List supplier payments

Second release (see Two releases): once the first release is live, this path answers 404 not_found until the second opens. Returns your supplier payments, newest first.

Parameters
NameInTypeRequiredDescription
external_idquerystringoptionalOnly the payment with this external_id. Example: AP-PAY-2026-10-0031.
bill_idquerystringoptionalOnly payments with an allocation to this bill. Example: bil_5Tg9Hw2Lr6Mx.
pay_date_fromquerydateoptionalOnly payments with a pay_date on or after this date. Example: 2026-10-01.
pay_date_toquerydateoptionalOnly payments with a pay_date on or before this date. Example: 2026-10-31.
limitqueryintegeroptionalNumber of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25.
cursorquerystring
≤ 512 characters
optionalThe next_cursor value from the previous page, unchanged. Omit it for the first page; an empty value is refused. For the refusals, see Pagination in Conventions. Example: cur_8Tn3XqL5Vd.
Responses

200 A page of payments. · list of Payment

{
  "data": [
    {
      "id": "pay_7Mv4Ys1Jn9Bc",
      "external_id": "AP-PAY-2026-10-0031",
      "pay_date": "2026-10-31",
      "method": "transfer",
      "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
      "reference": "TRF-20261031-0007",
      "allocations": [
        {
          "bill_id": "bil_5Tg9Hw2Lr6Mx",
          "amount": "15600.00",
          "wht_amount": "450.00"
        }
      ],
      "withholding": [
        {
          "payee_name": "Demo Services Co., Ltd.",
          "payee_tax_id": "0999000000035",
          "payee_branch_no": "00000",
          "payee_address": "1 Example Road, Khlong Toei, Bangkok 10110",
          "payee_type": "company",
          "income_type": "service",
          "income_description": null,
          "gross_amount": "15000.00",
          "rate": "3",
          "wht_amount": "450.00",
          "certificate_number": "WHT-2026-0101",
          "return_form": "pnd53"
        }
      ],
      "attachments": [],
      "state": "recorded",
      "reversed_at": null,
      "reverse_reason": null,
      "created_at": "2026-10-31T16:55:02+07:00"
    }
  ],
  "next_cursor": null
}

GET /v1/payments/{id}

Get a supplier payment

Second release (see Two releases): once the first release is live, this path answers 404 not_found until the second opens. Returns one payment, with its allocations and withholding.

Parameters
NameInTypeRequiredDescription
idpathstringrequiredThe payment's id (pay_). Example: pay_….
Responses

200 The payment. · Payment

{
  "id": "pay_7Mv4Ys1Jn9Bc",
  "external_id": "AP-PAY-2026-10-0031",
  "pay_date": "2026-10-31",
  "method": "transfer",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "reference": "TRF-20261031-0007",
  "allocations": [
    {
      "bill_id": "bil_5Tg9Hw2Lr6Mx",
      "amount": "15600.00",
      "wht_amount": "450.00"
    }
  ],
  "withholding": [
    {
      "payee_name": "Demo Services Co., Ltd.",
      "payee_tax_id": "0999000000035",
      "payee_branch_no": "00000",
      "payee_address": "1 Example Road, Khlong Toei, Bangkok 10110",
      "payee_type": "company",
      "income_type": "service",
      "income_description": null,
      "gross_amount": "15000.00",
      "rate": "3",
      "wht_amount": "450.00",
      "certificate_number": "WHT-2026-0101",
      "return_form": "pnd53"
    }
  ],
  "attachments": [],
  "state": "recorded",
  "reversed_at": null,
  "reverse_reason": null,
  "created_at": "2026-10-31T16:55:02+07:00"
}

POST /v1/payments/{id}/reverse

Reverse a supplier payment

Second release (see Two releases): once the first release is live, this path answers 404 not_found until the second opens. Reverses a payment recorded by mistake. The bills it was allocated to owe the allocated amounts again, and its withholding entries no longer count. The payment stays visible with state: reversed. A payment dated in a closed period cannot be reversed (period_closed), nor one whose settlement SLF has already posted to your books (void_not_allowed). Withholding already in a return SLF has filed stays there; SLF handles that with you.

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
idpathstringrequiredThe payment's id (pay_). Example: pay_….
Request body

application/json · VoidRequest

{
  "reason": "Transfer was rejected by the bank."
}
Responses

200 The reversed payment. · Payment

{
  "id": "pay_7Mv4Ys1Jn9Bc",
  "external_id": "AP-PAY-2026-10-0031",
  "pay_date": "2026-10-31",
  "method": "transfer",
  "bank_account_id": "bnk_5Qm2Rk8Lt3Vw",
  "reference": "TRF-20261031-0007",
  "allocations": [
    {
      "bill_id": "bil_5Tg9Hw2Lr6Mx",
      "amount": "15600.00",
      "wht_amount": "450.00"
    }
  ],
  "withholding": [
    {
      "payee_name": "Demo Services Co., Ltd.",
      "payee_tax_id": "0999000000035",
      "payee_branch_no": "00000",
      "payee_address": "1 Example Road, Khlong Toei, Bangkok 10110",
      "payee_type": "company",
      "income_type": "service",
      "income_description": null,
      "gross_amount": "15000.00",
      "rate": "3",
      "wht_amount": "450.00",
      "certificate_number": "WHT-2026-0101",
      "return_form": "pnd53"
    }
  ],
  "attachments": [],
  "state": "reversed",
  "reversed_at": "2026-11-01T10:12:45+07:00",
  "reverse_reason": "Transfer was rejected by the bank.",
  "created_at": "2026-10-31T16:55:02+07:00"
}

409 The payment is already reversed (already_voided), or it cannot be reversed now (void_not_allowed). · Error

{
  "error": {
    "code": "already_voided",
    "message": "This payment is already reversed.",
    "param": null,
    "details": [],
    "request_id": "req_2f7c4a9e1d5b8c36",
    "existing_id": null
  }
}
Errors specific to this endpoint

already_voided, void_not_allowed, period_closed. Common errors: see Error codes.

Objects

Payment Supplier payment

A payment you made to suppliers, allocated to bills, with any tax you withheld.

FieldTypeRequiredDescription
idstring
pay_ id
read-onlySLF's id for the payment. Example: pay_7Mv4Ys1Jn9Bc.
external_idstring
≤ 100 characters
requiredYour system's id for the payment. Unique among your payments, reversed ones included (duplicate_external_id). Required when you create the payment; null on payments SLF entered itself. Example: AP-PAY-2026-10-0031.
pay_datedaterequiredThe date you paid. Example: 2026-10-31.
methodstring · enum payment_methodrequiredHow you paid. Example: transfer.
bank_account_idstringrequired unless method is cashThe account you paid from. Example: bnk_5Qm2Rk8Lt3Vw.
referencestring
≤ 100 characters
optionalThe bank transfer or cheque reference. Example: TRF-20261031-0007.
allocationsarray of PaymentAllocation
1 to 100
requiredThe bills this payment settles.
withholdingarray of WithholdingEntry
up to 50
required when any allocations[].wht_amount is above zeroThe tax you withheld, one entry per payee and income type, as on your 50 ทวิ certificate. The sum of wht_amount must equal the sum over allocations (withholding_mismatch).
attachmentsarray of Attachmentread-onlyFiles attached with POST /attachments.
statestring · enum payment_stateread-onlyWhether the payment stands or was reversed. Example: recorded.
reversed_atdate-timeread-onlyWhen the payment was reversed, or null.
reverse_reasonstringread-onlyThe reason given when reversing, or null.
created_atdate-timeread-onlyWhen the payment was recorded. Example: 2026-10-31T16:55:02+07:00.

PaymentAllocation Payment allocation

The part of a payment that settles one bill.

FieldTypeRequiredDescription
bill_idstringrequiredThe bill. Example: bil_5Tg9Hw2Lr6Mx.
amountdecimal stringrequiredThe cash paid towards this bill, in the bill's currency. Example: 15600.00.
wht_amountdecimal stringoptionalThe tax you withheld from this bill's payment. amount plus wht_amount may not exceed the bill's outstanding. Above zero only for a bill in THB. Default: 0.00. Example: 450.00.

WithholdingEntry Withholding entry

Tax you withheld from one payee for one income type, as on the withholding tax certificate (50 ทวิ). The API does not say which rate applies to which income; SLF reviews the rates.

FieldTypeRequiredDescription
payee_namestring
≤ 300 characters
requiredThe payee's name. Example: Demo Services Co., Ltd..
payee_tax_idstring
13 digits
requiredThe payee's tax ID, with a valid check digit (invalid_tax_id). Example: 0999000000035.
payee_branch_nostring
5 digits
optionalThe payee's branch. 00000 is the head office. Default: 00000. Example: 00000.
payee_addressstring
≤ 1000 characters
optionalThe payee's address, as on the certificate. Example: 1 Example Road, Khlong Toei, Bangkok 10110.
payee_typestring · enum payee_typerequiredIndividual or company. return_form is set from it, and SLF confirms the return on review. Example: company.
income_typestring · enum income_typerequiredThe kind of payment the tax was withheld from. Example: service.
income_descriptionstring
≤ 300 characters
required when income_type is otherWhat the payment was for.
gross_amountdecimal stringrequiredThe amount the tax was computed on, in THB. Example: 15000.00.
ratestring · enum wht_raterequiredThe rate you actually withheld. Example: 3.
wht_amountdecimal stringrequiredThe tax withheld, in THB. Must equal round2(gross_amount × rate / 100) (totals_mismatch). Example: 450.00.
certificate_numberstring
≤ 40 characters
optionalThe number of the 50 ทวิ certificate, if you issued it yourself. Example: WHT-2026-0101.
return_formstring · enum return_formread-onlyThe return SLF expects the tax to be reported on, set from payee_type; SLF confirms it on review (see Tax notes). Example: pnd53.

Attachments

Not live yet (draft specification). PDFs and scans attached to sales documents and receipts, and from the second release to bills and payments. Only files uploaded through the API are listed and returned.

POST /v1/attachments

Upload an attachment

Uploads a file and attaches it to a sales document or receipt (to a bill or payment from the second release; until then reference_not_found). Send multipart/form-data with the fields of AttachmentUpload and one file; form fields the API does not know are ignored, and a second file is refused with validation_failed. The file's type is read from its content, not from its name or the media type you declare. Allowed types: PDF, JPEG, PNG and WebP, up to 10 MB; anything else is refused with 415 unsupported_media_type or 413 payload_too_large. HEIC is not accepted: convert such photos to JPEG first. A voided record takes no new attachments (validation_failed, invalid_value on /object_id). Your company may upload up to 1 GB or 1,000 files per day, Bangkok time; above that, uploads are refused with 429 rate_limited and Retry-After until midnight. If the same file (by SHA-256) is already attached to the same record, the existing attachment is returned with 200 instead of 201.

Parameters
NameInTypeRequiredDescription
Idempotency-Keyheaderstring
≤ 255 characters
requiredRequired on every POST. A unique value for this request, 1 to 255 visible ASCII characters; a UUID works well. Reuse it only to retry this exact request after a network error, a timeout, 429 or a 5xx. After any other 4xx, fix the request and send it with a new value. See Idempotency and duplicates. Example: 5f0c2b8e-3d4a-4c1e-9b7f-2a6d8e1c0f93.
Request body

multipart/form-data · AttachmentUpload

{
  "file": "(binary content of TIV-2026-0412.pdf)",
  "object_type": "sales_document",
  "object_id": "doc_9Rt3Vb6Nz1Hs",
  "kind": "document_copy"
}
Responses

201 The file was stored and attached. · Attachment

{
  "id": "att_2Bh5Rx8Fk3Wq",
  "object_type": "sales_document",
  "object_id": "doc_9Rt3Vb6Nz1Hs",
  "kind": "document_copy",
  "file_name": "TIV-2026-0412.pdf",
  "content_type": "application/pdf",
  "size": 184233,
  "sha256": "9f2b5c1e7a4d8f0c3b6e9a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3a6d9c2f",
  "created_at": "2026-10-08T11:05:37+07:00"
}

200 The same file was already attached to this record; the existing attachment is returned. · Attachment

{
  "id": "att_2Bh5Rx8Fk3Wq",
  "object_type": "sales_document",
  "object_id": "doc_9Rt3Vb6Nz1Hs",
  "kind": "document_copy",
  "file_name": "TIV-2026-0412.pdf",
  "content_type": "application/pdf",
  "size": 184233,
  "sha256": "9f2b5c1e7a4d8f0c3b6e9a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3a6d9c2f",
  "created_at": "2026-10-08T11:05:37+07:00"
}

413 The file is larger than 10 MB. · Error

{
  "error": {
    "code": "payload_too_large",
    "message": "The file is 14.2 MB; the limit is 10 MB.",
    "param": null,
    "details": [],
    "request_id": "req_4e9a2c7f1b5d8e36",
    "existing_id": null
  }
}

415 By its content, the file is not PDF, JPEG, PNG or WebP. · Error

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Files of type application/zip cannot be attached.",
    "param": "/file",
    "details": [],
    "request_id": "req_7b3d1f8a4c6e2b95",
    "existing_id": null
  }
}

422 The record to attach to does not exist or is voided, or a field is invalid. · Error

{
  "error": {
    "code": "reference_not_found",
    "message": "No sales document doc_9Rt3Vb6Nz1Hx exists for your company.",
    "param": "/object_id",
    "details": [],
    "request_id": "req_2a8c5e1d9f4b7a63",
    "existing_id": null
  }
}

429 Your company's daily upload limit is reached. · Error

{
  "error": {
    "code": "rate_limited",
    "message": "Your company has uploaded 1,000 files today, the daily limit. Retry after midnight Bangkok time.",
    "param": null,
    "details": [],
    "request_id": "req_5d8b2f6a9c1e4d73",
    "existing_id": null
  }
}
Errors specific to this endpoint

reference_not_found. Common errors: see Error codes.

GET /v1/attachments

List attachments of a record

Returns the attachments of one record that were uploaded through the API, newest first. Files SLF attached itself are not listed.

Parameters
NameInTypeRequiredDescription
object_typequerystring · enum attachment_object_typerequiredThe kind of record. Example: sales_document.
object_idquerystringrequiredThe record's id. Example: doc_9Rt3Vb6Nz1Hs.
limitqueryintegeroptionalNumber of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25.
cursorquerystring
≤ 512 characters
optionalThe next_cursor value from the previous page, unchanged. Omit it for the first page; an empty value is refused. For the refusals, see Pagination in Conventions. Example: cur_8Tn3XqL5Vd.
Responses

200 A page of attachments. · list of Attachment

{
  "data": [
    {
      "id": "att_2Bh5Rx8Fk3Wq",
      "object_type": "sales_document",
      "object_id": "doc_9Rt3Vb6Nz1Hs",
      "kind": "document_copy",
      "file_name": "TIV-2026-0412.pdf",
      "content_type": "application/pdf",
      "size": 184233,
      "sha256": "9f2b5c1e7a4d8f0c3b6e9a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3a6d9c2f",
      "created_at": "2026-10-08T11:05:37+07:00"
    }
  ],
  "next_cursor": null
}

GET /v1/attachments/{id}

Get attachment details

Returns an attachment's details, without the file.

Parameters
NameInTypeRequiredDescription
idpathstringrequiredThe attachment's id (att_). Example: att_….
Responses

200 The attachment's details. · Attachment

{
  "id": "att_2Bh5Rx8Fk3Wq",
  "object_type": "sales_document",
  "object_id": "doc_9Rt3Vb6Nz1Hs",
  "kind": "document_copy",
  "file_name": "TIV-2026-0412.pdf",
  "content_type": "application/pdf",
  "size": 184233,
  "sha256": "9f2b5c1e7a4d8f0c3b6e9a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3a6d9c2f",
  "created_at": "2026-10-08T11:05:37+07:00"
}

GET /v1/attachments/{id}/content

Download an attachment

Returns the file itself.

Parameters
NameInTypeRequiredDescription
idpathstringrequiredThe attachment's id (att_). Example: att_….
Responses

200 The file, with its content_type as the Content-Type header and its file_name in a Content-Disposition header. · the file itself (application/octet-stream)

Objects

Attachment Attachment

A file attached through the API to a sales document or receipt, or, from the second release, to a bill or payment.

FieldTypeRequiredDescription
idstring
att_ id
read-onlySLF's id for the attachment. Example: att_2Bh5Rx8Fk3Wq.
object_typestring · enum attachment_object_typeread-onlyThe kind of record it is attached to. Example: sales_document.
object_idstringread-onlyThe record's id. Example: doc_9Rt3Vb6Nz1Hs.
kindstring · enum attachment_kindread-onlyWhat the file is. Example: document_copy.
file_namestringread-onlyThe file name you uploaded. Example: TIV-2026-0412.pdf.
content_typestringread-onlyThe file's media type, as recognised from its content. Example: application/pdf.
sizeintegerread-onlySize in bytes. Example: 184233.
sha256string
64 hex characters
read-onlySHA-256 of the file. The same file on the same record is stored once.
created_atdate-timeread-onlyWhen the file was uploaded. Example: 2026-10-08T11:05:37+07:00.

AttachmentUpload Attachment upload

The multipart/form-data fields of POST /attachments. Other form fields are ignored.

FieldTypeRequiredDescription
filestring
binary; PDF, JPEG, PNG or WebP; ≤ 10 MB
requiredThe file, with its file name. One file per request. Its type is read from its content; HEIC is not accepted.
object_typestring · enum attachment_object_typerequiredThe kind of record to attach it to. Example: sales_document.
object_idstringrequiredThe record's id (reference_not_found if it does not exist). A voided record takes no new attachments (validation_failed, invalid_value). Example: doc_9Rt3Vb6Nz1Hs.
kindstring · enum attachment_kindrequiredWhat the file is. Example: document_copy.

Periods

Not live yet (draft specification). A month's figures as SLF has recorded them, for reconciliation with your own system.

GET /v1/periods/{month}

Get a month's totals

Returns the month's figures as SLF has recorded them, so you can compare them with your own system's totals. If they differ, contact SLF. All amounts are in THB. status shows whether SLF has filed the month's VAT return or closed the period. Sales lines SLF has not classified yet are counted in sales.unclassified_value, and sales records held back by SLF's arithmetic check in quarantined; neither is included in the other figures. For a company that is not VAT-registered, every VAT figure is zero. A month with no records returns zero totals.

Parameters
NameInTypeRequiredDescription
monthpathstringrequiredThe month, as YYYY-MM. Example: 2026-09.
Responses

200 The month's figures. · Period

{
  "month": "2026-09",
  "status": "vat_filed",
  "filings": [
    {
      "form": "pp30",
      "filed": true
    },
    {
      "form": "pnd3",
      "filed": true
    },
    {
      "form": "pnd53",
      "filed": true
    },
    {
      "form": "pnd1",
      "filed": false
    }
  ],
  "sales": {
    "document_count": 64,
    "taxable_value": "412350.00",
    "zero_rated_value": "38900.00",
    "exempt_value": "0.00",
    "unclassified_value": "0.00",
    "vat": "28864.50"
  },
  "purchases": {
    "document_count": 41,
    "taxable_value": "158200.00",
    "vat": "11074.00"
  },
  "out_of_scope_value": "12750.00",
  "quarantined": {
    "count": 0,
    "value": "0.00"
  },
  "withholding": {
    "pnd3_amount": "1250.00",
    "pnd53_amount": "4830.00"
  }
}

Objects

Period Period

A month's figures as SLF has recorded them. All amounts are in THB. For a company that is not VAT-registered, every VAT figure (sales, purchases, out_of_scope_value, quarantined) is zero.

FieldTypeRequiredDescription
monthstring
YYYY-MM
read-onlyThe month. Example: 2026-09.
statusstring · enum period_statusread-onlyWhether the month is open, VAT-filed or closed. Example: vat_filed.
filingsarray of PeriodFilingread-onlyWhich of the month's returns SLF has filed.
salesPeriodSalesread-onlySales recorded for VAT in the month.
purchasesPeriodPurchasesread-onlyPurchases recorded for VAT in the month.
out_of_scope_valuedecimal stringread-onlyDisbursements recovered from customers in the month: disbursement lines billed to the customer. Not included in sales. Example: 12750.00.
quarantinedPeriodQuarantinedread-onlySales records of the month held back by SLF's arithmetic check until SLF resolves them. Not included in sales.
withholdingPeriodWithholdingread-onlyTax you withheld from supplier payments in the month.

PeriodFiling Period filing

One return for the month and whether SLF has filed it.

FieldTypeRequiredDescription
formstring · enum filing_formread-onlyThe return. Example: pp30.
filedbooleanread-onlyWhether SLF has filed it. Example: true.

PeriodSales Period sales

The month's sales as recorded for VAT: tax invoices, cash sales and debit notes by issue_date; receipts against invoice documents whose lines carry VAT, by payment_date (the service rule; see Tax notes); less credit notes by their issue_date. Voided records are left out, and so are records held back by SLF's arithmetic check (see quarantined). Records SLF entered itself are included.

FieldTypeRequiredDescription
document_countintegerread-onlyNumber of documents and receipts counted. Example: 64.
taxable_valuedecimal stringread-onlyValue before VAT of standard and standard_incl lines. Example: 412350.00.
zero_rated_valuedecimal stringread-onlyValue of zero lines. Example: 38900.00.
exempt_valuedecimal stringread-onlyValue of exempt lines. Example: 0.00.
unclassified_valuedecimal stringread-onlyValue of lines SLF has not classified yet: none lines that are not a disbursement billed to the customer. Not included in the other figures until SLF classifies them. Example: 0.00.
vatdecimal stringread-onlyOutput VAT. Example: 28864.50.

PeriodPurchases Period purchases

The month's purchases as recorded for VAT: the bills whose input VAT SLF claims in this month's return. SLF decides the month, and bills SLF entered itself are included, so it can differ from your bill dates.

FieldTypeRequiredDescription
document_countintegerread-onlyNumber of documents counted. Example: 41.
taxable_valuedecimal stringread-onlyValue before VAT. Example: 158200.00.
vatdecimal stringread-onlyInput VAT. Example: 11074.00.

PeriodQuarantined Period quarantined

Sales records of the month, documents and receipts alike and including those SLF entered itself, whose VAT did not pass SLF's arithmetic check (on sales documents you send, the warning vat_arithmetic_review; a receipt carries no warning for it). They are held back from sales until SLF resolves them; see How the API checks totals.

FieldTypeRequiredDescription
countintegerread-onlyHow many are held back. Example: 0.
valuedecimal stringread-onlyTheir value before VAT. Example: 0.00.

PeriodWithholding Period withholding

Tax you withheld in the month, by return, as SLF has recorded it: withholding SLF entered, and from the second release the withholding entries of payments recorded through the API, by pay_date. Reversed payments are left out.

FieldTypeRequiredDescription
pnd3_amountdecimal stringread-onlyEntries with return_form pnd3, for the ภ.ง.ด.3 withholding return. Example: 1250.00.
pnd53_amountdecimal stringread-onlyEntries with return_form pnd53, for the ภ.ง.ด.53 withholding return. Example: 4830.00.

Shared objects

Error Error

The body of every error response.

FieldTypeRequiredDescription
errorErrorObjectread-onlyWhat went wrong.

ErrorObject Error object

Details of an error. See Errors and retries.

FieldTypeRequiredDescription
codestringread-onlyThe error code, one of those listed under Error codes. Stable; branch on this. Example: validation_failed.
messagestringread-onlyA short explanation for people. It may change; do not parse it.
paramstring
JSON Pointer
read-onlyThe field concerned, as a JSON Pointer into your request body, or null. A query parameter is written as / followed by its name, such as /limit. Example: /lines/2/vat_category.
detailsarray of ErrorDetailread-onlyFor validation_failed, mismatch errors and vat_category_unexplained: one entry per problem. Empty otherwise.
request_idstring
≤ 64 characters
read-onlyThe same value as the Request-Id header. Quote it when you contact SLF. Example: req_3c9e1f7a2b5d8c04.
existing_idstringread-onlyFor a 409 duplicate refusal (duplicate_external_id, duplicate_number, customer_exists, duplicate_bill), the id of the record already there, when it is one the API returns. null for a duplicate_number whose number belongs to a pro-forma document SLF recorded, for recorded_by_slf and recorded_by_slf_posted, and for every other error. Example: cus_7Hc4Nf2Pq9Xz.

ErrorDetail Error detail

One problem found in a request.

FieldTypeRequiredDescription
paramstring
JSON Pointer
read-onlyThe field concerned, as a JSON Pointer into your request body. A query parameter is written as / followed by its name, such as /limit or /cursor. Example: /tax_id.
codestring · enum error_detail_coderead-onlyWhat is wrong with it. Example: invalid_tax_id.
messagestringread-onlyA short explanation for people.
expecteddecimal stringread-onlyMismatch details only: the figure the API computed. null otherwise. Example: 1326.66.
receiveddecimal stringread-onlyMismatch details only: the figure you sent. null otherwise. Example: 1431.66.

Warning Warning

A note that a record was accepted but flagged for SLF to review. Warnings never block a record.

FieldTypeRequiredDescription
codestringread-onlyThe warning code, one of those listed under Warning codes. Example: zero_rated_review.
messagestringread-onlyA short explanation for people.
paramstring
JSON Pointer
read-onlyThe field concerned, or null. Example: /lines/0/vat_category.

Enumerations

sales_document_type

The kind of sales document.

ValueMeaning
quotationQuotation (ใบเสนอราคา). Creates nothing owed and cannot be receipted.
invoiceAn invoice that is not a tax invoice (ใบแจ้งหนี้). Paid with receipts.
tax_invoiceTax invoice (ใบกำกับภาษี). Paid with receipts.
tax_invoice_receiptA tax invoice that is also the receipt, for a cash sale. Recorded as paid; it cannot be receipted.
credit_noteCredit note (ใบลดหนี้): a decrease in value after a tax invoice. Needs the note fields; see Corrections.
debit_noteDebit note (ใบเพิ่มหนี้): an increase in value after a tax invoice. Needs the note fields; see Corrections. Paid with receipts.

sales_document_status

Status of a sales document. Quotations use open, expired, rejected and void.

ValueMeaning
openRecorded, and no receipt recorded against it yet. For a quotation: still valid. For a credit note: recorded.
partially_paidReceipts are recorded against it, and something is still outstanding.
paidNothing is outstanding: receipts, together with any credit notes, cover the total. A tax_invoice_receipt, and a tax_invoice with related_invoice_id, are paid from the start.
expiredQuotations only: valid_until has passed.
rejectedQuotations only: recorded at SLF as rejected by the customer. The API does not set it.
voidVoided. The document keeps its number.

vat_category

How VAT applies to a sales line. The VAT rate is never sent; it follows from the category. Not tax advice; see Tax notes.

ValueMeaning
standardCurrent standard rate: 7% including local tax (the Revenue Code s.80 rate is 10%, reduced by royal decree). The price excludes VAT.
standard_inclSame rate as standard; the price includes VAT.
zero0%: only where s.80/1 applies, for example qualifying exports. SLF reviews every zero line (warning zero_rated_review).
exemptExempt under s.81.
noneNot subject to VAT on this document: disbursements recovered, or the company is not VAT-registered. On a VAT-registered company's document, a none line that is not a disbursement billed to the customer is counted as unclassified until SLF classifies it, and a document with no other kind of line is refused (vat_category_unexplained).

wht_rate

Withholding tax rate as a percentage. On sales lines: the rate your customer is expected to withhold. On supplier payments: the rate you actually withheld. The API does not say which rate applies to which income; SLF reviews the rates.

ValueMeaning
0No tax withheld.
11%.
22%.
33%.
55%.
1010%.
1515%.

charge_role

Whether a sales line is your own supply or the recovery of an amount you paid for the customer. Optional, with no default: a line sent without it is recorded as not stated and returned with charge_role: null.

ValueMeaning
own_supplyYour own sale of goods or services.
disbursementRecovering an amount you paid on your customer's behalf (เงินทดรองจ่าย). In ruling กค 0811/12237 the Revenue Department answered one company that recovering costs it had paid on another company's behalf was not a sale or service under s.77/2, so it could not issue a tax invoice for it; a ruling answers the facts put to it. Needs supplier_invoice_billed_to, pass_through_cost and vat_category: none. Recorded as printed, never split; a line priced above its pass_through_cost is refused (disbursement_marked_up), except for a company that is not VAT-registered, or on a document not in THB, where it is flagged with the warning of the same name. Whether a charge is a disbursement, and how such lines are shown on your documents: confirm with SLF. Not tax advice; see Tax notes.

supplier_invoice_billed_to

For a disbursement line: who the supplier's document for the cost is addressed to.

ValueMeaning
customerAddressed to your customer.
selfAddressed to your company. The line is recorded and flagged disbursement_billed_to_self for SLF review.

payment_terms

Trade payment terms printed on a sales document.

ValueMeaning
ttTelegraphic (bank) transfer.
lcLetter of credit.
sblcStandby letter of credit.
bgBank guarantee.

credit_note_reason

Reason for a credit note (ใบลดหนี้), following s.82/10 of the Revenue Code, DG VAT Notice No. 82 and Order ป.80/2542 clause 2(3). There is no other value. Not tax advice; see Tax notes.

ValueMeaning
goods_nonconformingPrice reduced because goods did not meet the agreed specification, were damaged or short.
goods_returnedGoods returned because defective, not matching sample or description.
goods_return_exchangeGoods returned or exchanged under a trade agreement: between VAT-registered businesses, or between you and a buyer if done within a reasonable time.
service_deficientService price reduced because the service was deficient, short or not as agreed.
price_overchargedPrice calculated higher than it should have been.
service_contract_cancelled_deficientService contract cancelled because the service was deficient or not as agreed.
service_contract_cancelled_not_providedService contract cancelled because the service was not provided.
deposit_refundRefund of an advance payment, deposit, guarantee or booking money under a trade agreement.
compensationCompensation or similar the seller must pay the buyer by law.

debit_note_reason

Reason for a debit note (ใบเพิ่มหนี้), following s.82/9 of the Revenue Code. Not tax advice; see Tax notes.

ValueMeaning
goods_quantity_exceededMore goods delivered than agreed.
goods_underchargedGoods price calculated lower than it should have been.
service_exceededService provided beyond what was agreed.
service_underchargedService price calculated lower than it should have been.
other_announcedAnother cause set by the Director-General. reason_text must name it; SLF reviews.

bill_document_type

What the supplier gave you. SLF uses it when booking the bill, including input VAT, and reviews it.

ValueMeaning
tax_invoiceA tax invoice (ใบกำกับภาษี) issued to you.
receiptA receipt that is not a tax invoice.
invoiceAn invoice or bill that is not a tax invoice.
import_entryAn import entry (customs declaration) for goods you imported.
foreign_invoiceAn invoice from a supplier outside Thailand.
otherAny other document. Describe it in remark.

bill_status

Payment status of a supplier bill.

ValueMeaning
openNothing paid yet.
partially_paidPayments or supplier credit notes cover part of it, and something is still outstanding.
paidNothing is outstanding.
voidVoided.

bill_note_type

The kind of supplier adjustment note.

ValueMeaning
credit_noteThe supplier reduced the amount.
debit_noteThe supplier increased the amount.

payment_method

How a supplier payment was made.

ValueMeaning
transferBank transfer. Needs bank_account_id.
chequeCheque. Needs bank_account_id.
cashCash.

payment_state

Whether a supplier payment stands.

ValueMeaning
recordedThe payment is recorded.
reversedThe payment was reversed; its allocations and withholding no longer count.

payee_type

Who tax was withheld from. return_form is set from it. Not tax advice; see Tax notes.

ValueMeaning
individualAn individual. return_form is set to pnd3 (ภ.ง.ด.3); SLF confirms the return on review (see Tax notes).
companyA company or juristic partnership. return_form is set to pnd53 (ภ.ง.ด.53); SLF confirms the return on review (see Tax notes).

income_type

The kind of payment tax was withheld from, as on your 50 ทวิ certificate. The API does not link income types to rates.

ValueMeaning
serviceService fees.
professional_feeProfessional fees.
rentRent.
transportTransport charges.
advertisingAdvertising fees.
commissionCommissions.
interestInterest.
otherAnything else. Describe it in income_description.

return_form

The withholding tax return SLF expects the entry to go on, set from payee_type. Read-only. SLF confirms it on review; some withholding belongs on another return. Not tax advice; see Tax notes.

ValueMeaning
pnd3ภ.ง.ด.3.
pnd53ภ.ง.ด.53.

attachment_object_type

The kind of record a file is attached to.

ValueMeaning
sales_documentA sales document (doc_).
receiptA receipt (rcp_).
billA supplier bill (bil_). From the second release; until then refused with reference_not_found.
paymentA supplier payment (pay_). From the second release; until then refused with reference_not_found.

attachment_kind

What an attached file is.

ValueMeaning
document_copyA copy of a document you issued.
supplier_documentA document you received from a supplier.
payment_proofProof of payment, such as a transfer slip.
wht_certificate_receivedA withholding tax certificate (50 ทวิ) your customer gave you.
wht_certificate_issuedA withholding tax certificate (50 ทวิ) you issued.
customs_documentA customs document, such as an import or export entry.
otherAnything else.

bank_account_type

Whose name a bank account is held in, where SLF has recorded it. Any other account has type null.

ValueMeaning
personalA person's name.

period_status

Where a month stands at SLF.

ValueMeaning
openSLF has not filed the month's VAT return or closed the period.
vat_filedSLF has filed the month's VAT return. Records that would change it are refused with period_filed; see Filed periods.
closedSLF has closed the accounting period. Records dated in it are refused with period_closed; see Filed periods.

filing_form

A monthly return shown in GET /periods/{month}.

ValueMeaning
pp30ภ.พ.30, the VAT return.
pnd3ภ.ง.ด.3, a withholding tax return.
pnd53ภ.ง.ด.53, a withholding tax return.
pnd1ภ.ง.ด.1. Shown for completeness; the API does not record payroll.

error_detail_code

What is wrong with one field, in error.details[].

ValueMeaning
requiredA required field is missing or empty.
invalid_formatThe value has the wrong format, such as a date that is not YYYY-MM-DD or money without two decimals.
invalid_valueThe value is not allowed here, such as an unknown enum value or a voided original.
out_of_rangeThe value is too small or too large, such as a quantity of zero.
too_longThe text is longer than the limit.
invalid_tax_idA Thai tax ID does not have 13 digits or its check digit is wrong.
mismatchA figure differs from the one the API computed; see expected and received.

Error codes

CodeHTTPMeaningWhat to do
invalid_json400The body is not valid JSON or not UTF-8, its top level is not a JSON object, or it is nested more than 64 levels deep.Fix the body and resend with a new Idempotency-Key.
idempotency_key_missing400A POST request has no Idempotency-Key header, or its value is longer than 255 characters or has characters other than visible ASCII.Add a new key of 1 to 255 visible ASCII characters and resend.
unauthenticated401The Authorization header is missing or malformed, or the key is unknown, revoked, suspended by SLF, replaced by a new key and past its 7-day overlap period, or for the other environment.Stop. Check which key you send; contact SLF if it should work.
forbidden403The request is not allowed: the key is valid but may not perform this operation or reach this record (for example, a write with a key limited to reading, or a request from an IP address outside the key's allowed addresses); or, for a write, your company profile's VAT setting (vat_registered in GET /company) and SLF's own record of your VAT registration are not both set and in agreement (reads keep working); or the request was sent over plain http://.Do not retry unchanged; contact SLF. If a write was refused because of the VAT setting, keep the refused records and send them again once SLF tells you the two agree; a 403 is not stored, so the same Idempotency-Key works. For a request sent over http://, use https:// and ask SLF to revoke the key, which crossed the network unencrypted.
not_found404The path does not exist, or the id in the path does not exist for your company. Once the first release is live, the paths of supplier bills and payments answer it too, until the second release opens.Check the id and path. Do not retry unchanged.
method_not_allowed405The HTTP method is not supported on this path.Use a method listed in this reference.
idempotency_key_reused409This Idempotency-Key was used in the last 24 hours with a different body.Use a new key for a new request. Reuse a key only to retry the identical request.
idempotency_key_in_use409A request with this Idempotency-Key is still being processed.Wait a few seconds, then retry with the same key.
payload_too_large413The JSON body is larger than 1 MB, or the uploaded file larger than 10 MB.Reduce the size. Do not retry unchanged.
unsupported_media_type415The Content-Type is not supported for this operation, or the uploaded file is not PDF, JPEG, PNG or WebP, judged by its content. HEIC is not accepted.Send JSON as application/json and uploads as multipart/form-data with an allowed file type.
validation_failed422One or more fields or query parameters are missing or invalid. details lists each problem with its param and a detail code.Fix what details lists and resend; for a POST, use a new Idempotency-Key.
rate_limited429More than 60 requests per minute with this key (draft value), or your company has reached its daily upload limit (1 GB or 1,000 files per day, Bangkok time).Wait the number of seconds in Retry-After, then retry with the same Idempotency-Key.
internal_error500An unexpected error at SLF.Retry with the same Idempotency-Key, with exponential backoff. If it persists, contact SLF with the request_id.
service_unavailable503The API is unavailable. Until launch, every request under /v1 on either hostname gets this error. After launch it means a temporary outage: for example a short restart during the nightly maintenance window (20:00 to 05:59 Bangkok time), or a moment when SLF's system is busy.Before launch, retrying does not help. After launch, retry with the same Idempotency-Key, with exponential backoff; wait for Retry-After if present.
duplicate_external_id409A record of this type with the same external_id already exists. existing_id holds its id.Do not resend. Fetch the record in existing_id; if it is yours, store its id and carry on.
duplicate_number409A sales document that is not voided (for a quotation: a quotation), or a receipt that is not voided, already has this number. existing_id holds its id, or null when the number belongs to a pro-forma document SLF recorded, which the API does not return.Check whether it is the same document sent before. If not, fix the numbering in your system. If existing_id is null, contact SLF. Do not retry unchanged.
recorded_by_slf409SLF has already entered this tax_invoice or tax_invoice_receipt by hand: an output VAT entry with the same number and the same total in THB is in your books at SLF, in any month. existing_id is null, because SLF's entry is not a record the API returns.Do not resend. Contact SLF; it decides with you which record stands.
recorded_by_slf_posted409The receipt books VAT that SLF had already entered by hand for the document being paid (an entry with its number and total), in a month whose VAT return SLF has not filed, and SLF has posted that entry to your books, so the receipt cannot take its place. In a filed month the receipt is recorded with the warning recorded_by_slf_filed instead. existing_id is null.Do not retry; contact SLF.
customer_exists409A customer with the same tax_id and branch_no already exists. existing_id holds its id.Use the existing customer: store its id with your customer record.
duplicate_bill409Second release. A bill with the same supplier tax_id (or, without one, the same supplier name) and bill_number already exists. existing_id holds its id.Do not resend. Use the bill in existing_id.
already_voided409The record is already voided, or the payment already reversed.Nothing to do. Fetch the record to see its state.
void_not_allowed409The record cannot be voided or reversed now: receipts, payments or adjustment notes are recorded against it or depend on it, a tax invoice with related_invoice_id stands on the invoice it pays, SLF has already posted it to your books, or SLF entered it itself.If receipts are recorded against it, void them first (or reverse the payments). Otherwise do not retry; contact SLF.
reference_not_found422An id in the body, such as customer_id, document_id, bill_id, bank_account_id or object_id, does not exist for your company. Until the second release opens, object_type bill or payment on POST /attachments gets it too.Create or look up the referenced record first, then resend with a new Idempotency-Key.
totals_mismatch422Figures do not add up: a sales document's totals differ from its lines by more than the tolerance (0.01 per line; for vat_total, 0.01 per line that carries VAT), a bill's total is not subtotal plus vat_amount, or a withholding entry's wht_amount is not round2(gross_amount × rate / 100). details shows expected and received values.Compare with your system's figures, fix them and resend with a new Idempotency-Key.
note_difference_mismatch422The difference between original_value and correct_value does not equal the note's subtotal.Fix the values and resend with a new Idempotency-Key.
note_original_not_tax_invoice422A credit or debit note refers to an original that is neither a tax_invoice or tax_invoice_receipt nor a receipt recorded against an invoice whose lines carry VAT.Refer to the original tax invoice. For an invoice with VAT lines that has been paid, refer to the receipt that recorded the payment; otherwise void the invoice and record a new one.
note_exceeds_original422A credit note is larger than the value still remaining on its originals (with the debit notes recorded against them) after earlier credit notes.Check the credit notes already recorded against the originals; do not retry unchanged.
note_cannot_be_voided422Credit and debit notes cannot be voided through the API.Do not retry; contact SLF.
disbursement_vat_category422A line with charge_role: disbursement has a vat_category other than none.Set the line's vat_category to none, or send it as own_supply if it is your own charge; resend with a new Idempotency-Key.
disbursement_marked_up422A disbursement line's amount is above its pass_through_cost, on a document of a VAT-registered company in THB. (For a company that is not VAT-registered, or a document in another currency, the line is recorded with the warning of the same name instead.)Do not resend unchanged. Have your system show such a charge as two lines: the cost as the disbursement line, and the difference as a separate own_supply line. For a document already issued as one line, contact SLF.
vat_category_unexplained422Your company is VAT-registered, and no line of the document carries VAT or says why it does not: every line is none, and none is a disbursement billed to the customer. details has one entry for each such line, with param pointing to its vat_category (such as /lines/0/vat_category) and the detail code invalid_value.Send each line's vat_category as printed (zero or exempt where that applies), or mark a recovered cost as a disbursement billed to the customer; resend with a new Idempotency-Key. Ask SLF if you are unsure.
tax_invoice_duplicates_receipt422A tax_invoice or tax_invoice_receipt without related_invoice_id has a total equal to the gross_amount of a receipt that is not voided, recorded for the same customer against an invoice with VAT lines, whose payment_date is in the same month as this document's issue_date; that receipt has already booked the VAT. Tax invoices issued for each part payment are not supported in the first release.If it is the tax invoice for an invoice now paid in full, send it with related_invoice_id. Otherwise do not retry; contact SLF.
period_filed422The record's date falls in a month whose return SLF has already filed for your company: the VAT return, or for a supplier payment with withholding, the withholding return of its return_form. This is SLF's control to protect filed returns, not a legal rule.Do not retry; contact SLF.
period_closed422The record's date falls in an accounting period SLF has closed.Do not retry; contact SLF.
foreign_currency_tax_invoice_not_allowed422A tax_invoice, tax_invoice_receipt, credit_note or debit_note in a currency other than THB, and SLF does not hold the Director-General's approval for your company.Send it in THB, or contact SLF about the approval.
fx_basis_unavailable422A tax_invoice or tax_invoice_receipt in a currency other than THB, for a company whose foreign-currency documents SLF converts to THB for VAT at a rate of its own. In the first release SLF does not issue keys to such companies. See Foreign currency in Conventions.Do not retry; contact SLF.
fx_rate_required422currency is not THB and fx_rate or fx_date is missing.Add the exchange rate and its date; resend with a new Idempotency-Key.
receipt_not_allowed422The document cannot be receipted: it is a quotation, a tax_invoice_receipt (its own receipt), a credit note, or voided.Do not retry. Record receipts only against an invoice, tax_invoice or debit_note.
receipt_exceeds_outstanding422amount_received plus wht_amount is more than the document's outstanding.Check the receipts already recorded against the document; fix the amounts and resend with a new Idempotency-Key.
wht_exceeds_expected422wht_amount is more than the withholding still expected from the document's lines (their wht_rate).Check the amount withheld and the lines' wht_rate. Contact SLF if the document's rates were wrong.
withholding_mismatch422Second release. The sum of allocations[].wht_amount does not equal the sum of withholding[].wht_amount.Make both add up to the tax you withheld; resend with a new Idempotency-Key.
immutable_field422A PATCH tried to change tax_id or branch_no after they were set.Leave them out of the request. Contact SLF if a tax ID was recorded wrongly.
account_code_unknown422account_code is not an active account on your company's chart of accounts at SLF that can be used on this line (income accounts on sales lines).Leave account_code out and let SLF map item_code, or use a code SLF gave you.

Warning codes

Warnings never block a request: the record is saved and SLF reviews it.

CodeMeaning
disbursement_marked_upA disbursement line's amount is above its pass_through_cost, so it is more than you paid on your customer's behalf, on a document of a company that is not VAT-registered or in a currency other than THB. The line is recorded as sent, never split; SLF reviews it. If the difference is your own charge, show it as a separate own_supply line. For a VAT-registered company in THB, such a line is refused with the error of the same name.
disbursement_billed_to_selfA disbursement line has supplier_invoice_billed_to: self: the supplier's document is addressed to you, not to your customer. The line is recorded as sent; SLF reviews how it should be treated.
vat_category_ignoredYour company is not VAT-registered, so a line's vat_category was recorded as none.
zero_rated_reviewA line uses vat_category: zero. SLF checks that the conditions for the 0% rate are met.
vat_arithmetic_reviewThe VAT a sales document books did not pass SLF's arithmetic check, for example because it differs from 7% of the value it is charged on by more than 0.05. The document is recorded, but its VAT is held back from the month's figures (quarantined in GET /periods/{month}) until SLF resolves it. The VAT a receipt books is checked the same way, but a receipt does not carry this warning. See How the API checks totals.
wht_rate_reviewThe tax withheld on a receipt does not match the wht_rate on the document's lines. The receipt is recorded; SLF reviews it.
short_paymentA receipt's gross_amount is less than the document's outstanding before it; the message gives the difference. The receipt is recorded, your remark is kept as sent, and the rest stays outstanding (partially_paid). If the rest is still to be paid, nothing needs to change; otherwise SLF settles the difference with you.
recorded_by_slf_filedA receipt books VAT for a document whose VAT SLF had already entered by hand, in a month whose VAT return SLF has filed, so the receipt did not take that entry's place. The receipt is recorded; SLF reviews it.
possible_double_vatA receipt books VAT for a customer who already has a tax_invoice that is not voided, of the same amount, without related_invoice_id, dated in the month of the receipt's payment_date, so the same VAT may be counted twice. The receipt is recorded, because the money arrived; SLF reviews it.

Changelog

0.9.2-draft · 2026-11-04

  • Two releases: the first covers company, bank accounts, customers, sales documents (credit and debit notes, replaces_id and voids included), receipts (voids included), attachments and periods; supplier bills, supplier notes and supplier payments come in the second. Once the first release is live, the paths of the second answer 404 not_found until it opens, and POST /attachments refuses object_type bill or payment with reference_not_found. See Two releases.
  • A disbursement line above its pass_through_cost is now refused with 422 disbursement_marked_up; it stays a warning for companies that are not VAT-registered and for documents not in THB.
  • Within the tolerance, the vat_total you print is recorded and any difference goes on the last line that carries VAT; the tolerance for vat_total is now 0.01 per line that carries VAT. The recorded subtotal, wht_total and total follow the API's line figures. A receipt's vat_amount is now its share of the recorded vat_total. Rounding is unchanged: halves round up.
  • charge_role has no default any more: left out, it is recorded as not stated and returned as null.
  • GET /periods/{month} adds sales.unclassified_value and quarantined (count, value); every VAT figure is zero for a company that is not VAT-registered.
  • Attachments: HEIC is no longer accepted, the type is read from the file's content, voided records take no new files, uploads are limited per company per day (429 rate_limited), unknown form fields are ignored, and only files uploaded through the API are listed.
  • A receipt that books VAT takes the place of SLF's hand entry of the same document's VAT in a month not yet filed, and voiding the receipt restores that entry. New 409 recorded_by_slf_posted on POST /receipts when SLF has already posted that entry; in a filed month the receipt is recorded with the warning recorded_by_slf_filed.
  • Documents and receipts dated before your start date are now refused; SLF loads your unpaid invoices from before it; a tax invoice or cash sale SLF already entered by hand is refused with the new 409 recorded_by_slf.
  • Lists and GET: pro-forma documents SLF recorded, and receipts SLF recorded against a quotation, are no longer returned (GET answers 404); credit and debit notes SLF entered are listed, read-only. A number that clashes with a pro-forma document returns duplicate_number with existing_id: null; quotations can have the new status rejected.
  • Idempotency: a replay returns the first Request-Id and the header Idempotent-Replayed: true; idempotency_key_reused and refusals made before the body is read (404 and 405 for an unknown path or method, 413, and 415 for an unsupported Content-Type) are not stored; a key must be 1 to 255 visible ASCII characters, or the request gets idempotency_key_missing; a body that is not valid JSON, or is nested more than 64 levels deep, is compared byte for byte; invalid_json also covers a body whose top level is not a JSON object.
  • Keys: the old key works for 7 days after a rotation and then gets 401; one switch-over runs at a time; a key can be limited to reading or to IP addresses (403 outside those limits); SLF keeps a hash of the key and its last four characters. Request-Id is an opaque string of up to 64 characters. Short bursts across all companies may get 503 service_unavailable with Retry-After.
  • Requests to /v1 over plain http:// are refused with 403 forbidden instead of being redirected.
  • Writes are refused with 403 forbidden unless your company profile's VAT setting and SLF's own record of your VAT registration are both set and agree (send the refused records again once they agree), and a tax invoice or cash sale dated before your VAT registration date is refused. A key SLF has suspended gets 401 unauthenticated.
  • Sandbox: it starts with your company's details, bank accounts, account codes and currency settings; a reset now means a new test company with new ids, SLF may rebuild the whole sandbox, and sandbox data is not backed up. The date rules apply there too.
  • A tax invoice or cash sale dated after today, and a receipt with a payment_date after today, are now refused.
  • New 422 tax_invoice_duplicates_receipt: tax invoices issued for each part payment are not supported in the first release.
  • New 422 vat_category_unexplained for a VAT-registered company's document with no line that carries VAT or says why not.
  • New 422 fx_basis_unavailable, and a note that SLF's THB figures can differ from your fx_rate for companies whose VAT SLF converts at its own rate.
  • New warnings: short_payment, vat_arithmetic_review, recorded_by_slf_filed and possible_double_vat. If your company is VAT-registered, flagged records that book VAT are listed for SLF when it prepares that month's VAT return. A receipt whose VAT fails SLF's arithmetic check is held back without a warning on the receipt; quarantined shows it.
  • Error object: existing_id is null for recorded_by_slf, recorded_by_slf_posted and a duplicate_number against a pro-forma document; details is also used by vat_category_unexplained, one entry per line.
  • Nightly maintenance window, 20:00 to 05:59 Bangkok time: requests may briefly get 503 service_unavailable with Retry-After.
  • OpenAPI document: response headers (Request-Id, Idempotent-Replayed, Retry-After), the Idempotency-Key length and character limits, the bounds and default of limit, and x-release on each operation (1 or 2).
  • Company, bank accounts, paging and keys: bank accounts can be added, edited and deleted by SLF or in your client portal, so read the list again before you use a bnk_ id; BankAccount.type is personal or null (corporate is no longer listed); a value SLF holds that does not fit a field's format is returned as null (a currency code is first put in upper case, and a 6-digit branch number starting with 0 given in 5 digits); vat_registered is your company profile's VAT setting, which can also be changed in the client portal, and is null when the profile has none; Company.name, name_local, branch_no and bank_name now say what SLF has on record, and a key is bound to a company record, not to a fixed tax ID and branch number; in details, a query parameter is /limit or /cursor; the refusals of limit and cursor are listed, and a cursor longer than 512 characters is refused; a live key is issued on or after the start date; Getting keys lists what must be in place before SLF issues a key, including that in the first release no key is issued to a company SLF holds as several records under one tax ID, and the limits on how many keys a company can have.

0.9.1-draft · 2026-11-04

  • Both hostnames are now set up. Until launch, every request under /v1 gets 503 service_unavailable; each hostname's root redirects to this guide.

0.9.0-draft · 2026-11-03

  • First public draft.