{ "info": { "name": "Invoice-AI API v1", "description": "Generated from /api/v1/openapi.json — do not edit by hand.\nRegenerate with `pnpm postman:gen`.\n\n## How to run\n1. Import both files: this collection and `invoice-ai.postman_environment.json`.\n2. Select the \"Invoice-AI — production\" environment and set `api_key` to a key\n from Settings → API keys (it needs every scope the run touches).\n3. Run the whole collection, in order, with the Collection Runner — or:\n `newman run invoice-ai.postman_collection.json -e invoice-ai.postman_environment.json --env-var api_key=…`\n\nThe folders are a lifecycle, not an index: each request depends on ids captured\nby the ones before it (stored as collection variables), so running a single\nrequest on its own needs those ids filled in first.\n\nThe run sends one real invoice email, to Resend's test sink delivered@resend.dev.\nA fresh Idempotency-Key is injected automatically for every request.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "auth": { "type": "bearer", "bearer": [ { "key": "token", "value": "{{api_key}}", "type": "string" } ] }, "event": [ { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [ "// A distinct Idempotency-Key per *request*, not per retry loop.", "// Reusing one key across different bodies is a 422 by design.", "pm.variables.set('idempotency_key', pm.variables.replaceIn('{{$guid}}'));", "", "// Relative, so the collection does not rot: a due date in the past", "// would make every invoice this run creates read as overdue.", "pm.variables.set('due_date', new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10));" ] } } ], "variable": [ { "key": "base_url", "value": "https://invoice.horizonpay.co/api/v1" }, { "key": "customer_id", "value": "" }, { "key": "product_id", "value": "" }, { "key": "price_id", "value": "" }, { "key": "webhook_endpoint_id", "value": "" }, { "key": "invoice_id", "value": "" }, { "key": "invoice_item_id", "value": "" } ], "item": [ { "name": "1. Setup", "description": "Create and edit the catalogue the invoices bill from.", "item": [ { "name": "Retrieve the business profile", "request": { "method": "GET", "header": [], "description": "Returns your business profile — the issuer printed on every invoice: legal name, address, tax id, default currency, bank details and invoice prefix. Read-only over the API; edit it in Settings.\n\n**Scope:** `business:read`", "url": { "raw": "{{base_url}}/business", "host": [ "{{base_url}}" ], "path": [ "business" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Create a customer", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "description": "Creates a customer to bill. Only `name` is required. Calls are not deduplicated: the same name twice makes two customers.\n\n**Scope:** `clients:write`\n\n**Idempotency:** `Idempotency-Key` optional. With one, a retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/customers", "host": [ "{{base_url}}" ], "path": [ "customers" ] }, "body": { "mode": "raw", "raw": "{\n \"name\": \"Acme Industries\",\n \"tax_id\": \"US-EIN 12-3456789\",\n \"email\": \"delivered@resend.dev\",\n \"address\": {\n \"line1\": \"4th Floor, Market Tower\",\n \"city\": \"Austin\",\n \"state\": \"TX\",\n \"postal_code\": \"73301\",\n \"country\": \"US\"\n }\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 201', () => pm.response.to.have.status(201));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "if (pm.response.code === 201) pm.collectionVariables.set('customer_id', pm.response.json().data.id);" ] } } ] }, { "name": "List customers", "request": { "method": "GET", "header": [], "description": "Lists customers, newest first. Archived customers are left out unless `include_deleted=true`.\n\n**Scope:** `clients:read`", "url": { "raw": "{{base_url}}/customers", "host": [ "{{base_url}}" ], "path": [ "customers" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Retrieve a customer", "request": { "method": "GET", "header": [], "description": "Returns a customer by `cus_…` id or UUID, including archived ones (`deleted: true`).\n\n**Scope:** `clients:read`", "url": { "raw": "{{base_url}}/customers/{{customer_id}}", "host": [ "{{base_url}}" ], "path": [ "customers", "{{customer_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Update a customer", "request": { "method": "PATCH", "header": [ { "key": "Content-Type", "value": "application/json" } ], "description": "Updates the fields you send and leaves the rest unchanged. Issued invoices keep the customer details they were billed with. No `Idempotency-Key` needed — repeating the call is harmless.\n\n**Scope:** `clients:write`", "url": { "raw": "{{base_url}}/customers/{{customer_id}}", "host": [ "{{base_url}}" ], "path": [ "customers", "{{customer_id}}" ] }, "body": { "mode": "raw", "raw": "{\n \"phone\": \"+1 512 555 0100\"\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Create a product", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "description": "Creates a product. Amounts live on prices: add one with `POST /prices` before billing it.\n\n**Scope:** `products:write`\n\n**Idempotency:** `Idempotency-Key` optional. With one, a retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/products", "host": [ "{{base_url}}" ], "path": [ "products" ] }, "body": { "mode": "raw", "raw": "{\n \"name\": \"Consulting retainer\",\n \"description\": \"Monthly advisory block.\"\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 201', () => pm.response.to.have.status(201));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "if (pm.response.code === 201) pm.collectionVariables.set('product_id', pm.response.json().data.id);" ] } } ] }, { "name": "List products", "request": { "method": "GET", "header": [], "description": "Lists products, newest first. Archived products are included unless you filter with `active`.\n\n**Scope:** `products:read`", "url": { "raw": "{{base_url}}/products", "host": [ "{{base_url}}" ], "path": [ "products" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Retrieve a product", "request": { "method": "GET", "header": [], "description": "Returns a product by `prod_…` id or UUID, active or archived.\n\n**Scope:** `products:read`", "url": { "raw": "{{base_url}}/products/{{product_id}}", "host": [ "{{base_url}}" ], "path": [ "products", "{{product_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Update a product", "request": { "method": "PATCH", "header": [ { "key": "Content-Type", "value": "application/json" } ], "description": "Updates the fields you send and leaves the rest unchanged. Send `active: true` to restore an archived product. Finalized invoices keep the line descriptions they were issued with.\n\n**Scope:** `products:write`", "url": { "raw": "{{base_url}}/products/{{product_id}}", "host": [ "{{base_url}}" ], "path": [ "products", "{{product_id}}" ] }, "body": { "mode": "raw", "raw": "{\n \"description\": \"Monthly advisory block, up to 10 hours.\"\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Create a price", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "description": "Adds a price to a product. `unit_amount` is in integer minor units. A `recurring` price needs `recurring.interval`; it is stored for reporting — nothing auto-bills yet.\n\n**Scope:** `products:write`\n\n**Idempotency:** `Idempotency-Key` optional. With one, a retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/prices", "host": [ "{{base_url}}" ], "path": [ "prices" ] }, "body": { "mode": "raw", "raw": "{\n \"product\": \"{{product_id}}\",\n \"nickname\": \"Monthly\",\n \"unit_amount\": 2500000,\n \"currency\": \"USD\",\n \"type\": \"recurring\",\n \"recurring\": {\n \"interval\": \"month\",\n \"interval_count\": 1\n },\n \"tax_rate\": 0\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 201', () => pm.response.to.have.status(201));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "if (pm.response.code === 201) pm.collectionVariables.set('price_id', pm.response.json().data.id);" ] } } ] }, { "name": "List prices", "request": { "method": "GET", "header": [], "description": "Lists prices, newest first, optionally filtered by `product`, `active`, `currency` or `type`. An unknown `product` is a `404`, not an empty page.\n\n**Scope:** `products:read`", "url": { "raw": "{{base_url}}/prices", "host": [ "{{base_url}}" ], "path": [ "prices" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Retrieve a price", "request": { "method": "GET", "header": [], "description": "Returns a price by `price_…` id or UUID, active or archived.\n\n**Scope:** `products:read`", "url": { "raw": "{{base_url}}/prices/{{price_id}}", "host": [ "{{base_url}}" ], "path": [ "prices", "{{price_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Update a price", "request": { "method": "PATCH", "header": [ { "key": "Content-Type", "value": "application/json" } ], "description": "Updates the fields you send and leaves the rest unchanged. A price cannot move to another product (`422`) — create a new price instead. Finalized invoices keep the amount they were billed at; a draft picks up the change the next time it is saved.\n\n**Scope:** `products:write`", "url": { "raw": "{{base_url}}/prices/{{price_id}}", "host": [ "{{base_url}}" ], "path": [ "prices", "{{price_id}}" ] }, "body": { "mode": "raw", "raw": "{\n \"nickname\": \"Monthly (annual contract)\",\n \"type\": \"recurring\",\n \"recurring\": {\n \"interval\": \"month\",\n \"interval_count\": 1\n }\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Create a webhook endpoint", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "description": "Registers a URL to receive signed invoice events. The response carries the signing `secret` (`whsec_…`) — the only time it is returned, so store it.\n\n**URL rules:** `https` only, at most 2048 characters, no username or password, and every address it resolves to must be public — private, loopback, link-local and IPv4-embedding IPv6 ranges (NAT64, 6to4, Teredo) are rejected. The check runs again at every delivery, and redirects are not followed.\n\n**Scope:** `webhooks:manage`\n\n**Idempotency:** `Idempotency-Key` optional. With one, a retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/webhook-endpoints", "host": [ "{{base_url}}" ], "path": [ "webhook-endpoints" ] }, "body": { "mode": "raw", "raw": "{\n \"url\": \"https://example.com/webhooks/invoice-ai\",\n \"events\": [\n \"invoice.finalized\",\n \"invoice.paid\"\n ]\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 201', () => pm.response.to.have.status(201));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "if (pm.response.code === 201) pm.collectionVariables.set('webhook_endpoint_id', pm.response.json().data.id);", "pm.test('the secret is returned exactly once, here', () => {", " if (pm.response.code === 201) pm.expect(pm.response.json().data).to.have.property('secret');", "});" ] } } ] }, { "name": "List webhook endpoints", "request": { "method": "GET", "header": [], "description": "Lists your webhook endpoints, newest first. Signing secrets are never included.\n\n**Scope:** `webhooks:manage`", "url": { "raw": "{{base_url}}/webhook-endpoints", "host": [ "{{base_url}}" ], "path": [ "webhook-endpoints" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] } ] }, { "name": "2. Draft invoice", "description": "A draft is freely editable: no number has been spent on it yet.", "item": [ { "name": "Create a draft invoice", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "Idempotency-Key", "value": "{{idempotency_key}}" } ], "description": "Creates a `draft` invoice for an existing customer. Each line is either a catalog `price` (its name, amount and tax rate fill in) or an ad-hoc `description` + `unit_amount`. Totals are computed server-side; `currency` defaults to your business currency.\n\nA draft has no number until you finalize or send it. Emits `invoice.created`.\n\n**Scope:** `invoices:write` + `business:read` + `clients:read`\n\n**Idempotency:** `Idempotency-Key` **required** (`428` without it). A retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/invoices", "host": [ "{{base_url}}" ], "path": [ "invoices" ] }, "body": { "mode": "raw", "raw": "{\n \"customer\": \"{{customer_id}}\",\n \"currency\": \"USD\",\n \"due_date\": \"{{due_date}}\",\n \"description\": \"Consulting retainer.\",\n \"items\": [\n {\n \"price\": \"{{price_id}}\",\n \"quantity\": 1\n },\n {\n \"description\": \"Onboarding workshop\",\n \"quantity\": 1,\n \"unit\": \"NOS\",\n \"unit_amount\": 50000,\n \"tax_rate\": 0\n }\n ]\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 201', () => pm.response.to.have.status(201));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "if (pm.response.code === 201) pm.collectionVariables.set('invoice_id', pm.response.json().data.id);" ] } } ] }, { "name": "List invoices", "request": { "method": "GET", "header": [], "description": "Lists invoices, newest first, without `lines`. `status=overdue` returns open invoices whose `due_date` is before today (UTC); `status=open` includes them. An unknown `customer` filter is a `404`.\n\n**Scope:** `invoices:read` + `clients:read` (when filtering by `customer`)", "url": { "raw": "{{base_url}}/invoices", "host": [ "{{base_url}}" ], "path": [ "invoices" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Retrieve an invoice", "request": { "method": "GET", "header": [], "description": "Returns an invoice by `in_…` id or UUID, with its `lines`. `status` reads `overdue` when an open invoice is past its due date.\n\n**Scope:** `invoices:read`", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Update a draft invoice", "request": { "method": "PATCH", "header": [ { "key": "Content-Type", "value": "application/json" } ], "description": "Updates a draft. Send only the fields to change; `items`, when sent, replaces every line (line ids change). Once finalized an invoice is frozen and this returns `409`. Emits `invoice.updated`.\n\n**Scope:** `invoices:write` + `business:read` + `clients:read`", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}" ] }, "body": { "mode": "raw", "raw": "{\n \"customer\": \"{{customer_id}}\",\n \"due_date\": \"{{due_date}}\",\n \"description\": \"Consulting retainer — net 30.\",\n \"footer\": \"Thank you for your business.\"\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Add an invoice item", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "Idempotency-Key", "value": "{{idempotency_key}}" } ], "description": "Appends a line to a draft: a `price`, or `description` + `unit_amount`. Returns the whole recomputed invoice; every line gets a new id. Finalized invoices return `409`. Emits `invoice.updated`.\n\n**Scope:** `invoices:write` + `business:read`\n\n**Idempotency:** `Idempotency-Key` **required** (`428` without it). A retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/invoice-items", "host": [ "{{base_url}}" ], "path": [ "invoice-items" ] }, "body": { "mode": "raw", "raw": "{\n \"invoice\": \"{{invoice_id}}\",\n \"description\": \"Extra review round\",\n \"quantity\": 1,\n \"unit_amount\": 10000\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 201', () => pm.response.to.have.status(201));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "if (pm.response.code === 201) {", " const lines = pm.response.json().data.lines.data;", " const added = lines.find((line) => line.description === 'Extra review round') || lines[lines.length - 1];", " pm.collectionVariables.set('invoice_item_id', added.id);", "}" ] } } ] }, { "name": "List invoice items", "request": { "method": "GET", "header": [], "description": "Returns every line of one invoice, in order. `invoice` is required; the result is not paginated.\n\n**Scope:** `invoices:read`", "url": { "raw": "{{base_url}}/invoice-items?invoice={{invoice_id}}", "host": [ "{{base_url}}" ], "path": [ "invoice-items" ], "query": [ { "key": "invoice", "value": "{{invoice_id}}" } ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Retrieve an invoice item", "request": { "method": "GET", "header": [], "description": "Returns one invoice line by `ii_…` id or UUID.\n\n**Scope:** `invoices:read`", "url": { "raw": "{{base_url}}/invoice-items/{{invoice_item_id}}", "host": [ "{{base_url}}" ], "path": [ "invoice-items", "{{invoice_item_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Remove an invoice item", "request": { "method": "DELETE", "header": [], "description": "Removes a line from a draft and returns the recomputed invoice. A draft keeps at least one line, and finalized invoices are frozen — both `409`. Pass `invoice` to speed up the lookup. Emits `invoice.updated`.\n\n**Scope:** `invoices:write` + `business:read`", "url": { "raw": "{{base_url}}/invoice-items/{{invoice_item_id}}?invoice={{invoice_id}}", "host": [ "{{base_url}}" ], "path": [ "invoice-items", "{{invoice_item_id}}" ], "query": [ { "key": "invoice", "value": "{{invoice_id}}" } ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] } ] }, { "name": "3. Invoice lifecycle", "description": "Finalize (spends a number), email it to delivered@resend.dev, then mark it paid.", "item": [ { "name": "Finalize an invoice", "request": { "method": "POST", "header": [ { "key": "Idempotency-Key", "value": "{{idempotency_key}}" } ], "description": "Issues a draft: assigns the next number in your series (e.g. `INV-0042`) and moves it to `open`. The draft needs at least one line. Finalizing an invoice that already has a number returns it unchanged, so a retry never spends a second number. Does not email the customer — see `/send`. Emits `invoice.finalized`.\n\n**Scope:** `invoices:finalize`\n\n**Idempotency:** `Idempotency-Key` **required** (`428` without it). A retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}/finalize", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}", "finalize" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "pm.test('finalizing returns an invoice number', () => {", " if (pm.response.code === 200) {", " pm.expect(pm.response.json().data.number).to.match(/^[A-Z0-9\\-\\/]{1,16}-\\d{4}$/);", " }", "});", "// Run this request twice with the SAME Idempotency-Key to see the replay:", "// the second response carries Idempotent-Replayed: true and the same number." ] } } ] }, { "name": "Send an invoice", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "Idempotency-Key", "value": "{{idempotency_key}}" } ], "description": "Emails the invoice PDF and a link to its public page to `to`, or else the customer email on the invoice. A draft is finalized first (no `invoices:finalize` scope needed). Open and paid invoices can be re-sent; void ones cannot.\n\n**Requires a verified account email** — until then this returns `409` `invalid_state`.\n\n**Rate limits:** 10 sends per hour per API key, and 50 per hour per account across every key and the dashboard (`429` `rate_limited`, with `Retry-After`).\n\nIf the email fails after finalizing, the response is `502` `upstream_failed` and the number stays spent; retry the send. Emits `invoice.emailed` (or `invoice.email_failed`), plus `invoice.finalized` when it finalized.\n\n**Scope:** `invoices:send`\n\n**Idempotency:** `Idempotency-Key` **required** (`428` without it). A retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}/send", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}", "send" ] }, "body": { "mode": "raw", "raw": "{\n \"to\": \"delivered@resend.dev\"\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Download an invoice PDF", "request": { "method": "GET", "header": [], "description": "Returns the PDF bytes (not a link), rendered on demand from the current invoice — drafts too, without a number. Served inline; pass `download=1` for `Content-Disposition: attachment`. Not cacheable.\n\n**Scope:** `invoices:read`", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}/pdf", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}", "pdf" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "pm.test('is a PDF', () => {", " if (pm.response.code === 200) pm.expect(pm.response.headers.get('content-type')).to.include('application/pdf');", "});" ] } } ] }, { "name": "List invoice events", "request": { "method": "GET", "header": [], "description": "Lists one invoice’s history, newest first: created, updated, finalized, emailed, viewed, downloaded, paid, voided. `invoice.viewed` and `invoice.downloaded` mean the customer opened the public link; each is recorded at most once per invoice every 10 minutes.\n\n**Scope:** `invoices:read`", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}/events", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}", "events" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Mark an invoice paid", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "Idempotency-Key", "value": "{{idempotency_key}}" } ], "description": "Marks an `open` (or overdue) invoice paid outside Invoice-AI — no money moves. `paid_on` is an ISO 8601 date or timestamp (defaults to now); `reference` is up to 200 characters. Drafts, void and already-paid invoices return `409`. The response omits `lines`. Emits `invoice.paid`, with `reference` in its meta.\n\n**Scope:** `payments:write`\n\n**Idempotency:** `Idempotency-Key` **required** (`428` without it). A retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}/pay", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}", "pay" ] }, "body": { "mode": "raw", "raw": "{\n \"reference\": \"chk_123456\"\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] } ] }, { "name": "4. Delete & void paths", "description": "A paid invoice cannot be voided and a finalized one cannot be deleted, so each path gets its own fresh invoice.", "item": [ { "name": "Create a throwaway draft", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "Idempotency-Key", "value": "{{idempotency_key}}" } ], "description": "Creates a `draft` invoice for an existing customer. Each line is either a catalog `price` (its name, amount and tax rate fill in) or an ad-hoc `description` + `unit_amount`. Totals are computed server-side; `currency` defaults to your business currency.\n\nA draft has no number until you finalize or send it. Emits `invoice.created`.\n\n**Scope:** `invoices:write` + `business:read` + `clients:read`\n\n**Idempotency:** `Idempotency-Key` **required** (`428` without it). A retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/invoices", "host": [ "{{base_url}}" ], "path": [ "invoices" ] }, "body": { "mode": "raw", "raw": "{\n \"customer\": \"{{customer_id}}\",\n \"currency\": \"USD\",\n \"description\": \"Throwaway draft — deleted by the next request.\",\n \"items\": [\n {\n \"description\": \"Placeholder\",\n \"quantity\": 1,\n \"unit_amount\": 1000,\n \"tax_rate\": 0\n }\n ]\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 201', () => pm.response.to.have.status(201));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "if (pm.response.code === 201) pm.collectionVariables.set('invoice_id', pm.response.json().data.id);" ] } } ] }, { "name": "Delete the throwaway draft", "request": { "method": "DELETE", "header": [], "description": "Permanently deletes a draft. A finalized invoice cannot be deleted (`409`) — its number belongs to a gap-free series. Void it instead.\n\n**Scope:** `invoices:write`", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 204', () => pm.response.to.have.status(204));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Create a draft to void", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "Idempotency-Key", "value": "{{idempotency_key}}" } ], "description": "Creates a `draft` invoice for an existing customer. Each line is either a catalog `price` (its name, amount and tax rate fill in) or an ad-hoc `description` + `unit_amount`. Totals are computed server-side; `currency` defaults to your business currency.\n\nA draft has no number until you finalize or send it. Emits `invoice.created`.\n\n**Scope:** `invoices:write` + `business:read` + `clients:read`\n\n**Idempotency:** `Idempotency-Key` **required** (`428` without it). A retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/invoices", "host": [ "{{base_url}}" ], "path": [ "invoices" ] }, "body": { "mode": "raw", "raw": "{\n \"customer\": \"{{customer_id}}\",\n \"currency\": \"USD\",\n \"due_date\": \"{{due_date}}\",\n \"description\": \"Raised by mistake — voided below.\",\n \"items\": [\n {\n \"price\": \"{{price_id}}\",\n \"quantity\": 1\n }\n ]\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 201', () => pm.response.to.have.status(201));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "if (pm.response.code === 201) pm.collectionVariables.set('invoice_id', pm.response.json().data.id);" ] } } ] }, { "name": "Finalize the draft to void", "request": { "method": "POST", "header": [ { "key": "Idempotency-Key", "value": "{{idempotency_key}}" } ], "description": "Issues a draft: assigns the next number in your series (e.g. `INV-0042`) and moves it to `open`. The draft needs at least one line. Finalizing an invoice that already has a number returns it unchanged, so a retry never spends a second number. Does not email the customer — see `/send`. Emits `invoice.finalized`.\n\n**Scope:** `invoices:finalize`\n\n**Idempotency:** `Idempotency-Key` **required** (`428` without it). A retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}/finalize", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}", "finalize" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));", "pm.test('finalizing returns an invoice number', () => {", " if (pm.response.code === 200) {", " pm.expect(pm.response.json().data.number).to.match(/^[A-Z0-9\\-\\/]{1,16}-\\d{4}$/);", " }", "});", "// Run this request twice with the SAME Idempotency-Key to see the replay:", "// the second response carries Idempotent-Replayed: true and the same number." ] } } ] }, { "name": "Void an invoice", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "Idempotency-Key", "value": "{{idempotency_key}}" } ], "description": "Voids an `open` (or overdue) invoice. `reason` is required, up to 500 characters. The number stays on record, so the series has no gaps. A paid invoice cannot be voided (it needs a credit note) and a draft should be deleted instead — both `409`. The response omits `lines`. Emits `invoice.voided`.\n\n**Scope:** `invoices:finalize`\n\n**Idempotency:** `Idempotency-Key` **required** (`428` without it). A retry with the same key and body replays the first response (`Idempotent-Replayed: true`); the same key with a different body is a `422` `idempotency_mismatch`.", "url": { "raw": "{{base_url}}/invoices/{{invoice_id}}/void", "host": [ "{{base_url}}" ], "path": [ "invoices", "{{invoice_id}}", "void" ] }, "body": { "mode": "raw", "raw": "{\n \"reason\": \"Raised against the wrong customer.\"\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] } ] }, { "name": "5. Cleanup", "description": "Customers, products and prices archive rather than delete — issued invoices still reference them.", "item": [ { "name": "Delete a webhook endpoint", "request": { "method": "DELETE", "header": [], "description": "Permanently deletes an endpoint; no further deliveries are made to it. Deleting it again is a `404`.\n\n**Scope:** `webhooks:manage`", "url": { "raw": "{{base_url}}/webhook-endpoints/{{webhook_endpoint_id}}", "host": [ "{{base_url}}" ], "path": [ "webhook-endpoints", "{{webhook_endpoint_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 204', () => pm.response.to.have.status(204));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Archive a price", "request": { "method": "DELETE", "header": [], "description": "Archives a price (`active: false`). It is kept because invoice lines may reference it; archiving twice changes nothing. Restore with `PATCH` and `active: true`.\n\n**Scope:** `products:write`", "url": { "raw": "{{base_url}}/prices/{{price_id}}", "host": [ "{{base_url}}" ], "path": [ "prices", "{{price_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Archive a product", "request": { "method": "DELETE", "header": [], "description": "Archives a product (`active: false`). Its prices and the invoice lines that use it stay intact; archiving twice changes nothing. Restore with `PATCH` and `active: true`.\n\n**Scope:** `products:write`", "url": { "raw": "{{base_url}}/products/{{product_id}}", "host": [ "{{base_url}}" ], "path": [ "products", "{{product_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] }, { "name": "Archive a customer", "request": { "method": "DELETE", "header": [], "description": "Archives a customer (`deleted: true`). Customers are never hard-deleted, because issued invoices must keep naming who they billed. Archived customers drop out of `GET /customers` but stay readable by id; archiving twice changes nothing.\n\n**Scope:** `clients:write`", "url": { "raw": "{{base_url}}/customers/{{customer_id}}", "host": [ "{{base_url}}" ], "path": [ "customers", "{{customer_id}}" ] } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('is not an auth failure', () => pm.expect(pm.response.code).to.not.be.oneOf([401, 403]));", "pm.test('errors are problem+json', () => {", " if (pm.response.code >= 400) {", " pm.expect(pm.response.headers.get('content-type')).to.include('application/problem+json');", " pm.expect(pm.response.json()).to.have.property('code');", " }", "});", "pm.test('echoes a request id', () => pm.expect(pm.response.headers.get('x-request-id')).to.be.a('string'));" ] } } ] } ] } ] }