Write readiness

Use optional non-mutating dryRun checks before supported POST, PATCH, PUT, and DELETE writes.

The API supports dryRun=true on Chart of Accounts, Shipping Options, tags and tag assignments, representatives, payment terms, notes, contacts, customer, supplier, warehouse, reorder-point, parent-scoped child location/address, archive and restore, attached document, product, product-family, Product SDS, Cost Book, Current Source, and Price Book write endpoints. A dry run checks the exact intended request without creating or updating records.

Certificate filing and renewal, lot COA assignment, and supplier carrier profiles also support dryRun=true. Use the same intended request and the endpoint's current-record timestamp or COA version when required.

dryRun=true is optional. Authorized callers may execute directly when their integration policy allows it. ShelfCycle does not require a human approval step before supported public API writes.

Supported endpoints

EndpointScopeExecution header
POST /certification-recordscertifications:write; warehouse subjects also require Warehouse read and write accessNone
PATCH /certification-records/{recordId}certifications:write; warehouse subjects also require Warehouse read and write accessIf-Match using the record's updatedAt for a changed request
PUT /lots/{id}/coalots:writeIf-Match using the returned COA version for replacement; omit on first assignment
POST /reference/ledger-accountscompany-reference:writeIdempotency-Key when externalId is absent
PATCH /reference/ledger-accounts/{id}company-reference:writeIf-Match
POST /shipping-optionsshipping-options:writeIdempotency-Key for a new option
PATCH /shipping-options/{id}, POST /shipping-options/{id}/archive, or /unarchiveshipping-options:writeIf-Match
POST /warehouseswarehouses:writeIdempotency-Key when externalId is absent
PATCH /warehouses/{id}, POST /warehouses/{id}/archive, or /unarchivewarehouses:writeIf-Match
POST /reorder-pointsreorder:writeNone
POST /tags, PATCH /tags/{id}, and POST or DELETE /tag-assignmentstags:writeNone
POST /representatives, representative PATCH, archive, or unarchivecompany-reference:writeIdempotency-Key for create; If-Match for update, archive, or restore
POST /payment-terms, PATCH /payment-terms/{id}company-reference:writeIdempotency-Key for create; If-Match for update
POST /notesnotes:writeIdempotency-Key
PATCH /notes/{id}notes:writeIf-Match
POST /contactscontacts:createIdempotency-Key
PATCH /contacts/{id}contacts:updateIf-Match
POST /contacts/{id}/archive or /unarchivecontacts:updateIf-Match
POST /customers, POST /supplierscustomers:write or suppliers:writeIdempotency-Key
PATCH /customers/{id}, PATCH /suppliers/{id}customers:write or suppliers:writeIf-Match
POST /customers/{id}/archive or /unarchive, POST /suppliers/{id}/archive or /unarchivecustomers:write or suppliers:writeIf-Match
POST /customers/{id}/locations, POST /customers/{id}/locations/{locationId}/addressescustomers:writeIdempotency-Key
PATCH /customers/{id}/locations/{locationId}, PATCH /customers/{id}/locations/{locationId}/addresses/{addressId}customers:writeIf-Match
Customer-address archive or unarchive actioncustomers:writeIf-Match
POST /customers/{id}/documentscustomers:writeIdempotency-Key
POST /suppliers/{id}/ship-from-locationssuppliers:writeIdempotency-Key
PATCH /suppliers/{id}/ship-from-locations/{locationId}suppliers:writeIf-Match
Supplier ship-from archive or unarchive actionsuppliers:writeIf-Match
POST /suppliers/{id}/documentssuppliers:writeIdempotency-Key
POST /productsproducts:writeIdempotency-Key
PATCH /products/{id}products:writeIf-Match
POST /products/{id}/documentsproducts:writeIdempotency-Key
PUT /products/{id}/sdsproducts:writeIf-Match
POST /product-familiesproducts:writeIdempotency-Key
PATCH /product-families/{id}products:writeIf-Match
POST /cost-book-entriescost-book-entries:writeIdempotency-Key
PATCH /cost-book-entries/{id}cost-book-entries:writeIdempotency-Key
DELETE /cost-book-entries/{id}cost-book-entries:writeIdempotency-Key
POST /cost-book-entries/batchcost-book-entries:writeIdempotency-Key
PUT /products/{id}/current-cost-sourcecurrent-cost-sources:writeexpectedCurrentSupplierId in the body
POST /price-book-entriesprice-book-entries:writeIdempotency-Key

Dry runs use the same write scope, live user permission, and rate-limit family as execution. They do not introduce a separate permission model.

Request

Send the same method, endpoint, headers, and body you plan to execute, with dryRun=true in the query string.

curl "$SHELFCYCLE_API_BASE_URL/contacts?dryRun=true" \
  -X POST \
  -H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: gmail-thread-123:customer-id:create-contact:jane@example.com" \
  -d '{
    "parent": { "type": "customer", "id": "customer-id" },
    "name": "Jane Buyer",
    "title": "Purchasing",
    "email": "jane@example.com",
    "phone": "555-0100"
  }'

dryRun must be exactly true. Invalid values such as dryRun=false, dryRun=1, or repeated dryRun parameters fail validation and must not be retried as execution.

