Deliverable
API test collection
A Postman collection against the billing API in the demo repo: assertions that check contracts rather than status codes, a database check after the charge, and a Newman run that fails where it should.
Sample deliverable. Written for this site against a fictional product with no client data and no confidential material.
Structure
Billing API
├── 00 Setup
│ ├── POST /sandbox/reset
│ ├── POST /sandbox/clock → pins the clock to {{clockStart}}
│ ├── POST /v1/accounts → sets {{customerId}}
│ └── POST /v1/auth/token → sets {{accessToken}}
├── 01 Subscriptions
│ ├── POST /v1/subscriptions
│ ├── POST /v1/subscriptions (idempotency replay)
│ └── GET /sandbox/processor/charges
├── 02 Database
│ ├── GET /sandbox/db/subscriptions
│ ├── GET /sandbox/db/invoices
│ └── GET /sandbox/ledger
├── 03 Plan change
│ ├── POST /sandbox/clock → advances 12 days
│ └── PATCH /v1/subscriptions/:id/plan
├── 04 Invoices
│ └── GET /v1/invoices?status=open
├── 05 Webhooks
│ ├── GET /sandbox/events
│ ├── POST /v1/webhooks/events (replay)
│ └── GET /sandbox/ledger
└── 06 Negative
├── GET /v1/subscriptions/:id (no token)
├── POST /v1/subscriptions (malformed body)
└── POST /v1/webhooks/events (missing field)Ordered folders, not a flat list. The run has to be reproducible from a cold environment, which means setup comes first and nothing depends on state left by a previous run.
Assertions
| Request | Asserts | Why |
|---|---|---|
| POST /v1/subscriptions | Status 201 · exact key set · state is active · Location header present · responds inside 800 ms | The key-set assertion catches field renames that a status-code check sails past. |
| POST /v1/subscriptions (replay, same Idempotency-Key) | Status 200 · returns the original subscription ID | Passes on both builds. The defect is invisible in the response, which is why the next check exists. |
| GET /sandbox/processor/charges | Exactly one charge captured for the subscription, for the plan price | BUG-1142 lives here, not in the UI or the API response: the processor was charged twice. |
| GET /sandbox/db/subscriptions · /db/invoices · /ledger | One subscription row · one paid invoice equal to the plan price · one ledger entry for the payment | The database check after a charge. On rc3 it passes, so the app has no record of the second charge: the processor and the database disagree. |
| PATCH /v1/subscriptions/:id/plan | Status 200 · tier updated · effective date is the day of the change · proration equals the finance-supplied value | BUG-1156 lives here. Expected values come from the environment, not from the code under test. |
| GET /v1/invoices?status=open | Status 200 · array · every item belongs to the authenticated account | The ownership assertion is an authorization test disguised as a list endpoint test. |
| POST /v1/webhooks/events (replay of a delivered payment) | Duplicate event acknowledged · ledger entry count unchanged on the next request | Idempotency at the consumer, verified independently of delivery. |
| GET /v1/subscriptions/:id (no token) | Status 401 · no subscription data in the body | Every collection gets at least one negative auth case. |
| POST /v1/subscriptions (malformed body) | Status 422 · error names the missing card | A bad request must say what is wrong, not fail with a 500. |
| POST /v1/webhooks/events (missing field) | Status 400 · event rejected | A 4xx tells the sender not to retry, so a bad event cannot cause a retry storm. |
Test script
pm.test("201 with a usable subscription", () => {
pm.response.to.have.status(201);
const body = pm.response.json();
pm.expect(body).to.have.all.keys(
"id", "status", "plan", "current_period_end", "customer_id"
);
pm.expect(body.status).to.eql("active");
pm.collectionVariables.set("subscriptionId", body.id);
});
pm.test("no second charge on the processor", () => {
const body = pm.response.json();
pm.expect(body.count, "charges captured for this subscription").to.eql(1);
pm.expect(body.total, "total captured, in cents").to.eql(9900);
});
pm.test("proration matches the finance-supplied value", () => {
const expected = Number(pm.environment.get("expectedProration"));
const actual = pm.response.json().amount_due / 100;
pm.expect(actual).to.eql(expected);
});Asserting the full key set rather than a single field is deliberate. It fails when a field is dropped or renamed, which is the change most likely to break a downstream consumer silently. The other two tests come from different requests in the collection.
Environment
| Variable | Value | Notes |
|---|---|---|
| baseUrl | http://127.0.0.1:5055 | The local demo app. On an engagement, staging and the time-shifted environment each get their own file. No production environment ships with the collection. |
| accessToken | set at runtime | Never committed. Populated by the Setup folder. |
| clockStart · clockUpgrade | 2026-01-01 · 2026-01-13 | Pins the sandbox clock to the first day of a 31-day period, then moves it 12 days on. |
| expectedProration | 47.42 | Supplied by finance for the 31-day scenario. |
| cardNumber · cardExpiry | 4242 4242 4242 4242 · 12/30 | The sandbox processor's test card. |
CI run
scripts/newman.sh rc3 # or: fixed newman run postman/billing-api.postman_collection.json \ --environment postman/local.postman_environment.json \ --reporters cli,junit \ --reporter-junit-export results/newman-rc3.xml
| Build | Requests | Assertions | Failed |
|---|---|---|---|
| 4.2.1-rc3 | 19 | 23 | 2 |
| 4.2.1 (fixed) | 19 | 23 | 0 |
The two rc3 failures are the point of the exercise. Each one is a seeded defect, and neither is visible from the response of the request that causes it.
| Defect | Request | Failed assertion | Detail |
|---|---|---|---|
| BUG-1142 | Processor view: charges for the subscription | no second charge on the processor | charges captured for this subscription: expected 2 to deeply equal 1 |
| BUG-1156 | Upgrade to the pro plan | proration matches the finance-supplied value | expected 49 to deeply equal 47.42 |
JUnit XML output, the format Xray and Zephyr import, so results land in the test management tool instead of a terminal.
Source
The collection and its environment are in demo repository under postman/, next to the app they run against. Clone it, run make newman, and the two failures above are what you should see. The automation framework checks the same defects from the browser and with direct SQL.