Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.arcuserp.com/llms.txt

Use this file to discover all available pages before exploring further.

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:
POST /v1/accounts        # account + addresses + contacts + payment methods + tags
POST /v1/orders          # order + line items + discount + tax + allocation + payment
POST /v1/products        # product + variants + components + pricing tiers + vendors
POST /v1/vendor-bills    # bill + line items (linked to PO + receipt)
POST /v1/journal-entries # entry + balanced lines (two or more)

Anatomy of an atomic-create request

Every atomic-create request follows the same structure:
{
  "parent_field_1": "value",
  "parent_field_2": "value",
  "children": [
    {
      "child_field": "value",
      "grandchildren": [...]
    }
  ]
}
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).
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.
{
  "error": "missing_required_field",
  "code": "missing_required_field",
  "type": "validation_error",
  "hint": "pricing_level_id or account_id is required for each pricing row.",
  "param": "variants[0].pricing[2].pricing_level_id"
}
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

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.
curl -X POST https://api.arcuserp.com/v1/orders \
  -H "Authorization: Bearer $ARCUS_API_KEY" \
  -H "Idempotency-Key: order-import-session-2026-05-12-row-001" \
  -d '{...}'
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.
{
  "data": {
    "id": "prod_abc123",
    "title": "Dome Riser Kit",
    "product_type": "kit",
    "components": [
      { "id": "kc_uuid1", "component_product_id": "prod_stone_uuid", "quantity_per_kit": 2 },
      { "id": "kc_uuid2", "component_product_id": "prod_dome_lid_uuid", "quantity_per_kit": 1 }
    ],
    "pricing": [
      { "id": "pp_uuid1", "pricing_level_id": "pl_default_uuid", "qty_break": 1, "list_price": 49.99 }
    ]
  }
}
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:
POST /v1/accounts/{id}/addresses
POST /v1/products/{id}/pricing
POST /v1/orders/{id}/payments
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

SituationRecommendation
Onboarding a new record with all its children at onceAtomic-create (single POST)
Migrating data from another systemAtomic-create with idempotency key per source ID
Adding one address to an existing accountIncremental add: POST /v1/accounts/{id}/addresses
Adding one pricing tier after a product is liveIncremental add: POST /v1/products/{id}/pricing

Entity isolation

All child rows inherit entity_id from the authenticated API key. You cannot set entity_id in the request body; any attempt returns 403 forbidden_field. This ensures data isolation is structurally enforced at the API layer — one API key, one entity, every time.

See Also