{"openapi":"3.1.0","info":{"title":"IMS API","version":"1.0.0","description":"REST access to your inventory, catalog, orders and stock ledger.\n\n## Authentication\n\n`Authorization: Bearer sk_live_…`. Create a key in the Inventory app under\n**Admin → API keys**; it is shown once and stored only as a hash. A key carries\nexplicit scopes, may be restricted to a single location, and expires within a\nyear.\n\nAPI access requires a plan that includes it — see your plan in the app under\n**Settings → Billing**. A request from an organization without it returns\n`403 api_not_in_plan` naming the plans that qualify.\n\n## Conventions worth reading before you start\n\n**Money and quantities are strings, not JSON numbers.** `\"19.99\"`, not\n`19.99`. Prices are stored as exact decimals; sending them as JSON numbers\nwould hand you an IEEE-754 double and your totals would drift a fraction of a\ncent away from the invoice. Parse them with a decimal library.\n\n**Stock lives on the variant, never on the product.** Every product has at\nleast one variant — a simple product has a single default variant with no\nattributes. Product-level `onHand` and `stockValue` are aggregates across\nvariants.\n\n**A variant's `priceOverride` may be null**, meaning it inherits the product's\nprice. `sellingPrice` is the resolved figure; use that unless you specifically\nneed to know whether an override exists.\n\n**The ledger is the source of truth.** `/v1/stock/movements` is append-only —\nrows are never updated or deleted — so it is safe to treat as a change feed.\n`/v1/stock` is a maintained cache of the same information.\n\n**Purchase order lines are in purchase units.** A line for 10 with\n`unitsPerPurchase` 6 is ten cases of six. Both the purchase-unit and\nstock-unit figures are returned.\n\n**Sales order lines carry `qtyFulfilled`.** `qty − qtyFulfilled` is still\nowed; an order in `PARTIAL` has shipped some and owes the rest. Recognise\nrevenue on the fulfilled portion.\n\n## Pagination\n\nCursor-based. Read `pagination.nextCursor` and pass it back as `?cursor=`.\nCursors are opaque and signed — do not construct, decode or modify them. There\nis no offset paging and no total count: rows are inserted continuously, so an\noffset would duplicate and skip rows, and counting a ledger on every page is\nnot something worth doing to a live database.\n\n## Rate limits\n\nTwo allowances apply together: a small fast-refilling **burst** and a larger\n**sustained** one. Reports and exports draw on a separate **heavy** allowance.\n`RateLimit-Remaining` reports whichever is tightest. On 429, honour\n`Retry-After`.\n\n## Errors\n\n`application/problem+json` per RFC 9457. Switch on the stable `code` field.\nThe full vocabulary is at `GET /v1/meta/errors`."},"servers":[{"url":"https://api.stockroom-ims.com"}],"tags":[{"name":"Meta","description":"Health, versioning, and key introspection."},{"name":"Catalog","description":"Products, variants, categories."},{"name":"Reference","description":"Locations, suppliers, customers."},{"name":"Stock","description":"On-hand levels and the movement ledger."},{"name":"Sales","description":"Sales orders and payments."},{"name":"Reports","description":"Financial, tax and inventory reporting."},{"name":"Purchasing","description":"Purchase orders and receiving."},{"name":"Webhooks","description":"Event subscriptions and delivery history."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key. Available scopes: dashboard:read, inventory:read, inventory:write, transfers:read, transfers:write, serials:read, serials:write, customers:read, customers:write, sales:read, sales:write, purchasing:read, purchasing:write, audits:read, audits:write, reports:read, admin:read, admin:write, webhooks:read, webhooks:write, keys:read, discount:override, discount:override_max."}},"schemas":{"Problem":{"type":"object","description":"RFC 9457 problem detail. Switch on `code`, never on `title` or `status`.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string","description":"Human-readable summary. Prose — may be reworded."},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string","enum":["invalid_request","invalid_sort_field","invalid_cursor","missing_token","malformed_token","invalid_token","key_revoked","key_expired","insufficient_scope","location_scope_violation","location_scoped_key_unsupported","feature_not_in_plan","api_not_in_plan","subscription_inactive","plan_limit_exceeded","not_found","conflict","invalid_reference","invalid_state_transition","invalid_serial_transition","discount_exceeds_maximum","insufficient_stock","already_fulfilled","order_cancelled","payment_exceeds_balance","idempotency_key_reused","idempotency_in_progress","rate_limited","payload_too_large","unsupported_media_type","method_not_allowed","service_disabled","service_unavailable","internal_error"],"description":"Stable machine-readable code."},"requestId":{"type":"string","description":"Quote this to support."},"errors":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["type","title","status","code"]},"Pagination":{"type":"object","properties":{"nextCursor":{"type":["string","null"],"description":"Pass verbatim as `?cursor=`. Opaque and signed — do not construct or modify."},"hasMore":{"type":"boolean"},"limit":{"type":"integer"}}}}},"security":[{"bearerAuth":[]}],"paths":{"/healthz":{"get":{"summary":"Liveness","description":"Does not touch the database. Unauthenticated.","tags":["Meta"],"security":[],"responses":{"200":{"description":"The process is up."}}}},"/readyz":{"get":{"summary":"Readiness","description":"Checks the database and that the `api` schema is visible. Unauthenticated.","tags":["Meta"],"security":[],"responses":{"200":{"description":"Ready to serve."},"503":{"description":"Not ready."}}}},"/v1/meta":{"get":{"summary":"Version and compatibility policy","tags":["Meta"],"security":[],"responses":{"200":{"description":"Version information."}}}},"/v1/meta/errors":{"get":{"summary":"The stable error-code vocabulary","tags":["Meta"],"security":[],"responses":{"200":{"description":"All error codes."}}}},"/v1/me":{"get":{"summary":"Describe the calling key","description":"Organization, plan, features, scopes and location restriction. The first call to make, and the first thing support will ask for.","tags":["Meta"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Key and organization context."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/products":{"get":{"summary":"List products","description":"Active products by default. `onHand`, `stockValue` and `averageCost` are aggregated across the product's variants; `averageCost` is the blended cost (value ÷ on-hand), matching the figure shown in the app.","tags":["Catalog"],"security":[{"bearerAuth":["inventory:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Free-text search."},{"name":"locationId","in":"query","schema":{"type":"string"},"description":"Restrict to one location. A location-scoped key may only name its own; naming another returns 403."},{"name":"categoryId","in":"query","schema":{"type":"string"}},{"name":"supplierId","in":"query","schema":{"type":"string"}},{"name":"isActive","in":"query","schema":{"type":"boolean"},"description":"Defaults to true. Pass false for archived products."},{"name":"lowStock","in":"query","schema":{"type":"boolean"},"description":"Only products at or below their reorder point. Applied after the page is read, so a filtered page may hold fewer than `limit` rows."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated sparse fieldset, e.g. `sku,name,onHand`."}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/products/{id}":{"get":{"summary":"Retrieve a product","description":"Includes every variant with its per-location stock, reorder settings, and resolved price.","tags":["Catalog"],"security":[{"bearerAuth":["inventory:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"locationId","in":"query","schema":{"type":"string"},"description":"Restrict to one location. A location-scoped key may only name its own; naming another returns 403."},{"name":"include","in":"query","schema":{"type":"string"},"description":"Comma-separated: `images`."}],"responses":{"200":{"description":"The product and its variants."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such product in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/variants":{"get":{"summary":"List variants","description":"Stock-tracked SKUs across all products. This is the level stock is held at.","tags":["Catalog"],"security":[{"bearerAuth":["inventory:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Free-text search."},{"name":"locationId","in":"query","schema":{"type":"string"},"description":"Restrict to one location. A location-scoped key may only name its own; naming another returns 403."},{"name":"productId","in":"query","schema":{"type":"string"}},{"name":"isActive","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/categories":{"get":{"summary":"List categories","description":"Includes each category's field definitions. AXIS fields multiply into stock-tracked variants; ATTRIBUTE fields are informational.","tags":["Catalog"],"security":[{"bearerAuth":["inventory:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/locations":{"get":{"summary":"List locations","description":"Not paginated — locations are few. A location-scoped key sees only its own.","tags":["Reference"],"security":[{"bearerAuth":["inventory:read"]}],"responses":{"200":{"description":"Locations with tax settings."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/suppliers":{"post":{"summary":"Create a supplier","tags":["Reference"],"security":[{"bearerAuth":["purchasing:write"]}],"parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string","maxLength":255},"description":"Makes a retry safe. A timed-out request tells you nothing about whether it happened; resending with the same key returns the original response instead of doing the work twice. Same key and same body replays (with `Idempotent-Replay: true`). Same key with a different body is a 422. Still in flight is a 409. Keys expire after 24 hours."}],"responses":{"201":{"description":"The supplier."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"summary":"List suppliers","description":"Vendors that purchase orders are raised against.","tags":["Reference"],"security":[{"bearerAuth":["purchasing:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Free-text search."}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/customers":{"post":{"summary":"Create a customer","description":"Always created in the calling key's organization. `organizationId` is not a field a client can supply.","tags":["Reference"],"security":[{"bearerAuth":["customers:write"]}],"parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string","maxLength":255},"description":"Makes a retry safe. A timed-out request tells you nothing about whether it happened; resending with the same key returns the original response instead of doing the work twice. Same key and same body replays (with `Idempotent-Replay: true`). Same key with a different body is a 422. Still in flight is a 409. Keys expire after 24 hours."}],"responses":{"201":{"description":"The customer."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"summary":"List customers","description":"Searchable by name, email or phone.","tags":["Reference"],"security":[{"bearerAuth":["customers:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Free-text search."},{"name":"taxExempt","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/customers/{id}":{"patch":{"summary":"Update a customer","description":"Partial update. Another organization's id returns 404 and changes nothing.","tags":["Reference"],"security":[{"bearerAuth":["customers:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The updated customer."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"summary":"Retrieve a customer","tags":["Reference"],"security":[{"bearerAuth":["customers:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The customer."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/suppliers/{id}":{"patch":{"summary":"Update a supplier","tags":["Reference"],"security":[{"bearerAuth":["purchasing:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The updated supplier."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/stock":{"get":{"summary":"List stock levels","description":"On-hand per variant per location. A maintained cache of the movement ledger — to reconcile exactly, sum `/v1/stock/movements`.","tags":["Stock"],"security":[{"bearerAuth":["inventory:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"locationId","in":"query","schema":{"type":"string"},"description":"Restrict to one location. A location-scoped key may only name its own; naming another returns 403."},{"name":"variantId","in":"query","schema":{"type":"string"}},{"name":"productId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/stock/movements":{"get":{"summary":"The stock ledger","description":"Append-only and never modified, so it is safe to treat as a change feed: poll with `?since=` and a cursor. `quantity` is signed — positive inbound, negative outbound. Draws on the heavy rate-limit allowance.","tags":["Stock"],"security":[{"bearerAuth":["inventory:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"locationId","in":"query","schema":{"type":"string"},"description":"Restrict to one location. A location-scoped key may only name its own; naming another returns 403."},{"name":"variantId","in":"query","schema":{"type":"string"}},{"name":"productId","in":"query","schema":{"type":"string"}},{"name":"type","in":"query","schema":{"type":"string","enum":["INITIAL_COUNT","PURCHASE_RECEIPT","SALE","ADJUSTMENT","AUDIT","TRANSFER_IN","TRANSFER_OUT","TRANSFER_SHRINK","RETURN_IN","RETURN_OUT"]}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/sales-orders":{"get":{"summary":"List sales orders","description":"Includes payment totals and balance due. A location-scoped key sees only that location's orders.","tags":["Sales"],"security":[{"bearerAuth":["sales:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"status","in":"query","schema":{"type":"string","enum":["DRAFT","CONFIRMED","PARTIAL","FULFILLED","CANCELLED","REFUNDED"]}},{"name":"customerId","in":"query","schema":{"type":"string"}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/sales-orders/{id}":{"get":{"summary":"Retrieve a sales order","description":"Full lines with per-line discount and cost of goods, payments (negative amounts are refunds), and any open backorders.","tags":["Sales"],"security":[{"bearerAuth":["sales:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The order."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/purchase-orders":{"get":{"summary":"List purchase orders","description":"`DRAFT` is shown as “Requested” in the app; the API reports the stored value.","tags":["Purchasing"],"security":[{"bearerAuth":["purchasing:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"status","in":"query","schema":{"type":"string","enum":["DRAFT","ORDERED","PARTIAL","RECEIVED","CANCELLED"]}},{"name":"supplierId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/availability":{"get":{"summary":"Available to promise","description":"What can still be sold, as opposed to what is in the building.\n\n**Reservations are derived, not stored.** A reservation is the units still\noutstanding (`qty − qtyFulfilled`) on an order in `CONFIRMED` or `PARTIAL`.\nA `DRAFT` order is a quote and holds nothing.\n\n**`available` can be negative, and that is the point.** It means more units are\npromised than exist — the item is backordered — and the magnitude is how far\nshort you are. It is deliberately not clamped to zero.\n\n**Kits hold no stock.** A kit's `onHand` is always 0; its `available` is\nbounded by its scarcest component after that component's own reservations, and\nthe per-component breakdown shows which one binds.\n\nOn-hand narrows to a location-scoped key's location, but reserved and available\nare always organization-wide — orders are not apportioned across locations, so a\nper-location \"available\" would compare one site's stock against every site's\npromises. `meta` states which scope each figure used.","tags":["Stock"],"security":[{"bearerAuth":["inventory:read"]}],"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Free-text search."},{"name":"variantId","in":"query","schema":{"type":"string"},"description":"Comma-separated variant ids."},{"name":"productId","in":"query","schema":{"type":"string"}},{"name":"backordered","in":"query","schema":{"type":"boolean"},"description":"Only items where available < 0."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Availability per variant."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Scope missing, or the plan does not include `reservations`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/lookup/{code}":{"get":{"summary":"Resolve any code","description":"One endpoint for anything a barcode scanner might emit. Resolves a SKU, barcode,\nserial number, order number or carrier tracking number without the caller having\nto know which it is.\n\nMatching is case-insensitive and every match is returned — a code that resolves\nto more than one kind of thing is a real situation, and silently picking one\nwould be worse than saying so. `best` is the most specific match, in the order\nvariant, product, serial, sales order, purchase order, shipment.\n\nTracking numbers are matched in BOTH directions. A `shipment` match carries\n`direction`: `inbound` for a supplier delivery, with a `purchaseOrder`, or\n`outbound` for a parcel sent to a customer, with a `salesOrder`.","tags":["Stock"],"security":[{"bearerAuth":["inventory:read"]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","maxLength":128}}],"responses":{"200":{"description":"Matches, best first. `matchCount` 0 means nothing matched."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/variants/{id}/components":{"get":{"summary":"Kit bill of materials","description":"The components a kit is assembled from, how many complete kits current stock allows, and the rolled-up unit cost recorded as COGS on sale. `isKit: false` means an ordinary stocked variant.","tags":["Catalog"],"security":[{"bearerAuth":["inventory:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The bill of materials."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/backorders":{"get":{"summary":"List backorders","description":"Units an order was short on AT FULFILMENT — a record of something that already happened, distinct from the forward-looking `backordered` flag on /v1/availability. Defaults to those still owed (OPEN, ORDERED, RECEIVED). Includes the supplier to reorder from and the covering purchase order once raised.","tags":["Sales"],"security":[{"bearerAuth":["sales:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"status","in":"query","schema":{"type":"string","enum":["OPEN","ORDERED","RECEIVED","FULFILLED","CANCELLED"]}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/serials":{"get":{"summary":"List serial units","description":"One row per physical unit for serial-tracked products. The aggregate ledger is unaffected — serialised items still post ordinary quantity movements; serial identity lives alongside.","tags":["Stock"],"security":[{"bearerAuth":["serials:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"locationId","in":"query","schema":{"type":"string"},"description":"Restrict to one location. A location-scoped key may only name its own; naming another returns 403."},{"name":"variantId","in":"query","schema":{"type":"string"}},{"name":"serialNumber","in":"query","schema":{"type":"string"},"description":"Partial match."},{"name":"status","in":"query","schema":{"type":"string","enum":["IN_STOCK","RESERVED","IN_TRANSIT","SOLD","RETURNED_TO_SUPPLIER","IN_REPAIR","SCRAPPED","LOST"]}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/serials/{id}":{"get":{"summary":"A unit's chain of custody","description":"Every event for one physical unit, oldest first — received, reserved, sold, transferred, returned. Append-only.","tags":["Stock"],"security":[{"bearerAuth":["serials:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The unit and its history."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/transfers":{"get":{"summary":"List inter-location transfers","description":"**Stock moves on receipt, not despatch.** Units between shipping and receiving are still counted at the source; `inTransit` reports them. A client reconciling stock mid-transfer that assumes otherwise will find quantities it cannot explain.","tags":["Stock"],"security":[{"bearerAuth":["transfers:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"status","in":"query","schema":{"type":"string","enum":["PENDING","SHIPPED","PARTIAL","FULFILLED","DENIED"]}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/audits":{"get":{"summary":"List stock counts","description":"Physical count sessions, newest first.","tags":["Stock"],"security":[{"bearerAuth":["audits:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rows per page (1–200)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` from a previous response."},{"name":"status","in":"query","schema":{"type":"string","enum":["OPEN","COMPLETED","CANCELLED"]}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/audits/{id}":{"get":{"summary":"A count with its lines and variance","description":"`systemQty` is the snapshot taken when the count started, not live stock. `counted` is separate from `countedQty` on purpose: a count of zero means “we looked and there were none”, which is not the same as “not counted yet”.","tags":["Stock"],"security":[{"bearerAuth":["audits:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The count, its lines and shrink/overage metrics."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/stock/adjustments":{"post":{"summary":"Adjust stock","description":"Posts a movement to the stock ledger. This is the ONLY way stock changes: on-hand is a cache derived from the ledger, and writing to it directly would leave the two disagreeing with no way to tell which is right.\n\n`quantity` is SIGNED - positive adds, negative removes. `unitCost` applies only to an inbound movement and blends into the variant's weighted-average cost; an outbound movement never changes it, because selling a widget does not alter what the remaining widgets cost.\n\nSerial-tracked items are refused: their quantity is the count of individually tracked units, and changing the aggregate without saying WHICH units would desync the two permanently. Receive, sell, transfer or return specific units instead.\n\nThe response reports on-hand and average cost AFTER the movement.","tags":["Stock"],"security":[{"bearerAuth":["inventory:write"]}],"parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string","maxLength":255},"description":"Makes a retry safe. A timed-out request tells you nothing about whether it happened; resending with the same key returns the original response instead of doing the work twice. Same key and same body replays (with `Idempotent-Replay: true`). Same key with a different body is a 422. Still in flight is a 409. Keys expire after 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["variantId","quantity"],"properties":{"variantId":{"type":"string"},"quantity":{"type":"number","description":"Signed. Must not be zero."},"locationId":{"type":"string","description":"Defaults to the organization's default location."},"unitCost":{"type":"number","minimum":0,"description":"Inbound only."},"reasonId":{"type":"string"},"note":{"type":"string","maxLength":500}}}}}},"responses":{"201":{"description":"The movement, with resulting on-hand and average cost."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/sales-orders/{id}/payments":{"post":{"summary":"Record a payment","description":"Payment and fulfilment are independent: recording money does not ship goods, and shipping goods does not collect money.\n\nThe balance is re-read inside the transaction that writes the payment, so two concurrent payments cannot both see the same remaining amount and overpay the order. A payment exceeding the balance is refused rather than clamped.","tags":["Sales"],"security":[{"bearerAuth":["sales:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","schema":{"type":"string","maxLength":255},"description":"Makes a retry safe. A timed-out request tells you nothing about whether it happened; resending with the same key returns the original response instead of doing the work twice. Same key and same body replays (with `Idempotent-Replay: true`). Same key with a different body is a 422. Still in flight is a 409. Keys expire after 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount"],"properties":{"amount":{"type":"number","exclusiveMinimum":0},"method":{"type":"string","maxLength":50},"note":{"type":"string","maxLength":500}}}}}},"responses":{"201":{"description":"The payment and the order's new balance."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/reports/profit-and-loss":{"get":{"summary":"Profit and loss","description":"Revenue, cost of goods and gross profit for a period.\n\n**Recognition follows the fulfilled portion of orders.** A `PARTIAL` order\ncontributes only the units that have actually shipped — those have left\ninventory and had their cost posted to the ledger. The unshipped remainder is a\npromise, not a sale, and including it would produce a profit figure that does\nnot reconcile against stock.\n\nAn order-level discount is recognised in proportion to the fulfilled value, so\nit telescopes to the full discount as the order completes.\n\n**Operating expenses are not tracked**, so this is GROSS profit.\n\nNot available to a location-scoped key: the report has no location dimension,\nso answering would disclose organization-wide figures.","tags":["Reports"],"security":[{"bearerAuth":["reports:read"]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Start of the period. Defaults to 30 days ago."},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"End of the period. Defaults to now. Ranges are capped at 800 days."}],"responses":{"200":{"description":"The period summary."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Scope missing, or the key is restricted to a location.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/reports/best-sellers":{"get":{"summary":"Best-selling products","description":"Ranked by recognised revenue. An order-level discount is allocated across products on the same basis the P&L uses, so the two reports reconcile. Supports `Accept: text/csv`.","tags":["Reports"],"security":[{"bearerAuth":["reports:read"]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Start of the period. Defaults to 30 days ago."},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"End of the period. Defaults to now. Ranges are capped at 800 days."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":10}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]},"description":"Or send `Accept: text/csv`. CSV is returned as an attachment."}],"responses":{"200":{"description":"Ranked products."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/reports/inventory-valuation":{"get":{"summary":"Inventory valuation","description":"Current stock at weighted-average cost, by category. Pass `asOf` for a historical book value, reconstructed from the ledger — the only thing that knows what stock was worth on a past date, since the on-hand cache only knows about now. Supports `Accept: text/csv`.","tags":["Reports"],"security":[{"bearerAuth":["reports:read"]}],"parameters":[{"name":"asOf","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]},"description":"Or send `Accept: text/csv`. CSV is returned as an attachment."}],"responses":{"200":{"description":"Valuation."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/reports/sales-tax":{"get":{"summary":"Sales tax collected","description":"Tax collected in a period, overall and by location.\n\nThe taxable base is reconstructed as `total − tax`, which holds for both\ntax-inclusive and tax-exclusive pricing — the same organization can have both,\nsince inclusiveness resolves as an OR across org, location and product.\n\nTax collected is a **liability owed to the taxing authority**, not income.\nSales tax was captured going forward only; orders predating that read as zero.\n\nUnlike the P&L, this report has a location dimension, so a location-scoped key\ngets its own figures rather than a refusal. Supports `Accept: text/csv`.","tags":["Reports"],"security":[{"bearerAuth":["reports:read"]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Start of the period. Defaults to 30 days ago."},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"End of the period. Defaults to now. Ranges are capped at 800 days."},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]},"description":"Or send `Accept: text/csv`. CSV is returned as an attachment."}],"responses":{"200":{"description":"Tax summary."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/reports/income":{"get":{"summary":"Income summary","description":"The P&L plus stock purchased at landed cost, cash in by payment method, and the liabilities (sales tax owed, outstanding gift-card balance) that are NOT income. Intended as the figure set handed to an accountant.","tags":["Reports"],"security":[{"bearerAuth":["reports:read"]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Start of the period. Defaults to 30 days ago."},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"End of the period. Defaults to now. Ranges are capped at 800 days."}],"responses":{"200":{"description":"Income summary."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/reports/expiring-lots":{"get":{"summary":"Expiring and expired lots","description":"Lots RECEIVED with an expiry date within the window. This is a traceability and recall view, not a remaining balance — lots are not consumed first-expiring-first, so units listed may already have been sold. Supports `Accept: text/csv`.","tags":["Reports"],"security":[{"bearerAuth":["reports:read"]}],"parameters":[{"name":"withinDays","in":"query","schema":{"type":"integer","minimum":1,"maximum":365,"default":60}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]},"description":"Or send `Accept: text/csv`. CSV is returned as an attachment."}],"responses":{"200":{"description":"Lots."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Scope missing, or the plan does not include `lots`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/reports/discount-overrides":{"get":{"summary":"Discount override audit trail","description":"Who authorised what, and for how much. A removed discount is marked reversed, never deleted — a discount cannot be given and its evidence then erased. Supports `Accept: text/csv`.","tags":["Reports"],"security":[{"bearerAuth":["reports:read"]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Start of the period. Defaults to 30 days ago."},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"End of the period. Defaults to now. Ranges are capped at 800 days."},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]},"description":"Or send `Accept: text/csv`. CSV is returned as an attachment."}],"responses":{"200":{"description":"Override records."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/reports/warranties-expiring":{"get":{"summary":"Warranties expiring","description":"Serial units whose warranty lapses within the window. Scrapped and supplier-returned units are excluded. Supports `Accept: text/csv`.","tags":["Reports"],"security":[{"bearerAuth":["reports:read"]}],"parameters":[{"name":"withinDays","in":"query","schema":{"type":"integer","minimum":1,"maximum":365,"default":30}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]},"description":"Or send `Accept: text/csv`. CSV is returned as an attachment."}],"responses":{"200":{"description":"Units."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Scope missing, or the plan does not include `serials`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/usage":{"get":{"summary":"API usage for this organization","description":"Requests, errors and rate-limited calls per day and per route, plus your current allowances — so a 429 can be reasoned about rather than guessed at. Retained for 90 days. Supports `Accept: text/csv`.","tags":["Meta"],"security":[{"bearerAuth":["keys:read"]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Start of the period. Defaults to 30 days ago."},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"End of the period. Defaults to now. Ranges are capped at 800 days."},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]},"description":"Or send `Accept: text/csv`. CSV is returned as an attachment."}],"responses":{"200":{"description":"Usage."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/keys":{"get":{"summary":"List this organization's API keys","description":"Prefix, scopes, status and usage — enough to identify a key, never enough to use one. Secrets are scrypt hashes and are unrecoverable by anyone. Keys are minted from the Inventory app or its CLI, never over this API, so a leaked key cannot mint successors.","tags":["Meta"],"security":[{"bearerAuth":["keys:read"]}],"responses":{"200":{"description":"Keys."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/purchase-orders/{id}":{"get":{"summary":"Retrieve a purchase order","description":"Lines are returned in both purchase units (as ordered) and stock units (as received). Freight is allocated across lines by value on receipt, so unit cost becomes landed cost.","tags":["Purchasing"],"security":[{"bearerAuth":["purchasing:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The order."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks":{"get":{"summary":"List webhook endpoints","description":"Your registered endpoints, with delivery health. The signing secret is never included — only its last four characters.","tags":["Webhooks"],"security":[{"bearerAuth":["webhooks:read"]}],"responses":{"200":{"description":"Endpoints."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"post":{"summary":"Register a webhook endpoint","description":"Events are delivered as a signed POST. **The response contains the signing secret, and it is the only time you will ever see it** — store it before moving on. If it is lost, rotate rather than recover.\n\nOmit `eventTypes` (or send an empty array) to receive every event. Narrowing later only removes traffic.\n\n**Verifying a delivery.** Each request carries `webhook-id`, `webhook-timestamp` and `webhook-signature`. Recompute an HMAC-SHA256 over `` `${id}.${timestamp}.${rawBody}` `` with your secret, base64 it, and compare against the `v1,` entry in the signature header in constant time. Reject anything whose timestamp is more than 300 seconds from now — that check is what makes a captured delivery useless to replay. A rotation may present several space-separated signatures; any one matching is valid.\n\n**Delivery semantics.** At-least-once, not exactly-once: treat `webhook-id` as an idempotency key on your side. Any 2xx is success. Anything else is retried on a widening schedule (10s, 30s, 2m, 10m, 30m, 2h, 6h, 12h) and then abandoned. Twenty consecutive failures with no success in between disables the endpoint, and you re-enable it with a PATCH.\n\n**Ordering is not guaranteed.** Events are discovered by polling, and a retried delivery can arrive after a later event. Use the `data` in the payload, or re-read the resource, rather than assuming sequence.\n\nThe URL must be publicly reachable. Private, loopback, link-local and cloud-metadata addresses are refused at registration and again at delivery, against the address the hostname actually resolves to.","tags":["Webhooks"],"security":[{"bearerAuth":["webhooks:write"]}],"parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string","maxLength":255},"description":"Makes a retry safe. A timed-out request tells you nothing about whether it happened; resending with the same key returns the original response instead of doing the work twice. Same key and same body replays (with `Idempotent-Replay: true`). Same key with a different body is a 422. Still in flight is a 409. Keys expire after 24 hours."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048},"description":{"type":"string","maxLength":200},"eventTypes":{"type":"array","items":{"type":"string","enum":["stock.movement.created","product.created","product.updated","variant.created","variant.updated","customer.created","customer.updated","sales_order.created","sales_order.updated","sales_order.fulfilled","sales_order.cancelled","purchase_order.created","purchase_order.updated","purchase_order.received","transfer.created","transfer.updated","transfer.received"]},"description":"Empty or omitted means every type."}}}}}},"responses":{"201":{"description":"The endpoint, including its signing secret (shown once)."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks/{id}":{"get":{"summary":"Retrieve a webhook endpoint","tags":["Webhooks"],"security":[{"bearerAuth":["webhooks:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The endpoint."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"patch":{"summary":"Update a webhook endpoint","description":"Setting `isActive: true` on an auto-disabled endpoint clears the failure run as well, so it is not disabled again by its very next failure.","tags":["Webhooks"],"security":[{"bearerAuth":["webhooks:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"description":{"type":"string","nullable":true},"eventTypes":{"type":"array","items":{"type":"string","enum":["stock.movement.created","product.created","product.updated","variant.created","variant.updated","customer.created","customer.updated","sales_order.created","sales_order.updated","sales_order.fulfilled","sales_order.cancelled","purchase_order.created","purchase_order.updated","purchase_order.received","transfer.created","transfer.updated","transfer.received"]}},"isActive":{"type":"boolean"}}}}}},"responses":{"200":{"description":"The updated endpoint."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"delete":{"summary":"Delete a webhook endpoint","description":"Its delivery history goes with it.","tags":["Webhooks"],"security":[{"bearerAuth":["webhooks:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks/{id}/rotate-secret":{"post":{"summary":"Rotate a signing secret","description":"Issues a new secret and returns it once. The previous one stops working immediately, so deploy the new secret before rotating, or accept a gap. Deliveries already in flight may still be signed with the old one.","tags":["Webhooks"],"security":[{"bearerAuth":["webhooks:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The endpoint, with its new secret."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks/deliveries":{"get":{"summary":"List delivery attempts","description":"Delivery history, newest first — what was sent, how many attempts it took, and the last error. `dead` means the retry ladder was exhausted; those are kept for 30 days so they can be inspected and requeued.","tags":["Webhooks"],"security":[{"bearerAuth":["webhooks:read"]}],"parameters":[{"name":"endpointId","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["pending","delivering","succeeded","failed","dead"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"Deliveries."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks/deliveries/{id}/redeliver":{"post":{"summary":"Retry a delivery","description":"Queues a delivery for another attempt whatever state it was in, including `dead`, and resets its attempt count so it gets the full retry ladder again.","tags":["Webhooks"],"security":[{"bearerAuth":["webhooks:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Queued."},"400":{"description":"Invalid request, sort field, or cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, malformed, revoked, or expired API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Insufficient scope, or the plan does not include API access.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No such resource in this organization.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Conflicting state, or an identical request is still in flight.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Semantically invalid, or the idempotency key was reused with a new body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}}}