Idempotency-Key header and the validation rules that most often surprise integration authors.
Send an idempotency key on every write
The DualEntry Public API accepts anIdempotency-Key header on write requests and uses it to collapse retries of the same write into a single result. The header applies to POST, PUT, PATCH, and DELETE on any /public/ path, on both V1 and V2. Send it, and a retry returns the stored response from the first request instead of creating a second record. Omit it, and a client that retries after a timeout, a dropped connection, or an ambiguous gateway error creates a second copy of the record, leaving you to archive the extra records afterwards.
Generate a fresh key per logical write, not per HTTP attempt. A UUID version 4 value is a good default. Reuse the same key across every retry of that one write, and never reuse a key for a different payload.
DualEntry scopes a key to your organization, the HTTP method, and the request path. The same key sent to a different endpoint is treated as a different write, and a key keeps working if you rotate your API key partway through a retry sequence. Results stay replayable for 48 hours, after which the key expires and a repeat request runs as a new write.
The example below posts a journal entry with an idempotency key so the request can be retried safely.
Idempotency-Key response header. A replayed response also carries Idempotency-Replayed: true, which is the only reliable way to tell a replay from a fresh write.
How the API responds to a repeated idempotency key
The response to a repeatedIdempotency-Key depends on the state of the first request, so a client that retries needs to handle more than a straight replay. The table below lists every outcome the DualEntry Public API returns for a key it has already seen.
Treat the
409 for an in-flight request as “retry shortly,” not as a failure. A client that retries immediately after a timeout is the case most likely to hit it, because the first request is often still running.
DualEntry only stores a response that represents a decision about your write. A 5xx, 401, 403, 408, or 429 response releases the key instead, so retrying with the same key runs the write again rather than replaying the failure.
Fields the API requires on posted records
A create request defaults torecord_status: "posted", and a posted record must carry every field its record type requires. When one is missing, the API answers with Required for posted records: followed by the field names, for example Required for posted records: due_date, exchange_rate.
The table below lists the fields the DualEntry Public API enforces on posted invoices, bills, and journal entries.
Bills resolve
due_date differently from invoices. It is not part of the posted-record check, but a bill cannot be stored without one. Send due_date directly, or send a term_id and let DualEntry calculate the date from that term. If you send neither, DualEntry falls back to the vendor’s default term, and then to the term on a linked purchase order. A bill with no due date and no term to derive one from fails on save with a different error, and this applies to draft bills as well as posted ones.
Create the record with record_status: "draft" when you do not have all of these values yet. A draft skips both the required-field check and the line check, and receives defaults instead: an empty memo, an exchange_rate of 1.0, and USD as the currency. Patch the record and post it once the missing values arrive.
Line item validation rules
Lines carry their own required fields, and those apply to drafts as well as posted records, so a draft does not let you defer them the way it defers record level fields. Which fields a line needs depends on the kind of line, and the three record types do not share one shape. The table below lists the fields required on each kind of line in the DualEntry Public API.
Two rules account for most line level failures on a first integration.
- The text field has no default. A line that omits
memo, orexpense_descriptionon a bill expense line, fails schema validation even when the parent record is a draft. An empty string is accepted, so send""when there is nothing to record against the line. - Send a quantity greater than zero. On the line types that carry
quantity, the field is constrained to values above zero. A zero quantity line is not a way to carry a description only line or a placeholder, so remove the line instead of zeroing it.
id field on a line is optional. Send it to update an existing line on a PUT, and omit it to create a new one. Classifications and an intercompany company_id are optional on every line type that supports them.
Fields the API does not expose
Some fields and actions available in the DualEntry interface have no Public API equivalent, so an integration cannot round-trip them. Check this section before you design a sync that assumes the two surfaces are at parity:- Item pricing: The
default_standalone_selling_priceon an item cannot be read, set, or updated through the Public API, on either V1 or V2. The item schemas do expose the accounting configuration needed for posting, includingincome_account_id,expense_account_id,deferral_account_id,asset_account_id,product_tax_code_id, and the revenue recognition defaults, but they carry no price field. Maintain item prices in the interface. - Contract editing on V1: The V1 contract endpoints cover creating and reading a contract, plus reading and upserting obligation usage. They do not update a contract or expose its change orders. V2 additionally supports updating a contract and managing its change orders, so check the version you are calling before you plan around this.

