# Write readiness

Use optional dryRun checks before supported writes.

Canonical HTML: https://developer.shelfcycle.com/guides/write-readiness

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

The API supports `dryRun=true` on existing notes, contacts, customer, supplier, parent-scoped child location/address, attached document, product, and product-family write endpoints. A dry run checks the exact intended request without creating or updating records.

`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

| Endpoint | Scope | Execution header |
| --- | --- | --- |
| `POST /notes` | `notes:write` | `Idempotency-Key` |
| `PATCH /notes/{id}` | `notes:write` | `If-Match` |
| `POST /contacts` | `contacts:create` | `Idempotency-Key` |
| `PATCH /contacts/{id}` | `contacts:update` | `If-Match` |
| `POST /customers`, `POST /suppliers` | `customers:write` or `suppliers:write` | `Idempotency-Key` |
| `PATCH /customers/{id}`, `PATCH /suppliers/{id}` | `customers:write` or `suppliers:write` | `If-Match` |
| `POST /customers/{id}/locations`, `POST /customers/{id}/locations/{locationId}/addresses` | `customers:write` | `Idempotency-Key` |
| `PATCH /customers/{id}/locations/{locationId}`, `PATCH /customers/{id}/locations/{locationId}/addresses/{addressId}` | `customers:write` | `If-Match` |
| `POST /customers/{id}/documents` | `customers:write` | `Idempotency-Key` |
| `POST /suppliers/{id}/ship-from-locations` | `suppliers:write` | `Idempotency-Key` |
| `PATCH /suppliers/{id}/ship-from-locations/{locationId}` | `suppliers:write` | `If-Match` |
| `POST /suppliers/{id}/documents` | `suppliers:write` | `Idempotency-Key` |
| `POST /products` | `products:write` | `Idempotency-Key` |
| `PATCH /products/{id}` | `products:write` | `If-Match` |
| `POST /products/{id}/documents` | `products:write` | `Idempotency-Key` |
| `POST /product-families` | `products:write` | `Idempotency-Key` |
| `PATCH /product-families/{id}` | `products:write` | `If-Match` |

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.

```bash
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.

## Response

Dry runs return HTTP 200 with `data.type: "write_readiness"` when the request can be evaluated. Use `data.status`, not HTTP status alone, to decide what happened.

```json
{
  "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 preflight checks available to the API at that moment. Execution is still authoritative and re-runs every invariant.

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

```text
duplicate_candidate_found
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
invalid_variable_qty
order_sensitive_update_blocked
validation_failed
archived_record
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.

POST execution requires `Idempotency-Key`. PATCH execution requires `If-Match`. 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.

> **Guardrail**: Write readiness does not add permissions or broaden write surfaces. Document readiness applies only to link-based document attachment, and never to binary upload, document deletion, broad file export, order mutation, purchase-order mutation, inventory, receiving, payments, AP, AR, GL, or duplicate bypasses.
