Skip to main content
Write requests to the DualEntry Public API are validated more strictly than the same records created in the interface, and a retried request creates a second record unless you tell the API it is a retry. This page covers the 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 an Idempotency-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.
Every response to a request carrying the header echoes the key back in an 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.
A client that pushes a batch of records without idempotency keys and then retries the batch posts the whole batch twice. Recovering means archiving the duplicate records one by one, so set the header before you run any bulk load.

How the API responds to a repeated idempotency key

The response to a repeated Idempotency-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 to record_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, or expense_description on 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.
The 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_price on 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, including income_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.
For the full field list on each resource, see the V2 API reference for that record type, such as Create InvoiceV2 record. For status codes and the shape of an error response, see the error handling guide. For end to end integration patterns, see Building a custom integration.
Last modified on August 24, 2026