>_ Analyst Engineering

Bruno vs Postman for Analysts: Git-Native Collections vs the Full Platform

Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.

Cover comparing Bruno and Postman as API clients for technical analysts, showing a bru file and a collection runner.

Key takeaways

  • Bruno stores each request as a plain text .bru file inside your repository, so an API collection is reviewed in a pull request and versioned with the code it tests; Postman stores collections in a cloud workspace and syncs them between accounts.
  • The practical dividing line is governance, not features: Bruno never sends your requests or environments to a vendor's cloud, which is why it clears bank security review faster than a platform that syncs by default.
  • Both run collections headlessly in CI, Bruno with the bru CLI and Postman with Newman, so either can gate a build on API contract tests rather than being a manual clicking tool.
  • Whichever client you use, secrets belong in environment variables resolved at runtime, never in the collection file, because collections get committed, exported, and shared far more often than anyone expects.

Bruno stores every API request as a plain text .bru file inside your repository, so a collection is reviewed in a pull request and versioned alongside the API it tests. Postman stores collections in a cloud workspace and adds a platform around them: team collaboration, mock servers, monitors, and governance. For an analyst in banking, the choice usually comes down to whether your requests and environments are allowed to leave the building.

Both tools do the same core job. You define a request, point it at an environment, send it, assert on the response, and run the whole set as a suite. If your work is testing an API end to end, either will carry you. The difference is where the collection lives and what that implies for review, secrets, and security sign-off.

I have run payment API test suites in both, and the deciding factor was never the feature list. It was that a .bru file shows up in a pull request diff as readable text, so a developer reviews my test for the pacs.008 submission endpoint the same way they review code. The full discipline of designing those requests, assertions, and negative cases is the subject of API Testing and QA Mastery for BAs.

What is the actual difference between Bruno and Postman?

BrunoPostman
Storage.bru text files in a folder you ownCloud workspace, synced to your account
Version controlNative: commit the folder, diff in a PRExport a JSON file, or use the paid git integration
Works offlineFullyCore client works, platform features do not
Collection reviewA readable text diffA large generated JSON diff
ScriptingJavaScript pre-request and post-responseJavaScript pre-request and test scripts
CI runnerbru CLINewman
Mock servers, monitors, governanceNoYes
Team collaborationThrough your git remoteBuilt into the platform
Licence and costOpen source, free for the core clientFree tier, paid tiers for team features

Read that table as one trade. Bruno gives up the platform to get the file. Postman gives up the file to get the platform. Everything else follows from that.

The file matters more than it sounds. When collections are files, the API test suite lives in the same repository as the API, moves on the same branch as a schema change, and is reviewed by the same person who wrote the endpoint. When a developer adds a field to the pacs.008 submission payload, the pull request contains both the code and the updated request, and a reviewer sees the mismatch immediately. That is the same property that makes contract testing work, applied to your manual suite.

What does a request look like in each?

Here is a payment submission in Bruno. This is the entire file, and it is what a reviewer sees.

meta {
  name: Submit credit transfer
  type: http
  seq: 1
}