Price Book uses a review-and-apply sequence. Its dry run returns expectedAffectedEntryIds and expectedPlanToken; include both in the later execution while keeping the same stable Idempotency-Key. See the Price Book guide for the complete request shape.

Current Source uses a read-preview-apply sequence. Read GET /products/{id}/current-cost-source, preview the desired supplierId and shipFromLocationId with the observed expectedCurrentSupplierId and expectedCurrentShipFromLocationId, then send the same PUT without dryRun=true. The preview returns the desired and previous source, consequence, eligibility, and a verification path. See the Cost Book guide for set, replace, and clear examples.

Cost Book updates, corrections, and batches use the same preview-and-apply pattern. Preview the exact POST or PATCH with dryRun=true, review every readiness check and predicted entry, then execute the identical request with the same stable Idempotency-Key. Batch preview validates the whole envelope and every item without saving results.

Archive and restore actions use a current-record preview-and-apply pattern. Preview the selected action without If-Match to receive impactAnalysis.currentUpdatedAt, review the target state and any customer or supplier safeguards, then execute with that timestamp in If-Match. See Archive and restore for the complete flow.

Shipping Options use the same preview-and-apply pattern. Create with a stable Idempotency-Key; update, archive, or restore with the selected option's latest updatedAt value in If-Match. See Shipping Options for complete examples.

Chart of Accounts uses the same pattern. Create with a stable Idempotency-Key when no source ID is available; update with the selected account's latest updatedAt value in If-Match. See Chart of Accounts for complete examples.

Create a separate source record

Customer, supplier, contact, combined customer-location, customer-address, and supplier ship-from creates accept optional top-level duplicateResolution: "create_distinct". Use it only when a reviewable similarity represents a separate legitimate source record. Without it, similarity candidates keep the write blocked for review.

The option does not change exact source-ID, archived-record, hidden-candidate, address-role, reference, or customer-location-name conflicts. Dry run is optional; execution checks the current records again.

Response

Most dry runs return HTTP 200 with data.type: "write_readiness" when the request can be evaluated. Current Source returns its focused status, consequence, eligibility, and verification fields. Use response data, not HTTP status alone, to decide what happened.

{
  "data": {
    "type": "write_readiness",
    "operation": "contacts.create",
    "status": "ready",
    "wouldWrite": false,
    "checks": [
      { "code": "scope_authorized", "status": "passed", "message": "The key can create contacts." },
      { "code": "parent_available", "status": "passed", "message": "The parent is visible and active." },
      { "code": "duplicate_check_clear", "status": "passed", "message": "No duplicate contact candidate was found." },
      { "code": "idempotency_available", "status": "passed", "message": "The idempotency key is available." }
    ],
    "duplicateCandidates": [],
    "idempotency": { "required": true, "keyProvided": true, "wouldReplay": false },
    "submit": { "method": "POST", "path": "/api/v1/contacts", "query": {} },
    "verification": {
      "available": true,
      "method": "GET",
      "path": null,
      "pathTemplate": "/api/v1/contacts/{id}",
      "requiresScopes": ["contacts:read"],
      "fallback": "detail_get"
    },
    "requestId": "request-id"
  }
}

wouldWrite is always false for dry runs. A dry run must not create business rows, update updatedAt, create idempotency identities, write embeddings, acquire mutation locks, or trigger downstream side effects.

Ready and blocked

status: "ready" means the request passed the available checks at that moment. Execution confirms the request again against current ShelfCycle data.

status: "blocked" means the API found a condition your workflow must handle before execution. Common blocked checks include:

duplicate_candidate_found
duplicate_candidate_overflow
duplicate_reference_identity
idempotency_key_required
idempotency_key_reused
stale_record
target_not_found_or_not_visible
unsupported_profile_shape
unsupported_linked_record
not_api_created
primary_change_unsupported
address_role_required
product_family_reference_active
product_packaging_reference_active
supplier_reference_active
reference_visibility_blocked
payment_terms_reference_exists
sales_person_reference_active
customer_service_representative_reference_active
supplier_representative_reference_active
ledger_account_reference_valid
cost_ship_from_location_not_visible
cost_book_base_ambiguous
invalid_variable_qty
order_sensitive_update_blocked
validation_failed
archived_record
archive_blocked
duplicate_document

Duplicate candidates are advisory and bounded. If the key cannot read the matching detail route, the candidate can be redacted to readable: false with only type and matchReasons.

Execute and verify

To execute, remove dryRun=true and send the same request when your integration policy allows it.

Use each endpoint's documented execution headers. Many POST creates require Idempotency-Key; certificate filing instead recognizes identical saved dates and documents. Most PATCH endpoints and Product SDS execution require If-Match; certificate PATCH uses the record's updatedAt for a change, while lot COA replacement uses its returned version. Cost Book PATCH requires Idempotency-Key and expectedCurrentEntryId. Current Source PUT requires the supplier and ship-from location observed before the change as expectedCurrentSupplierId and expectedCurrentShipFromLocationId. Execution can still return 409 if another write changes state after the dry run.

After execution, use verification.path when the response provides one and the key has the required scope. If verification is not available, use the execute response as readback. Price Book execution returns the complete saved entry and ready links.self and links.history paths.

Guardrail

A readiness response never grants extra access. Use the exact record-specific scope for an attachment or SDS assignment, and verify the result through the selected record's read route.