Skip to main content

What atomic-create means

Most ERP APIs require multiple round trips to build a complete record. You create the parent, get the ID, then POST each child separately. If one child call fails midway, you are left with a partial record. Arcus create endpoints accept the entire resource graph in a single POST. Children (addresses, line items, pricing tiers, kit components, contacts) nest directly inside the parent body. The server writes every row in a single database transaction. If anything fails, the whole request rolls back and you receive a structured error with the exact field path that failed. The pattern is the same across all primary resources:
One part of POST /v1/orders sits outside the all-or-nothing rule: an inline payment is charged after the order is committed. See What runs after the commit.

Anatomy of an atomic-create request

Every atomic-create request follows the same structure:
The response returns the full hydrated record. You never need a follow-up GET to see what was created.

Transactional guarantee

Every child is written inside a single BEGIN / COMMIT block. The database constraint rules that apply are:
  1. All-or-nothing: if any child fails validation or insert, the parent row and every already-inserted child are rolled back. You get a complete record or nothing.
  2. Validated upfront: required fields on every child are validated before any write begins. The first validation error stops execution and returns a 422 with the exact param path.
  3. Side effects in-transaction: GL postings, inventory reservations, and activity log entries all write inside the same transaction (or queue atomically post-commit for async paths).
  4. Unknown references stop the request before anything is created: on POST /v1/orders, an account_id that names no account of your entity returns 404 with code: account_not_found, and a shipping_address_id or billing_address_id that names no address of one of your accounts returns 404 with code: address_not_found. An id that belongs to another entity gives the same answer as an id that does not exist, and no order is created.
This means there are no orphan records. If you POST a product with bad pricing data, you do not end up with a product row that you then have to clean up.

Error response: exact param path

When a nested child fails, the param field points to the exact location inside the request body. This makes it possible to display an error next to the right field in your UI without parsing the error message.
The param format is dot-and-bracket notation, matching the shape of the request body:
  • line_items[2].quantity — third line item, quantity field
  • variants[0].pricing[2].pricing_level_id — first variant, third pricing tier, pricing level field
  • addresses[1].country — second address, country field

What runs after the commit

On POST /v1/orders, the order header, line items, discount, tax, and inventory allocation are one transaction. An inline payments[] entry is different: each payment is charged after the order has been committed, because the card or bank charge happens outside the Arcus database.
  • A payment that fails does not roll back the order. The order stays, unpaid, and the 201 response carries an _inline_payment_results array with one entry per payment (its index, and for a failure a code such as payment_method_not_found or payment_failed with a param such as payments[0].payment_method_id).
  • Retry a failed payment with POST /v1/orders/{id}/payments, which is one of the incremental endpoints below.
  • The order confirmation email and automatic coupons also run after the commit. A failure in either never changes the 201 response.

Posting dates on journal entries and vendor bills

POST /v1/journal-entries (the entry_date) and POST /v1/vendor-bills (the effective_date, which defaults to bill_date) post to the ledger on that date, so both check it before anything is written. The date must be inside the entity’s posting window (by default up to 180 days back and none ahead), in an open accounting period. A date outside the window is refused with 422 and nothing is written:
  • backdate_beyond_allowed_range when the date is further back than the window
  • future_beyond_allowed_range when the date is further ahead than the window
The body names the window and whether an override is available. To post outside the window, send backdate_reason (or future_reason for a future date), a string of 4 to 500 characters. The override is granted only when the identity behind the call holds the accounting.close_period permission and an accounting period covers the date; on a journal entry, a reason sent by an identity without it is refused 403; a reason that is too short or not a string is refused 422 with backdate_override_reason_invalid or future_override_reason_invalid. The reason is recorded on the audit trail of the entry or bill. A vendor bill that carries landed-cost lines also returns landed_receipt_warnings, landed_allocation, and landed_duplicate_overridden beside data. A vendor invoice number already on file, or a landed charge that looks like a duplicate, is refused 409 unless you send allow_duplicate_override with a duplicate_override_reason. See Purchase-to-Pay Flow for the full bill workflow.

Idempotency

Every atomic-create endpoint honors the Idempotency-Key header. Generate the key before the first attempt and reuse it on every retry. If Arcus processed the original request but your network failed, replaying the same key returns the original response without writing anything twice.
Keys are scoped per entity and expire after 24 hours.

Hydrated response

The 201 response always includes every child expanded inline. No additional API calls are needed to verify what was created.
Money fields are always returned as JavaScript numbers (float), never as Postgres numeric strings.

Incremental edits still work

Atomic-create does not replace the per-child endpoints. After creating a parent, you can add children incrementally:
These endpoints use the same canonical handlers as atomic-create. The idempotency behavior is additive: if you POST the same child body with the same idempotency key, you get the original child record back without inserting a duplicate.

When to use atomic-create vs incremental add


Entity isolation

All child rows inherit entity_id from the authenticated API key. You cannot set entity_id in the request body; on POST /v1/accounts, any attempt returns 422 with code: forbidden_field. This ensures data isolation is structurally enforced at the API layer — one API key, one entity, every time.

See Also