post {
  url: {{baseUrl}}/v1/payments
  body: json
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

headers {
  Content-Type: application/json
  Idempotency-Key: {{idempotencyKey}}
}

body:json {
  {
    "endToEndId": "E2E-{{runId}}",
    "instructedAmount": { "currency": "EUR", "value": "1250.00" },
    "debtor":   { "name": "ACME BV",   "iban": "NL91ABNA0417164300" },
    "creditor": { "name": "Beta GmbH", "iban": "DE89370400440532013000" },
    "remittanceInformation": "INVOICE 88213"
  }
}

script:pre-request {
  bru.setVar("runId", Date.now());
  bru.setVar("idempotencyKey", crypto.randomUUID());
}

assert {
  res.status: eq 201
  res.body.status: eq "ACCP"
}

tests {
  test("returns a UETR", function() {
    expect(res.getBody().uetr).to.match(
      /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
    );
    bru.setEnvVar("uetr", res.getBody().uetr);
  });
}

The equivalent Postman request is the same idea expressed through the UI, with the pre-request and test scripts in their own tabs:

// Pre-request script
pm.variables.set("runId", Date.now());
pm.variables.set("idempotencyKey", crypto.randomUUID());

// Tests
pm.test("accepted", () => pm.response.to.have.status(201));
pm.test("status is ACCP", () =>
  pm.expect(pm.response.json().status).to.eql("ACCP"));
pm.test("returns a UETR", () => {
  const uetr = pm.response.json().uetr;
  pm.expect(uetr).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
  pm.collectionVariables.set("uetr", uetr);
});

Functionally equivalent. The difference is that the Bruno version is one file a reviewer reads in thirty seconds, while the Postman version is a fragment of a collection JSON export where the scripts appear as arrays of escaped strings. Neither is wrong. One is reviewable in a pull request and one is not.

Note the two headers doing real work. Idempotency-Key is what lets you prove the submission endpoint is safe to retry, which in payments is not a nice-to-have but the difference between a duplicated transfer and a rejected duplicate. Building the suite that proves it is idempotency testing, and it belongs in the collection, not in a tester’s memory.

How do you build a real payment test suite in either tool?

A single request proves an endpoint responds. A payment suite proves a payment moves. The structure below is what I run against an ISO 20022 payment initiation API, and it maps directly onto the payment message flow the API is fronting.

payments-api/
├── tests/
│   ├── 00-auth/
│   │   └── get-token.bru
│   ├── 10-happy-path/
│   │   ├── submit-credit-transfer.bru
│   │   ├── poll-status-until-acsp.bru
│   │   └── fetch-by-uetr.bru
│   ├── 20-rejections/
│   │   ├── invalid-iban-expect-ac01.bru
│   │   ├── closed-account-expect-ac04.bru
│   │   └── amount-limit-expect-am02.bru
│   ├── 30-idempotency/
│   │   ├── replay-same-key.bru
│   │   └── same-payload-new-key.bru
│   └── environments/
│       ├── local.bru
│       └── sit.bru

Four groups, each proving something different. The happy path proves the lifecycle: submit, receive ACCP, poll until ACSP, retrieve by UETR. The rejections group is where the domain lives, because each request deliberately breaks one rule and asserts the exact reason code the scheme requires. The idempotency group proves a replayed key returns the original payment rather than creating a second one. And the environments carry the per-deployment values so that not one URL is hardcoded in a request.

The rejection tests are the ones analysts are uniquely equipped to write, and the ones developers most often skip. Asserting that a bad IBAN returns HTTP 422 is a developer test. Asserting that it returns AC01 and not AC04, because one is repairable and one must be returned to the originator, is negative test design informed by the domain. The client is just where you write it down.

Chaining is what turns the folder into a suite. In Bruno, bru.setEnvVar("uetr", ...) in the submit test makes the UETR available to the polling request as {{uetr}}; Postman does the same through pm.collectionVariables. Once the chain holds, one command runs the whole lifecycle. The same chaining approach applied to event-driven systems is covered in Automate Kafka Validation with Postman, because a payment API that publishes to Kafka needs both halves validated.

How do you run either one in CI?

This is the step that changes an API client from a personal tool into a project asset, and both tools support it.

# Bruno
npm i -g @usebruno/cli
bru run tests -r --env sit --output results.json

# Postman
npm i -g newman
newman run collection.json -e sit.postman_environment.json \
  -r cli,junit --reporter-junit-export results.xml

Both exit non-zero on a failed assertion, which is the only contract a pipeline needs. Wire that into the build and the payment API suite becomes a gate: a schema change that drops the uetr field from the response fails the pipeline instead of failing in system integration testing three weeks later.

Two practical notes from running these on real pipelines. Keep the CI run pointed at a dedicated test environment with disposable data, because a suite that creates payments is a suite that creates state. And keep the rejection tests in the pipeline, not just the happy path, because the happy path breaks loudly and the rejection behaviour breaks silently.

Which should an analyst choose?

Choose Bruno when the collection should live with the code, when your reviewers want to read the tests in a pull request, when the work is under a security policy that objects to requests and environments syncing to a vendor’s cloud, or when you want a free tool with no account and no seat management. This is the common case in banking, and the data locality argument is usually what clears security review.

Choose Postman when you need the platform rather than the client: mock servers so a consumer team can build against an API that does not exist yet, monitors that run a collection on a schedule against production, a shared workspace for a large distributed team, or governance features tied to an API specification. These are real capabilities that Bruno does not attempt, and if your programme depends on them the choice is made.

Choose both, briefly, if you are migrating. Bruno imports Postman collections, so the low-risk path is to import the existing suite, commit it, and see whether anyone misses the platform. Most analyst work turns out to be requests, environments, assertions, and a CI run.

What does not change with the tool is the skill. Knowing which requests to write, what to assert, which negative cases matter, and how to read the API contract that defines them is the analyst capability. The client is where you type it.

Where do credentials go?

In neither collection. This is the mistake that survives every tool migration, because it is convenient exactly once and expensive later.

A collection file is committed, exported, attached to a ticket, and sent to a vendor for troubleshooting. Any bearer token, API key, or basic auth string saved inside it travels with it. The correct pattern in Bruno is an environment file that reads from the process environment:

vars {
  baseUrl: https://sit.payments.internal/api
}
vars:secret [
  clientSecret
]

The secret is declared but never stored, resolved from your shell or the CI secret store at run time, and .gitignore excludes any local environment file that would hold one. Postman has the equivalent in environment variables typed as secret, plus vault integration on paid tiers, and the same rule holds: the collection references a credential, it does not contain one.

Better still, do not use a long-lived key at all where the API supports a client credentials flow. Have the 00-auth request exchange a client id and secret for a short-lived access token, chain that token into every subsequent request, and the only stored value is one that expires. The full picture of key types, token lifetimes, scoping, and rotation across the analyst toolchain is in API keys, PATs, and OAuth tokens.

The takeaway

Bruno and Postman do the same core job, and the real choice is where the collection lives. Bruno keeps every request as a readable .bru file in your repository, which makes API tests reviewable in a pull request, versioned with the API, and entirely local, and that last property is usually what gets it approved in a bank. Postman keeps collections in a cloud workspace and surrounds them with a platform, which is worth it when you genuinely need mock servers, monitors, or governance.

Either way, the value comes from the suite, not the client: a happy path that proves the payment lifecycle, a rejection group that asserts the exact reason code for each broken rule, an idempotency group that proves replays are safe, environments that hold no secrets, and a headless run that gates the build.

Start with API Testing and QA Mastery for BAs for the test design, and API Documentation from Scratch for the contract your tests run against, or browse everything at The Tech BA Toolkit.

Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.

Tags: API Testing, QA, Bruno, Postman, Payments

About the author

Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.

Newsletter

Subscribe

Practical, no-fluff playbooks for technical analysts who analyze, code, test, and support. New articles straight to your inbox.

No spam. Unsubscribe anytime.