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.
| Release | What it covers |
|---|
| First | Your 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}. |
| Second | Supplier 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 document | Where it goes | Notes |
|---|
| Customer record | POST /customers | Create once; then refer to it by id or by your external_id. |
| Quotation (ใบเสนอราคา) | POST /sales-documents, type: quotation | For reference; creates nothing owed. |
| Invoice that is not a tax invoice (ใบแจ้งหนี้) | type: invoice | Paid with receipts. |
| Tax invoice (ใบกำกับภาษี) | type: tax_invoice | Paid with receipts. |
| Tax invoice that is also the receipt (cash sale) | type: tax_invoice_receipt | Recorded as paid; no separate receipt. |
| Credit note (ใบลดหนี้) or debit note (ใบเพิ่มหนี้) | type: credit_note or debit_note | Refers to one or more original tax invoices, or to the receipt of a paid invoice whose lines carry VAT. |
| Receipt (ใบเสร็จรับเงิน) | POST /receipts | Against an invoice, tax invoice or debit note. |
| Document received from a supplier | POST /bills | Second release. Tax invoice, receipt, invoice, import entry, foreign invoice or other. |
| Supplier credit or debit note | POST /bills/{id}/notes | Second release. Recorded against the bill it adjusts. |
| Payment to suppliers, with withholding tax certificate (50 ทวิ) | POST /payments | Second release. Allocated to one or more bills. |
| PDF or scan | POST /attachments | Attached to a sales document or receipt; to a bill or payment from the second release. |
| Month totals | GET /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.
- 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). - Create each customer once with
POST /customers and store the returned cus_ id with your own customer record. - When your system issues a sales document, record it with
POST /sales-documents: the number, date, lines and totals exactly as printed. - When a customer pays, record a receipt with
POST /receipts, including any tax the customer withheld. - If a document needs correcting, record a credit or debit note, or void it and record a replacement (see Corrections).
- When a supplier document arrives, record it with
POST /bills (second release). - When you pay suppliers, record the payment with
POST /payments, allocated to bills, with any tax you withheld (second release). - Attach the PDF or scan of each document with
POST /attachments. - 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:
- Record the
invoice. It books no VAT. - 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. - 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
| Field | Applies to |
|---|
valid_until | quotation |
due_date | invoice, tax_invoice, debit_note |
related_invoice_id | tax_invoice |
replaces_id | tax_invoice, tax_invoice_receipt |
original_document_ids, reason_code, reason_text, original_value, correct_value | credit_note, debit_note, and required for them |
| All other fields | Every 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).
| Environment | Base URL | Key prefix | What happens to the data |
|---|
| Sandbox (test keys) | https://api-sandbox.slf-accounting.co/v1 | slf_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/v1 | slf_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.
| Header | Direction | When | Value |
|---|
Authorization | Request | Always | Bearer followed by a space and your key. |
Content-Type | Request | With a body | application/json; charset=utf-8, or multipart/form-data for POST /attachments. |
Idempotency-Key | Request | Every POST | A unique value per request, 1 to 255 visible ASCII characters. A UUID works well. |
Request-Id | Response | Always | SLF'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-Replayed | Response | On a replayed POST | true: the response is the stored response of an earlier request with the same Idempotency-Key; nothing new was recorded. |
Retry-After | Response | With 429, sometimes 503 | Seconds 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).
| Topic | Rule |
|---|
| Field names | Lower 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 values | Optional 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). |
| Money | A string with exactly two decimals, such as "1070.00". Never a JSON number. Negative amounts are refused; use an adjustment note instead. |
| Quantities | A decimal string with up to 4 decimals, greater than zero, such as "2.5". |
| Rates | wht_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. |
| Dates | YYYY-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). |
| Timestamps | created_at and similar fields are RFC 3339 with an offset, such as 2026-10-15T14:02:11+07:00. Only the server sets them. |
| Currency | ISO 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 IDs | 13 digits, no dashes or spaces. The last digit must be the correct check digit; otherwise the detail code is invalid_tax_id. |
| Branch numbers | 5 digits. 00000 is the head office. |
| Ids | Opaque 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. |
| Text | Plain text. Line breaks are kept; HTML is not rendered. |
| Atomicity | Each request is all-or-nothing: if any field, line or allocation is refused, nothing is recorded. |
| Records SLF entered | Lists 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.
| Figure | How the API computes it |
|---|
Line pre-VAT amount (all categories except standard_incl) | round2(quantity × unit_price) |
Line VAT, standard | round2(pre_vat × 7 / 100) |
Line VAT and pre-VAT amount, standard_incl | gross = round2(quantity × unit_price); VAT = round2(gross × 7 / 107); pre-VAT = gross − VAT |
Line VAT, zero, exempt, none | 0.00 |
| Line withholding tax | round2(pre_vat × wht_rate / 100) |
subtotal, vat_total, wht_total | Sums of the rounded line figures (subtotal covers all categories) |
total | subtotal + vat_total |
net_to_pay | total − 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).
| Line | Gross | VAT | Pre-VAT |
|---|
| Printer paper A4, 3 boxes × 430.00 | 1290.00 | round2(1290.00 × 7 / 107) = 84.39 | 1205.61 |
| Toner cartridge, 1 × 2890.00 | 2890.00 | round2(2890.00 × 7 / 107) = 189.07 | 2700.93 |
| Document totals | total 4180.00 | vat_total 273.46 | subtotal 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 prints | Your vat_total | Recorded | Warning |
|---|
| VAT on the document's value: 174.00 × 7 / 100 | 12.18 | vat_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 up | 12.24 | vat_total 12.24 | vat_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
| Check | Response | Applies 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_number | Sales documents |
Same number and the same total in THB as a VAT entry SLF made by hand, in any month | 409 recorded_by_slf | tax_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_date | 422 tax_invoice_duplicates_receipt | tax_invoice, tax_invoice_receipt |
Same number as another receipt that is not voided | 409 duplicate_number | Receipts |
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 books | 409 recorded_by_slf_posted | Receipts that book VAT |
Same tax_id and branch_no | 409 customer_exists | Customers |
Same supplier tax_id (or, without one, the same supplier name) and bill_number | 409 duplicate_bill | Bills (second release) |
| Same file (by SHA-256) on the same record | 200 with the existing attachment | Attachments |
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
}
}
| Field | Meaning |
|---|
code | The error code. |
message | A short explanation for people. |
param | JSON 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. |
details | For 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_id | The same value as the Request-Id header. |
existing_id | For 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 code | Meaning |
|---|
required | A required field is missing or empty. |
invalid_format | The value has the wrong format, such as a date that is not YYYY-MM-DD or money without two decimals. |
invalid_value | The value is not allowed here, such as an unknown enum value. |
out_of_range | The value is too small or too large, such as a quantity of zero. |
too_long | The text is longer than the limit. |
invalid_tax_id | A Thai tax ID does not have 13 digits or its check digit is wrong. |
mismatch | A figure differs from the one the API computed; see expected and received. |
When to retry
| What happened | What to do |
|---|
| Network error or timeout (no response) | Retry with the same Idempotency-Key, with exponential backoff. |
429 rate_limited | Wait the number of seconds in Retry-After, then retry with the same key. |
| 500 or 503 | Retry 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 time | SLF'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_use | The first request with this key is still running. Wait a few seconds, then retry with the same key. |
| 401 or 403 | Stop 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 4xx | Do 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"
}
]
}
| Warning | When it appears |
|---|
disbursement_marked_up | A 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_self | A disbursement line has supplier_invoice_billed_to: self. |
vat_category_ignored | Your company is not VAT-registered, so a line's vat_category was recorded as none. |
zero_rated_review | A line uses vat_category: zero. |
vat_arithmetic_review | The 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_review | The tax withheld on a receipt does not match the rates on the document's lines. |
short_payment | A receipt's gross_amount is less than what the document still owed. |
recorded_by_slf_filed | A receipt books VAT that SLF had already entered by hand, in a month whose VAT return it has filed. |
possible_double_vat | A 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
| Situation | What 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 values | A 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 rate | Ask SLF before you record anything. |
| A quotation or an invoice that is not a tax invoice is wrong or cancelled | Void it, and record a new one if needed. |
| A receipt was recorded by mistake | Void 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 record | Contact 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:
- The original is retrieved, stamped ยกเลิก (cancelled) or crossed out, and kept with its copy.
- The replacement has a new number but the same date as the original.
- The replacement carries the remark เป็นการยกเลิกและออกใบกำกับภาษีฉบับใหม่แทนฉบับเดิมเลขที่ … เล่มที่ … (cancelling and replacing the original, number … book …).
- The cancellation is noted in the output tax report of the month in which the replacement is issued.
In the API:
- 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. - 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.
| Record | Month taken from | Refused with period_filed | Refused with period_closed |
|---|
tax_invoice, tax_invoice_receipt, credit_note, debit_note | issue_date | Create and void, once the VAT return is filed | Create and void |
Receipt against an invoice whose lines carry VAT (it books that VAT) | payment_date | Create and void, once the VAT return is filed | Create and void |
Receipt against a tax_invoice or debit_note (the VAT is booked by that document) | payment_date | Never | Create and void |
| Supplier payment with withholding (second release) | pay_date | Create, once the withholding return of its return_form is filed | Create and reverse |
| Supplier payment without withholding (second release) | pay_date | Never | Create and reverse |
quotation, invoice; supplier bill and supplier note (second release) | — | Never | Never |
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.
| Value | Meaning |
|---|
goods_nonconforming | Price reduced because goods did not meet the agreed specification, were damaged or short. |
goods_returned | Goods returned because defective, not matching sample or description. |
goods_return_exchange | Goods returned or exchanged under a trade agreement: between VAT-registered businesses, or between you and a buyer if done within a reasonable time. |
service_deficient | Service price reduced because the service was deficient, short or not as agreed. |
price_overcharged | Price calculated higher than it should have been. |
service_contract_cancelled_deficient | Service contract cancelled because the service was deficient or not as agreed. |
service_contract_cancelled_not_provided | Service contract cancelled because the service was not provided. |
deposit_refund | Refund of an advance payment, deposit, guarantee or booking money under a trade agreement. |
compensation | Compensation or similar the seller must pay the buyer by law. |
Debit note reasons
debit_note_reason follows s.82/9 of the Revenue Code.
| Value | Meaning |
|---|
goods_quantity_exceeded | More goods delivered than agreed. |
goods_undercharged | Goods price calculated lower than it should have been. |
service_exceeded | Service provided beyond what was agreed. |
service_undercharged | Service price calculated lower than it should have been. |
other_announced | Another 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).
| Limit | Value |
|---|
| Requests | 60 per minute per key (draft value). Above it: 429 rate_limited with Retry-After. |
| Short bursts | When 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 body | 1 MB |
| File upload | 10 MB; PDF, JPEG, PNG or WebP, recognised from the file's content. HEIC is not accepted; convert such photos to JPEG. |
| Uploads per company | 1 GB or 1,000 files per day (Bangkok time), whichever comes first. Above it: 429 rate_limited with Retry-After until midnight. |
| Maintenance window | Once 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 document | 1 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 size | limit 1 to 100, default 25 |
| Idempotency window | 24 hours |
| Records per request | One. 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.
| Field | Type | Required | Description |
|---|
name | string | read-only | The 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_local | string | read-only | The company's registered name in Thai, as SLF has it on record. Example: บริษัท ตัวอย่าง จำกัด. |
tax_id | string 13 digits | read-only | The company's tax ID. Example: 0999000000019. |
branch_no | string 5 digits | read-only | The branch number SLF has on record for the company the key belongs to. 00000 is the head office. Example: 00000. |
vat_registered | boolean | read-only | The 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
| Name | In | Type | Required | Description |
|---|
limit | query | integer | optional | Number of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25. |
cursor | query | string ≤ 512 characters | optional | The 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.
| Field | Type | Required | Description |
|---|
id | string
bnk_ id | read-only | Use it as bank_account_id. Example: bnk_5Qm2Rk8Lt3Vw. |
bank_name | string | read-only | The 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_name | string | read-only | The name the account is held in. Example: Example Trading Co., Ltd.. |
account_number_last4 | string 4 digits | read-only | The last four digits of the account number. The full number is never returned. Example: 4821. |
currency | string ISO 4217 | read-only | The account's currency. Example: THB. |
type | string · enum bank_account_type | read-only | personal 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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
| Name | In | Type | Required | Description |
|---|
tax_id | query | string | optional | Only customers with this tax ID. Example: 0999000000027. |
branch_no | query | string | optional | Only customers with this branch number. Use together with tax_id. Example: 00000. |
external_id | query | string | optional | Only the customer with this external_id. Example: CUST-00412. |
limit | query | integer | optional | Number of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25. |
cursor | query | string ≤ 512 characters | optional | The 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
| Name | In | Type | Required | Description |
|---|
id | path | string | required | The 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
| Name | In | Type | Required | Description |
|---|
id | path | string | required | The 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.
| Field | Type | Required | Description |
|---|
id | string
cus_ id | read-only | SLF's id for the customer. Example: cus_7Hc4Nf2Pq9Xz. |
external_id | string ≤ 100 characters | required | Your 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. |
name | string ≤ 300 characters | required | The customer's name as printed on your documents. Example: Sample Retail Co., Ltd.. |
name_local | string ≤ 300 characters | optional | The name in Thai, if different. Example: บริษัท ตัวอย่างค้าปลีก จำกัด. |
country | string ISO 3166-1 alpha-2 | optional | The customer's country. Thai tax-ID validation applies only when it is TH. Default: TH. Example: TH. |
tax_id | string 13 digits when country is TH | optional | The 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_no | string 5 digits | optional | The 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. |
address | string ≤ 1000 characters | optional | The address printed on your documents. Example: 88 Example Road, Pathum Wan, Bangkok 10330. |
email | string ≤ 254 characters | optional | Billing email address. Example: accounts@sample-retail.example. |
phone | string ≤ 50 characters | optional | Phone number. Example: +66 2 000 0000. |
notes | string ≤ 2000 characters | optional | Your notes about the customer. |
created_at | date-time | read-only | When 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.
| Field | Type | Required | Description |
|---|
name | string ≤ 300 characters | optional | New name. Cannot be cleared. |
name_local | string ≤ 300 characters | optional | New name in Thai. |
address | string ≤ 1000 characters | optional | New address. |
email | string ≤ 254 characters | optional | New billing email address. Example: ap@sample-retail.example. |
phone | string ≤ 50 characters | optional | New phone number. Example: +66 2 000 0001. |
notes | string ≤ 2000 characters | optional | New notes. |
tax_id | string 13 digits when country is TH | optional | Only 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_no | string 5 digits | optional | Only 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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
| Name | In | Type | Required | Description |
|---|
type | query | string · enum sales_document_type | optional | Only documents of this type. Example: tax_invoice. |
customer_id | query | string | optional | Only documents for this customer. Example: cus_7Hc4Nf2Pq9Xz. |
external_id | query | string | optional | Only the document with this external_id. Example: SO-2026-10-0412. |
number | query | string | optional | Only documents with this printed number. Example: TIV-2026-0412. |
reference | query | string | optional | Only documents with this reference. Example: PRJ-2026-031. |
status | query | string · enum sales_document_status | optional | Only documents with this status, such as open to find what is still owed. Example: open. |
issue_date_from | query | date | optional | Only documents issued on or after this date. Example: 2026-10-01. |
issue_date_to | query | date | optional | Only documents issued on or before this date. Example: 2026-10-31. |
limit | query | integer | optional | Number of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25. |
cursor | query | string ≤ 512 characters | optional | The 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
| Name | In | Type | Required | Description |
|---|
id | path | string | required | The 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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. |
id | path | string | required | The 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.
| Field | Type | Required | Description |
|---|
id | string
doc_ id | read-only | SLF's id for the document. Example: doc_4Fp9Km2Qx7Ld. |
type | string · enum sales_document_type | required | What kind of document it is. Example: invoice. |
external_id | string ≤ 100 characters | required | Your 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. |
number | string ≤ 40 characters | required | The 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_date | date | required | The 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_date | date | optional | For invoice, tax_invoice and debit_note: the payment due date. Not before issue_date. Example: 2026-11-14. |
valid_until | date | optional | For 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_id | string | required unless customer_external_id is given | The customer the document is issued to. Example: cus_7Hc4Nf2Pq9Xz. |
customer_external_id | string | required unless customer_id is given | Instead of customer_id: the external_id you gave the customer. Request only; responses always carry customer_id. Example: CUST-00412. |
bill_to_address | string ≤ 1000 characters | optional | The buyer address printed on this document, if different from the customer's address. |
currency | string ISO 4217 | optional | The 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_rate | decimal string up to 6 decimals | required when currency is not THB | THB per one unit of currency, as used by your system (fx_rate_required). Example: 32.4512. |
fx_date | date | required when currency is not THB | The date of fx_rate. Example: 2026-10-27. |
payment_terms | string · enum payment_terms | optional | Trade payment terms printed on the document, if any. Example: tt. |
bank_account_id | string | optional | The bank account printed on the document for payment (a bnk_ id from GET /bank-accounts). Example: bnk_5Qm2Rk8Lt3Vw. |
remark | string ≤ 20000 characters | optional | The remark printed on the document, as plain text. Example: Please pay by bank transfer within 30 days.. |
reference | string ≤ 60 characters | optional | Your 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_id | string | optional | For 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_id | string | optional | For 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_ids | array of string | required for credit_note and debit_note | The 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_code | string · enum credit_note_reason or debit_note_reason | required for credit_note and debit_note | Why 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_text | string ≤ 500 characters | required for credit_note and debit_note | The 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_value | decimal string | required for credit_note and debit_note | The total value before VAT shown on the original document or documents. Example: 29168.00. |
correct_value | decimal string | required for credit_note and debit_note | The correct value before VAT. The difference from original_value must equal the note's subtotal (note_difference_mismatch). Example: 24268.00. |
lines | array of SalesDocumentLine 1 to 500 lines | required | The lines as printed. For a credit or debit note: the change in value, as positive amounts. |
totals | DocumentTotals | required | The totals as printed. Checked against the lines (totals_mismatch). |
attachments | array of Attachment | read-only | Files attached with POST /attachments. |
status | string · enum sales_document_status | read-only | Payment or validity status. Example: open. |
paid_amount | decimal string | read-only | The 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_amount | decimal string | read-only | What 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. |
outstanding | decimal string | read-only | total 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_at | date-time | read-only | When the document was voided, or null. |
void_reason | string | read-only | The reason given when voiding, or null. |
replaced_by_id | string | read-only | For a voided tax invoice: the replacement's id, or null. Example: doc_8Jd2Sv6Wm4Pa. |
warnings | array of Warning | read-only | Points flagged for SLF review. See Warnings and review. |
created_at | date-time | read-only | When the document was recorded. Example: 2026-10-15T14:02:11+07:00. |
SalesDocumentLine Sales document line
One line of a sales document, as printed.
| Field | Type | Required | Description |
|---|
name | string ≤ 300 characters | required | The item or service, as printed. Example: Accounting system setup. |
description | string ≤ 1000 characters | optional | Further description printed under the name. |
quantity | decimal string > 0, up to 4 decimals | required | Quantity. Example: 3. |
unit_price | decimal string ≥ 0, 2 decimals | required | Price per unit: excluding VAT, or including VAT for standard_incl. Example: 2150.75. |
vat_category | string · enum vat_category | optional | How 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_rate | string · enum wht_rate | optional | The 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_code | string ≤ 50 characters | optional | Your 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_code | string ≤ 20 characters | optional | An 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_role | string · enum charge_role | optional | Whether 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_to | string · enum supplier_invoice_billed_to | required when charge_role is disbursement | Who the supplier's document for the cost is addressed to. Example: customer. |
pass_through_cost | decimal string | required when charge_role is disbursement | The 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_amount | decimal string | read-only | The 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_rate | string percentage | read-only | The VAT rate applied, following vat_category: 7 for standard and standard_incl, 0 otherwise. Example: 7. |
vat_amount | decimal string | read-only | The 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_amount | decimal string | read-only | round2(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.
| Field | Type | Required | Description |
|---|
subtotal | decimal string | required | Sum of the lines' pre-VAT amounts, all VAT categories. Example: 20452.25. |
vat_total | decimal string | required | Sum 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. |
total | decimal string | required | subtotal plus vat_total. Example: 21778.91. |
wht_total | decimal string | optional | Sum of the lines' withholding tax, if printed. Example: 568.57. |
net_to_pay | decimal string | optional | total minus wht_total, if printed. Example: 21210.34. |
SalesDocumentVoidRequest Void request (sales document)
The body of POST /sales-documents/{id}/void.
| Field | Type | Required | Description |
|---|
reason | string ≤ 500 characters | required | Why the document is voided. Example: Buyer's address was wrong. Replaced by TIV-2026-0415.. |
replacement_id | string | optional | When 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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
| Name | In | Type | Required | Description |
|---|
document_id | query | string | optional | Only receipts against this sales document. Example: doc_4Fp9Km2Qx7Ld. |
external_id | query | string | optional | Only the receipt with this external_id. Example: RCV-2026-10-0233. |
payment_date_from | query | date | optional | Only receipts with a payment_date on or after this date. Example: 2026-10-01. |
payment_date_to | query | date | optional | Only receipts with a payment_date on or before this date. Example: 2026-10-31. |
limit | query | integer | optional | Number of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25. |
cursor | query | string ≤ 512 characters | optional | The 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
| Name | In | Type | Required | Description |
|---|
id | path | string | required | The 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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. |
id | path | string | required | The 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.
| Field | Type | Required | Description |
|---|
id | string
rcp_ id | read-only | SLF's id for the receipt. Example: rcp_3Np8Gz5Kd1Vt. |
external_id | string ≤ 100 characters | required | Your 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. |
number | string ≤ 40 characters | required | Your receipt number, as printed. Unique among your receipts that are not voided (duplicate_number). Example: RE-2026-0233. |
document_id | string | required | The invoice, tax_invoice or debit_note being paid. Example: doc_4Fp9Km2Qx7Ld. |
payment_date | date | required | The 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_received | decimal string | required | The money that arrived, in the document's currency. Example: 21210.34. |
wht_amount | decimal string | optional | Tax 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_id | string | optional | The account the money arrived in. Example: bnk_5Qm2Rk8Lt3Vw. |
remark | string ≤ 20000 characters | optional | Your 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_amount | decimal string | read-only | amount_received plus wht_amount. This is what reduces the document's outstanding. Example: 21778.91. |
vat_amount | decimal string | read-only | The 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. |
currency | string ISO 4217 | read-only | Taken from the document. Example: THB. |
fx_rate | decimal string | read-only | Taken from the document; null for THB. |
attachments | array of Attachment | read-only | Files attached with POST /attachments. |
warnings | array of Warning | read-only | Points flagged for SLF review, such as wht_rate_review or short_payment. |
voided_at | date-time | read-only | When the receipt was voided, or null. |
void_reason | string | read-only | The reason given when voiding, or null. |
created_at | date-time | read-only | When 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.
| Field | Type | Required | Description |
|---|
reason | string ≤ 500 characters | required | Why 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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
| Name | In | Type | Required | Description |
|---|
external_id | query | string | optional | Only the bill with this external_id. Example: AP-2026-10-0058. |
supplier_tax_id | query | string | optional | Only bills from the supplier with this tax ID. Example: 0999000000035. |
reference | query | string | optional | Only bills with this reference. Example: PRJ-2026-031. |
status | query | string · enum bill_status | optional | Only bills with this status, such as open to find what you still owe. Example: open. |
bill_date_from | query | date | optional | Only bills with a bill_date on or after this date. Example: 2026-10-01. |
bill_date_to | query | date | optional | Only bills with a bill_date on or before this date. Example: 2026-10-31. |
limit | query | integer | optional | Number of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25. |
cursor | query | string ≤ 512 characters | optional | The 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
| Name | In | Type | Required | Description |
|---|
id | path | string | required | The 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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. |
id | path | string | required | The 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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. |
id | path | string | required | The 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.
| Field | Type | Required | Description |
|---|
id | string
bil_ id | read-only | SLF's id for the bill. Example: bil_5Tg9Hw2Lr6Mx. |
external_id | string ≤ 100 characters | required | Your 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_type | string · enum bill_document_type | required | What the supplier gave you. Example: tax_invoice. |
supplier | Supplier | required | The supplier, as printed on the document. |
bill_number | string ≤ 40 characters | required | The supplier's document number. Example: DS-2610-0058. |
bill_date | date | required | The date printed on the supplier's document. Example: 2026-10-25. |
due_date | date | optional | When the bill is due. Example: 2026-11-24. |
currency | string ISO 4217 | optional | The bill's currency. Default: THB. Example: THB. |
fx_rate | decimal string up to 6 decimals | required when currency is not THB | THB per one unit of currency (fx_rate_required). |
fx_date | date | required when currency is not THB | The date of fx_rate. |
subtotal | decimal string | required | Value before VAT, as printed. Example: 15000.00. |
vat_amount | decimal string | required | VAT as printed; 0.00 if none. Example: 1050.00. |
total | decimal string | required | Total as printed. Must equal subtotal plus vat_amount (totals_mismatch). Example: 16050.00. |
lines | array of BillLine 0 to 500 lines | optional | The bill's lines, if you have them. They help SLF classify the cost. |
on_behalf_of_customer_id | string | optional | When 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. |
remark | string ≤ 20000 characters | optional | Your remark, as plain text. |
reference | string ≤ 60 characters | optional | Your 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. |
status | string · enum bill_status | read-only | Payment status. Example: open. |
paid_amount | decimal string | read-only | Cash paid plus tax withheld, from payment allocations to this bill. Example: 0.00. |
outstanding | decimal string | read-only | total minus paid_amount, less supplier credit notes and plus supplier debit notes recorded against the bill. Example: 16050.00. |
notes | array of BillNote | read-only | Supplier credit and debit notes recorded against the bill. |
attachments | array of Attachment | read-only | Files attached with POST /attachments. |
voided_at | date-time | read-only | When the bill was voided, or null. |
void_reason | string | read-only | The reason given when voiding, or null. |
created_at | date-time | read-only | When 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.
| Field | Type | Required | Description |
|---|
name | string ≤ 300 characters | required | The supplier's name. Example: Demo Services Co., Ltd.. |
tax_id | string 13 digits when country is TH | optional | The 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_no | string 5 digits | optional | The supplier's branch, as printed. 00000 is the head office. Example: 00000. |
country | string ISO 3166-1 alpha-2 | optional | The 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.
| Field | Type | Required | Description |
|---|
description | string ≤ 1000 characters | required | The line as printed. Example: IT support, October 2026. |
quantity | decimal string > 0, up to 4 decimals | optional | Quantity. Example: 1. |
unit_price | decimal string ≥ 0 | optional | Price per unit. Example: 15000.00. |
amount | decimal string | required | The line amount as printed. Example: 15000.00. |
item_code | string ≤ 50 characters | optional | The supplier's item code, if printed. SLF maps it to an account. Example: DS-ITS-01. |
account_code | string ≤ 20 characters | optional | An 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.
| Field | Type | Required | Description |
|---|
id | string
bnt_ id | read-only | SLF's id for the note. Example: bnt_1Qz6Ck3Pv8Ys. |
bill_id | string | read-only | The bill the note adjusts (from the path). Example: bil_2Cw8Np4Vs7Kf. |
external_id | string ≤ 100 characters | required | Your 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. |
type | string · enum bill_note_type | required | Credit or debit note. Example: credit_note. |
number | string ≤ 40 characters | required | The supplier's note number. Example: DS-CN-0007. |
date | date | required | The date printed on the note. Example: 2026-11-02. |
amount | decimal string | required | The change in value before VAT, as printed. Example: 1000.00. |
vat_amount | decimal string | required | The VAT on that change, as printed; 0.00 if none. Example: 70.00. |
reason | string ≤ 500 characters | required | The supplier's reason, as free text. Example: Two support visits were not provided. |
created_at | date-time | read-only | When 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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
| Name | In | Type | Required | Description |
|---|
external_id | query | string | optional | Only the payment with this external_id. Example: AP-PAY-2026-10-0031. |
bill_id | query | string | optional | Only payments with an allocation to this bill. Example: bil_5Tg9Hw2Lr6Mx. |
pay_date_from | query | date | optional | Only payments with a pay_date on or after this date. Example: 2026-10-01. |
pay_date_to | query | date | optional | Only payments with a pay_date on or before this date. Example: 2026-10-31. |
limit | query | integer | optional | Number of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25. |
cursor | query | string ≤ 512 characters | optional | The 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
| Name | In | Type | Required | Description |
|---|
id | path | string | required | The 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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. |
id | path | string | required | The 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.
| Field | Type | Required | Description |
|---|
id | string
pay_ id | read-only | SLF's id for the payment. Example: pay_7Mv4Ys1Jn9Bc. |
external_id | string ≤ 100 characters | required | Your 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_date | date | required | The date you paid. Example: 2026-10-31. |
method | string · enum payment_method | required | How you paid. Example: transfer. |
bank_account_id | string | required unless method is cash | The account you paid from. Example: bnk_5Qm2Rk8Lt3Vw. |
reference | string ≤ 100 characters | optional | The bank transfer or cheque reference. Example: TRF-20261031-0007. |
allocations | array of PaymentAllocation 1 to 100 | required | The bills this payment settles. |
withholding | array of WithholdingEntry up to 50 | required when any allocations[].wht_amount is above zero | The 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). |
attachments | array of Attachment | read-only | Files attached with POST /attachments. |
state | string · enum payment_state | read-only | Whether the payment stands or was reversed. Example: recorded. |
reversed_at | date-time | read-only | When the payment was reversed, or null. |
reverse_reason | string | read-only | The reason given when reversing, or null. |
created_at | date-time | read-only | When the payment was recorded. Example: 2026-10-31T16:55:02+07:00. |
PaymentAllocation Payment allocation
The part of a payment that settles one bill.
| Field | Type | Required | Description |
|---|
bill_id | string | required | The bill. Example: bil_5Tg9Hw2Lr6Mx. |
amount | decimal string | required | The cash paid towards this bill, in the bill's currency. Example: 15600.00. |
wht_amount | decimal string | optional | The 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.
| Field | Type | Required | Description |
|---|
payee_name | string ≤ 300 characters | required | The payee's name. Example: Demo Services Co., Ltd.. |
payee_tax_id | string 13 digits | required | The payee's tax ID, with a valid check digit (invalid_tax_id). Example: 0999000000035. |
payee_branch_no | string 5 digits | optional | The payee's branch. 00000 is the head office. Default: 00000. Example: 00000. |
payee_address | string ≤ 1000 characters | optional | The payee's address, as on the certificate. Example: 1 Example Road, Khlong Toei, Bangkok 10110. |
payee_type | string · enum payee_type | required | Individual or company. return_form is set from it, and SLF confirms the return on review. Example: company. |
income_type | string · enum income_type | required | The kind of payment the tax was withheld from. Example: service. |
income_description | string ≤ 300 characters | required when income_type is other | What the payment was for. |
gross_amount | decimal string | required | The amount the tax was computed on, in THB. Example: 15000.00. |
rate | string · enum wht_rate | required | The rate you actually withheld. Example: 3. |
wht_amount | decimal string | required | The tax withheld, in THB. Must equal round2(gross_amount × rate / 100) (totals_mismatch). Example: 450.00. |
certificate_number | string ≤ 40 characters | optional | The number of the 50 ทวิ certificate, if you issued it yourself. Example: WHT-2026-0101. |
return_form | string · enum return_form | read-only | The 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
| Name | In | Type | Required | Description |
|---|
Idempotency-Key | header | string ≤ 255 characters | required | Required 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
| Name | In | Type | Required | Description |
|---|
object_type | query | string · enum attachment_object_type | required | The kind of record. Example: sales_document. |
object_id | query | string | required | The record's id. Example: doc_9Rt3Vb6Nz1Hs. |
limit | query | integer | optional | Number of records per page: a whole number from 1 to 100. Default 25. For the refusals, see Pagination in Conventions. Example: 25. |
cursor | query | string ≤ 512 characters | optional | The 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
| Name | In | Type | Required | Description |
|---|
id | path | string | required | The 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
| Name | In | Type | Required | Description |
|---|
id | path | string | required | The 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.
| Field | Type | Required | Description |
|---|
id | string
att_ id | read-only | SLF's id for the attachment. Example: att_2Bh5Rx8Fk3Wq. |
object_type | string · enum attachment_object_type | read-only | The kind of record it is attached to. Example: sales_document. |
object_id | string | read-only | The record's id. Example: doc_9Rt3Vb6Nz1Hs. |
kind | string · enum attachment_kind | read-only | What the file is. Example: document_copy. |
file_name | string | read-only | The file name you uploaded. Example: TIV-2026-0412.pdf. |
content_type | string | read-only | The file's media type, as recognised from its content. Example: application/pdf. |
size | integer | read-only | Size in bytes. Example: 184233. |
sha256 | string 64 hex characters | read-only | SHA-256 of the file. The same file on the same record is stored once. |
created_at | date-time | read-only | When 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.
| Field | Type | Required | Description |
|---|
file | string binary; PDF, JPEG, PNG or WebP; ≤ 10 MB | required | The file, with its file name. One file per request. Its type is read from its content; HEIC is not accepted. |
object_type | string · enum attachment_object_type | required | The kind of record to attach it to. Example: sales_document. |
object_id | string | required | The 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. |
kind | string · enum attachment_kind | required | What 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
| Name | In | Type | Required | Description |
|---|
month | path | string | required | The 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.
| Field | Type | Required | Description |
|---|
month | string YYYY-MM | read-only | The month. Example: 2026-09. |
status | string · enum period_status | read-only | Whether the month is open, VAT-filed or closed. Example: vat_filed. |
filings | array of PeriodFiling | read-only | Which of the month's returns SLF has filed. |
sales | PeriodSales | read-only | Sales recorded for VAT in the month. |
purchases | PeriodPurchases | read-only | Purchases recorded for VAT in the month. |
out_of_scope_value | decimal string | read-only | Disbursements recovered from customers in the month: disbursement lines billed to the customer. Not included in sales. Example: 12750.00. |
quarantined | PeriodQuarantined | read-only | Sales records of the month held back by SLF's arithmetic check until SLF resolves them. Not included in sales. |
withholding | PeriodWithholding | read-only | Tax you withheld from supplier payments in the month. |
PeriodFiling Period filing
One return for the month and whether SLF has filed it.
| Field | Type | Required | Description |
|---|
form | string · enum filing_form | read-only | The return. Example: pp30. |
filed | boolean | read-only | Whether 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.
| Field | Type | Required | Description |
|---|
document_count | integer | read-only | Number of documents and receipts counted. Example: 64. |
taxable_value | decimal string | read-only | Value before VAT of standard and standard_incl lines. Example: 412350.00. |
zero_rated_value | decimal string | read-only | Value of zero lines. Example: 38900.00. |
exempt_value | decimal string | read-only | Value of exempt lines. Example: 0.00. |
unclassified_value | decimal string | read-only | Value 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. |
vat | decimal string | read-only | Output 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.
| Field | Type | Required | Description |
|---|
document_count | integer | read-only | Number of documents counted. Example: 41. |
taxable_value | decimal string | read-only | Value before VAT. Example: 158200.00. |
vat | decimal string | read-only | Input 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.
| Field | Type | Required | Description |
|---|
count | integer | read-only | How many are held back. Example: 0. |
value | decimal string | read-only | Their 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.
| Field | Type | Required | Description |
|---|
pnd3_amount | decimal string | read-only | Entries with return_form pnd3, for the ภ.ง.ด.3 withholding return. Example: 1250.00. |
pnd53_amount | decimal string | read-only | Entries with return_form pnd53, for the ภ.ง.ด.53 withholding return. Example: 4830.00. |
Shared objects
Error Error
The body of every error response.
| Field | Type | Required | Description |
|---|
error | ErrorObject | read-only | What went wrong. |
ErrorObject Error object
Details of an error. See Errors and retries.
| Field | Type | Required | Description |
|---|
code | string | read-only | The error code, one of those listed under Error codes. Stable; branch on this. Example: validation_failed. |
message | string | read-only | A short explanation for people. It may change; do not parse it. |
param | string JSON Pointer | read-only | The 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. |
details | array of ErrorDetail | read-only | For validation_failed, mismatch errors and vat_category_unexplained: one entry per problem. Empty otherwise. |
request_id | string ≤ 64 characters | read-only | The same value as the Request-Id header. Quote it when you contact SLF. Example: req_3c9e1f7a2b5d8c04. |
existing_id | string | read-only | For 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.
| Field | Type | Required | Description |
|---|
param | string JSON Pointer | read-only | The 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. |
code | string · enum error_detail_code | read-only | What is wrong with it. Example: invalid_tax_id. |
message | string | read-only | A short explanation for people. |
expected | decimal string | read-only | Mismatch details only: the figure the API computed. null otherwise. Example: 1326.66. |
received | decimal string | read-only | Mismatch 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.
| Field | Type | Required | Description |
|---|
code | string | read-only | The warning code, one of those listed under Warning codes. Example: zero_rated_review. |
message | string | read-only | A short explanation for people. |
param | string JSON Pointer | read-only | The field concerned, or null. Example: /lines/0/vat_category. |
Enumerations
sales_document_type
The kind of sales document.
| Value | Meaning |
|---|
quotation | Quotation (ใบเสนอราคา). Creates nothing owed and cannot be receipted. |
invoice | An invoice that is not a tax invoice (ใบแจ้งหนี้). Paid with receipts. |
tax_invoice | Tax invoice (ใบกำกับภาษี). Paid with receipts. |
tax_invoice_receipt | A tax invoice that is also the receipt, for a cash sale. Recorded as paid; it cannot be receipted. |
credit_note | Credit note (ใบลดหนี้): a decrease in value after a tax invoice. Needs the note fields; see Corrections. |
debit_note | Debit 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.
| Value | Meaning |
|---|
open | Recorded, and no receipt recorded against it yet. For a quotation: still valid. For a credit note: recorded. |
partially_paid | Receipts are recorded against it, and something is still outstanding. |
paid | Nothing 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. |
expired | Quotations only: valid_until has passed. |
rejected | Quotations only: recorded at SLF as rejected by the customer. The API does not set it. |
void | Voided. 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.
| Value | Meaning |
|---|
standard | Current standard rate: 7% including local tax (the Revenue Code s.80 rate is 10%, reduced by royal decree). The price excludes VAT. |
standard_incl | Same rate as standard; the price includes VAT. |
zero | 0%: only where s.80/1 applies, for example qualifying exports. SLF reviews every zero line (warning zero_rated_review). |
exempt | Exempt under s.81. |
none | Not 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.
| Value | Meaning |
|---|
0 | No tax withheld. |
1 | 1%. |
2 | 2%. |
3 | 3%. |
5 | 5%. |
10 | 10%. |
15 | 15%. |
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.
| Value | Meaning |
|---|
own_supply | Your own sale of goods or services. |
disbursement | Recovering 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.
| Value | Meaning |
|---|
customer | Addressed to your customer. |
self | Addressed 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.
| Value | Meaning |
|---|
tt | Telegraphic (bank) transfer. |
lc | Letter of credit. |
sblc | Standby letter of credit. |
bg | Bank 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.
| Value | Meaning |
|---|
goods_nonconforming | Price reduced because goods did not meet the agreed specification, were damaged or short. |
goods_returned | Goods returned because defective, not matching sample or description. |
goods_return_exchange | Goods returned or exchanged under a trade agreement: between VAT-registered businesses, or between you and a buyer if done within a reasonable time. |
service_deficient | Service price reduced because the service was deficient, short or not as agreed. |
price_overcharged | Price calculated higher than it should have been. |
service_contract_cancelled_deficient | Service contract cancelled because the service was deficient or not as agreed. |
service_contract_cancelled_not_provided | Service contract cancelled because the service was not provided. |
deposit_refund | Refund of an advance payment, deposit, guarantee or booking money under a trade agreement. |
compensation | Compensation 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.
| Value | Meaning |
|---|
goods_quantity_exceeded | More goods delivered than agreed. |
goods_undercharged | Goods price calculated lower than it should have been. |
service_exceeded | Service provided beyond what was agreed. |
service_undercharged | Service price calculated lower than it should have been. |
other_announced | Another 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.
| Value | Meaning |
|---|
tax_invoice | A tax invoice (ใบกำกับภาษี) issued to you. |
receipt | A receipt that is not a tax invoice. |
invoice | An invoice or bill that is not a tax invoice. |
import_entry | An import entry (customs declaration) for goods you imported. |
foreign_invoice | An invoice from a supplier outside Thailand. |
other | Any other document. Describe it in remark. |
bill_status
Payment status of a supplier bill.
| Value | Meaning |
|---|
open | Nothing paid yet. |
partially_paid | Payments or supplier credit notes cover part of it, and something is still outstanding. |
paid | Nothing is outstanding. |
void | Voided. |
bill_note_type
The kind of supplier adjustment note.
| Value | Meaning |
|---|
credit_note | The supplier reduced the amount. |
debit_note | The supplier increased the amount. |
payment_method
How a supplier payment was made.
| Value | Meaning |
|---|
transfer | Bank transfer. Needs bank_account_id. |
cheque | Cheque. Needs bank_account_id. |
cash | Cash. |
payment_state
Whether a supplier payment stands.
| Value | Meaning |
|---|
recorded | The payment is recorded. |
reversed | The 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.
| Value | Meaning |
|---|
individual | An individual. return_form is set to pnd3 (ภ.ง.ด.3); SLF confirms the return on review (see Tax notes). |
company | A 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.
| Value | Meaning |
|---|
service | Service fees. |
professional_fee | Professional fees. |
rent | Rent. |
transport | Transport charges. |
advertising | Advertising fees. |
commission | Commissions. |
interest | Interest. |
other | Anything else. Describe it in income_description. |
attachment_object_type
The kind of record a file is attached to.
| Value | Meaning |
|---|
sales_document | A sales document (doc_). |
receipt | A receipt (rcp_). |
bill | A supplier bill (bil_). From the second release; until then refused with reference_not_found. |
payment | A supplier payment (pay_). From the second release; until then refused with reference_not_found. |
attachment_kind
What an attached file is.
| Value | Meaning |
|---|
document_copy | A copy of a document you issued. |
supplier_document | A document you received from a supplier. |
payment_proof | Proof of payment, such as a transfer slip. |
wht_certificate_received | A withholding tax certificate (50 ทวิ) your customer gave you. |
wht_certificate_issued | A withholding tax certificate (50 ทวิ) you issued. |
customs_document | A customs document, such as an import or export entry. |
other | Anything else. |
bank_account_type
Whose name a bank account is held in, where SLF has recorded it. Any other account has type null.
| Value | Meaning |
|---|
personal | A person's name. |
period_status
Where a month stands at SLF.
| Value | Meaning |
|---|
open | SLF has not filed the month's VAT return or closed the period. |
vat_filed | SLF has filed the month's VAT return. Records that would change it are refused with period_filed; see Filed periods. |
closed | SLF has closed the accounting period. Records dated in it are refused with period_closed; see Filed periods. |
error_detail_code
What is wrong with one field, in error.details[].
| Value | Meaning |
|---|
required | A required field is missing or empty. |
invalid_format | The value has the wrong format, such as a date that is not YYYY-MM-DD or money without two decimals. |
invalid_value | The value is not allowed here, such as an unknown enum value or a voided original. |
out_of_range | The value is too small or too large, such as a quantity of zero. |
too_long | The text is longer than the limit. |
invalid_tax_id | A Thai tax ID does not have 13 digits or its check digit is wrong. |
mismatch | A figure differs from the one the API computed; see expected and received. |
Error codes
| Code | HTTP | Meaning | What to do |
|---|
invalid_json | 400 | The 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_missing | 400 | A 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. |
unauthenticated | 401 | The 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. |
forbidden | 403 | The 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_found | 404 | The 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_allowed | 405 | The HTTP method is not supported on this path. | Use a method listed in this reference. |
idempotency_key_reused | 409 | This 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_use | 409 | A request with this Idempotency-Key is still being processed. | Wait a few seconds, then retry with the same key. |
payload_too_large | 413 | The JSON body is larger than 1 MB, or the uploaded file larger than 10 MB. | Reduce the size. Do not retry unchanged. |
unsupported_media_type | 415 | The 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_failed | 422 | One 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_limited | 429 | More 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_error | 500 | An unexpected error at SLF. | Retry with the same Idempotency-Key, with exponential backoff. If it persists, contact SLF with the request_id. |
service_unavailable | 503 | The 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_id | 409 | A 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_number | 409 | A 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_slf | 409 | SLF 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_posted | 409 | The 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_exists | 409 | A 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_bill | 409 | Second 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_voided | 409 | The record is already voided, or the payment already reversed. | Nothing to do. Fetch the record to see its state. |
void_not_allowed | 409 | The 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_found | 422 | An 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_mismatch | 422 | Figures 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_mismatch | 422 | The 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_invoice | 422 | A 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_original | 422 | A 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_voided | 422 | Credit and debit notes cannot be voided through the API. | Do not retry; contact SLF. |
disbursement_vat_category | 422 | A 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_up | 422 | A 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_unexplained | 422 | Your 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_receipt | 422 | A 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_filed | 422 | The 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_closed | 422 | The record's date falls in an accounting period SLF has closed. | Do not retry; contact SLF. |
foreign_currency_tax_invoice_not_allowed | 422 | A 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_unavailable | 422 | A 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_required | 422 | currency 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_allowed | 422 | The 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_outstanding | 422 | amount_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_expected | 422 | wht_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_mismatch | 422 | Second 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_field | 422 | A 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_unknown | 422 | account_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.
| Code | Meaning |
|---|
disbursement_marked_up | A 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_self | A 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_ignored | Your company is not VAT-registered, so a line's vat_category was recorded as none. |
zero_rated_review | A line uses vat_category: zero. SLF checks that the conditions for the 0% rate are met. |
vat_arithmetic_review | The 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_review | The tax withheld on a receipt does not match the wht_rate on the document's lines. The receipt is recorded; SLF reviews it. |
short_payment | A 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_filed | A 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_vat | A 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