Your collection is already the most accurate description of your API. It holds every endpoint, the headers each one expects, example bodies, the auth it needs, and the tests that prove it still works. The next step is getting it in front of people outside your team: partners integrating against your API, customers reading your reference docs, or developers who found your open-source project on GitHub. This guide covers how to share a Bruno collection or workspace publicly, and how to publish API documentation from it, without a Bruno account or a hosted workspace.
Bruno gives you two ways to do that. You can share the collection itself through a public Git repository, with a Fetch in Bruno button that clones it into a reader's app in one click. Or you can generate a standalone HTML documentation site from the collection, playground included, and host it anywhere static files can live. Workspaces follow the same Git route as collections.
This post walks through both, shows what a reader sees on the other end, and covers how to keep secrets and unfinished requests out of anything you publish. If you are sharing with your own team rather than the public, start with How to Share Bruno Collections: Git, Export, and Import, which covers Git sync, exports, and imports.
To share a Bruno collection publicly, push it to a public Git repository and add a Fetch in Bruno button to your README or docs site; anyone who clicks it gets a Git-synced copy in their own Bruno. To publish API docs, open the collection's Overview tab and click Generate Docs (or run bru docs generate in CI) to get a single HTML file with a live playground that you can host anywhere. The recommended setup is both. Secret variables never land in the repository or the HTML, and everything here works in the free edition.
The two routes are not either/or, and the recommended setup is both: publish the generated docs for anyone with a browser, and put a Fetch in Bruno button tied to the Git repository next to them for readers who want the collection in their own app. The generated docs already embed the repository link as Open in Bruno, so the pair comes out of one Git-backed collection with no extra upkeep. If you only ship one, pick by what the reader should be able to do afterward: run and edit requests in Bruno means Git, while read, search, and try without installing anything means the docs.
One Git-backed collection feeds both the Fetch in Bruno button and the generated docs.
| You share | How | What the reader gets | Reader needs |
|---|---|---|---|
| A collection | Public Git repo plus a Fetch in Bruno button | A Git-synced copy in their own Bruno: every request, script, test, and doc. They pull your updates whenever you push | Bruno installed |
| A workspace | Public Git repo plus a Fetch in Bruno button from the Export action on the workspace overview | A pick list of every collection in the workspace repository, each opening as a Git-synced copy | Bruno installed |
| Docs | A single generated HTML file, hosted anywhere | A browsable, searchable API reference with a Try playground, code samples, and an Open in Bruno button that clones the source when the Git link is included, or offers a download when it is not | A browser |
| An OpenAPI spec | A Fetch in Bruno button pointed at the spec URL | The spec converted into a Bruno collection and opened in their app | Bruno installed |
Both routes read from the same collection files on disk, so there is one source of truth. The markdown you write at the workspace, collection, folder, and request levels travels with the Git clone and fills the descriptions in the generated docs site.
Everything in this post works in the free, open-source edition of Bruno, with one exception: doing some of the Git plumbing from inside the app. Generating docs from the collection's Overview tab or with bru docs generate, the Fetch in Bruno button, and the reader's clone, fetch, and pull are all included in the free edition, as are initializing Git and viewing diffs in the Git UI. Connecting a remote, committing, pushing, branching, stashing, and resolving merge conflicts inside the Git UI need Pro or Ultimate or a free trial, and so do private repositories through the Git providers. Check out more details on our pricing page.
In practice, a free-edition user initializes the repository in Bruno, on the command line, or with an IDE, then connects the remote and pushes from the terminal. The button, the clone on the reader's side, and the generated docs behave the same on every plan. If you want the in-app Git actions, a 14-day trial of Ultimate unlocks them with no credit card.
This is the first half of the recommended setup; the generated docs are the other. The collection lives in a public Git repository, and a Fetch in Bruno button on your README, docs site, or blog post turns the clone into a single click. The Fetch in Bruno button is a link to fetch.usebruno.com that carries your repository URL and clones it into the reader's Bruno. There is nothing to upload to us. The public Git repository is the share.
The collection or workspace has to be a Git repository with a public remote. You can initialize Git from the Bruno UI, from the command line, or from an IDE, then push to GitHub, GitLab, Bitbucket, or any other host. Initializing is free in every edition of Bruno. Connecting the remote and pushing from inside the app's Git UI needs a Pro or Ultimate license. On the free version, you can always push from the terminal, and the Fetch in Bruno button works the same either way.
... menu next to the collection name in the sidebar.... next to the workspace name and choose Export.The Export action is only offered for workspaces you created or opened from a folder; the default workspace does not have it.
A workspace button lists every Bruno collection in that repository, so this is the route when you want readers to pick from a set of related collections in one import.
The button is a link to fetch.usebruno.com with your repository URL in the query string. You can hand-write it, or generate it programmatically, anywhere that renders HTML or Markdown.
HTML:
<div
class="bruno-fetch-button"
data-bruno-collection-url="https://github.com/your-org/your-collection.git"
></div>
<script src="https://fetch.usebruno.com/button.js"></script>
Markdown, which is what you want in a GitHub README:
[<img src="https://fetch.usebruno.com/button.svg" alt="Fetch in Bruno" width="128" height="32">](https://fetch.usebruno.com?url=https://github.com/your-org/your-collection.git "target=_blank rel=noopener noreferrer")
Swap in your own image and alt text if you want the button to carry your project's branding. Keep the link target as it is.
For example, a README that uses a button image hosted in the project's own repository, with alt text that names the API:
<a href="https://fetch.usebruno.com?url=https://github.com/your-org/your-collection.git" target="_blank" rel="noopener noreferrer">
<img
src="https://raw.githubusercontent.com/your-org/your-collection/main/assets/try-in-bruno.svg"
alt="Try the Acme API in Bruno"
width="160"
height="40"
>
</a>
This form works both in a GitHub README, which renders inline HTML, and on any web page. Only three attributes on the image are yours to change. Everything else stays as the Embed tab generated it, including the link, which already carries your Git repository URL:
| Part | Generated by the Embed tab | Customized here |
|---|---|---|
src |
https://fetch.usebruno.com/button.svg |
Your own image, hosted in the repo or on your site |
alt |
Fetch in Bruno |
Try the Acme API in Bruno |
width / height |
128 / 32 |
160 / 40, to match the new image |
| Link target | https://fetch.usebruno.com?url= followed by your Git repository URL |
Same shape, with your own repository URL in the url parameter |
The fetch.usebruno.com link with your repository URL is what makes the click open Bruno, so that part stays exactly as generated.
Six steps from a README button to a Git-synced copy in the reader's Bruno.
The result is a real Git clone, not a snapshot. When you push a change, the reader pulls it from Bruno's Git UI or from their terminal and gets the new version.
The same button also accepts an OpenAPI spec URL. Bruno imports OpenAPI 2.0 (Swagger 2.0) and OpenAPI 3.x specs, written in either JSON or YAML, so the URL can end in .json, .yaml, or .yml. Add data-bruno-source-type="openapi" to the HTML form, or &type=openapi to the Markdown link, and Bruno fetches the spec, converts it into a collection, and opens it. That is useful when the public artifact you already maintain is the spec rather than a collection.
HTML, pointed at a spec instead of a repository:
<div
class="bruno-fetch-button"
data-bruno-collection-url="https://api.example.com/openapi.json"
data-bruno-source-type="openapi"
></div>
<script src="https://fetch.usebruno.com/button.js"></script>
Markdown, with &type=openapi added to the link:
[<img src="https://fetch.usebruno.com/button.svg" alt="Fetch in Bruno" width="128" height="32">](https://fetch.usebruno.com?url=https://api.example.com/openapi.json&type=openapi "target=_blank rel=noopener noreferrer")
When a reader clicks either one, Bruno fetches the spec from that URL, converts it into a collection, and asks where to save it. The spec URL has to be publicly reachable, since the reader's machine is what downloads it. The OpenAPI import docs cover the supported versions and how endpoints are grouped into folders.
Readers can also skip the button entirely. The GitHub tab in the Import Collection dialog searches all of GitHub for public repositories, no login required, and clones from the results. Private and organization repositories need the GitHub provider connected, which needs Pro, Ultimate, or a free trial. We covered that flow in Git provider integration in Bruno.
Generated documentation is for readers who shouldn't have to install anything. Bruno builds a self-contained HTML file from the collection: requests, params, headers, auth, scripts, tests, and the markdown docs you have written at each level. It also ships with an interactive playground, so readers can edit a request, send it, and see the response without leaving the page. You host the file wherever you like, and generating it is included in the free edition.
Collection files go in, one self-contained HTML file comes out, and it can live anywhere static files do.
... menu in the sidebar.collection-name-documentation.html to your file system.The CLI and the app share one implementation, so the same collection and options produce the same docs file. Run the CLI from the collection root, the folder that holds opencollection.yml:
$ bru docs generate --all-envs --exclude-tags "Work in progress" --no-git-link
That makes the docs a build artifact. A CI job can regenerate on every merge and publish the output:
$ bru docs generate \
--all-envs \
--exclude-envs Local \
--exclude-tags "WIP,Internal" \
--no-git-link \
--output dist/docs/api.html
The flags in those two commands:
--all-envs embeds every environment in the collection. Use --envs Production,Staging instead to name specific ones.--exclude-envs Local leaves the named environments out. It wins over --all-envs and --envs when the same name appears in both.--exclude-tags "WIP,Internal" drops every request carrying any of those tags. --tags does the opposite, keeping only matching requests, and --exclude-tags wins when they overlap.--no-git-link leaves the Git repository link out of the docs, for when the repo is private but the docs are public. Open in Bruno still appears, offering a .yml download and import steps instead of a clone, so readers can still open the collection in Bruno.--output dist/docs/api.html (or -o) sets the output path. Without it, the file lands in the current directory as collection-name-documentation.html.Every flag is listed in the bru docs generate options reference.
The output is one file with no server-side dependencies, so sharing it is as simple as moving a file:
When the API changes, regenerate and replace the deployed copy. You can even automate this using the Bruno CLI.
The generated site is executable, not just readable. At first glance it reads like any other API reference. The difference is the Try button on every request.
The generated page: environment switcher and Open in Bruno up top, navigation on the left, the request in the middle, and Try on the right.
Try requests live. The playground lets readers edit params, headers, and body, send the request, and see the response in the page. It also shows the pre-request script, post-response script, tests, and assertions attached to the request, so the behavior is visible rather than implied.
Copy code samples. Every request ships with ready-to-copy snippets in cURL, Python (requests), and JavaScript.
Switch environments. The topbar environment switcher lets readers pick any environment you embedded. Turning on Show vars resolves placeholders across URLs, headers, bodies, and code snippets. Secret environment variables stay masked even with Show vars on.
Search. Full-text search covers request names, URLs, params, and descriptions.
Share deep links. Every folder, request, and saved example has its own URL. The path is built from the names in the collection, so a request called Login inside an Authentication folder lives at #/authentication/login, and one of its examples at #/authentication/login/<example-name>. Copy the address bar and send it: the link opens the page on that exact request, survives a reload, and works with the browser's back and forward buttons. Playground state is in the URL too, so a link can land a reader in Try for a specific request.
Open in Bruno. Every generated site has an Open in Bruno button. For Git-backed collections the repository URL is embedded by default, so the button works like Fetch in Bruno and clones the collection straight from the docs. Without a repository link, the button opens a dialog instead: a Download Collection button that saves the collection as a .yml file, the two steps to import it in Bruno, and a download link for readers who do not have Bruno yet.
When a reader hits Try, the playground runs the same collection, folder, and request scripts the desktop app would, in that order, inside a browser sandbox that matches Safe Mode. The bru, req, and res objects are the ones you already use. Environment, collection, folder, and request variables, bru.setVar, bru.getSecretVar, bru.sendRequest, bru.setNextRequest, and bru.interpolate all work.
A few APIs need the desktop app and are not available in the browser: bru.runner.*, the OAuth2 credential helpers, bru.cookies.*, and bru.visualize. If a script calls one of them, the playground shows a warning in the response pane naming the API instead of failing quietly. Developer Mode features such as filesystem access and extra npm packages are also out of reach in the browser.
The practical rule: if a script works in Safe Mode in Bruno, it works for your readers.
The two routes stay current in different ways, and it is worth knowing which one you are relying on.
Git-shared collections update when readers pull; generated docs update when you regenerate and replace the file.
The Git route updates itself. A Fetch in Bruno button points at a repository, not at a copy. Push a change and every reader who cloned it can pull the new version from Bruno's Git UI. The button snippet in your README never needs to change.
The docs route is a build. The HTML file is a snapshot of the collection at the moment you generated it. To update public docs, regenerate and replace the deployed file. If that sounds like a chore, it is exactly the job for bru docs generate in CI: regenerate on every merge to main and publish the output to the same URL, so the docs can never drift behind the collection.
Publish more than one variant from the same collection. Tag filters are set per run, so you can generate once with an include tag like Production ready for the public file, then generate again with no filters for an internal copy. Both come from the same files, so nothing is maintained twice.
Set a collection version. Collections without one show Not Set in the generate modal and in the generated site. A version string in the collection overview gives readers a way to tell which iteration of the docs they are looking at.
Public means anyone. Run through this before the repository goes public or the HTML file goes live.
Bruno has two different things called secrets. A secret environment variable is a regular variable whose value is masked and kept off disk. An external secret is not stored in Bruno at all; it is fetched from a secrets provider when a request runs. Both stay out of anything you publish, for different reasons.
| Secret environment variable (masked) | External secret (fetched from a provider) | |
|---|---|---|
| How you set it | Mark a normal environment variable as Secret. Bruno masks it in the UI and stores the value encrypted on your machine instead of in the environment file | Add an entry on the environment's External Secrets tab that points at HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. Bruno fetches the value from the provider at run time; you never type it |
| In the Git repo | Name only, no value | Provider config and the secret's name or path, no value |
| In the generated docs | Name only, shown as (Secret) | Name only, no value; a browser cannot reach the manager |
In the playground, a reader who wants to try a request types their own value. That works for secret environment variables: the value lives in that browser tab and is never saved.
External secrets are different. The reference comes through as a variable with no value and no field to fill in, so a request that depends on one cannot be tried as written. Readers have two ways around that: copy the cURL, Python, or JavaScript sample and drop in their own credential, or use Open in Bruno and run the request in the desktop app, where their own environment and secrets provider apply. If you want the Try button to work for everyone, embed an environment for the docs in which that credential is a secret environment variable instead. Readers then get a field to fill in, and your internal environments keep the external secret.
What is not protected: anything typed straight into an Auth tab (a basic auth password, an OAuth2 client secret, AWS keys) is exported as written. Reference a secret environment variable or an external secret there instead, and move any token sitting in a plain variable or header to one of those before you publish.
Each environment you select goes into the file with its full variable set: staging and internal base URLs, account IDs, test data. Include only what a public reader should see, and use --exclude-envs in CI for the rest.
Excluded requests are absent from the file, not hidden. They are not in navigation, search, or the embedded collection data. If nothing is tagged yet, this is the moment to add tags.
Git-backed collections embed the repository URL by default, so Open in Bruno clones the repository. Turn it off in Advanced, or pass --no-git-link, when the repository is private and the docs are public.
Descriptions come from the markdown at the collection, folder, and request levels. Requests without docs still appear, with nothing to explain them. Fill in the ones a first-time reader hits first.
bru.runner, the OAuth2 credential helpers, bru.cookies, and bru.visualize show a warning in the playground. Readers can still send the request, but know which ones those are before a partner does.
No account, ever. For the Fetch in Bruno route they need the free Bruno desktop app installed, and the button clones the repository straight into it. For generated docs they need only a browser: the file is self-contained and the playground runs client-side.
No. Secret environment variables are exported as a name with no value and shown as (Secret). External secrets keep only their provider config. Readers who want to try a request type their own value in the playground, which stays in that browser tab and is never saved.
Yes. Generating the button, the reader's clone, fetch, and pull, initializing Git, and generating docs are all in the free, open-source edition. Only Git actions inside the app, such as connecting a remote and pushing, need Pro or Ultimate; free users do those from the terminal.
Yes. The output is one static HTML file with no server-side dependencies, so it works on GitHub Pages, Netlify, Vercel, any static host, or inside an existing developer portal. Regenerate and replace the file when the API changes, ideally from CI with bru docs generate.
Fetch in Bruno is the button you place on a README or website; it clones a public Git repository into the reader's app. Open in Bruno is the button inside generated docs: with a Git link it clones the same way, and without one it offers a .yml download plus import steps.
Regenerate the HTML file and replace the deployed copy. The CLI makes this a build step: run bru docs generate on every merge to main and publish the output to the same URL. Collections shared through Git need nothing from you; readers pull the new version themselves.
If you maintain an open-source API or a public collection, add a Fetch in Bruno button to the README today. It is one line of Markdown and it gives every visitor a working, Git-synced collection in one click.
If you have partners or customers who need a reference without the tooling, generate the docs once from the collection's Overview tab, open the file in a browser, and see what they would see. Then wire bru docs generate into the pipeline that already ships your API so the docs never fall behind it.
Better still, do both. Host the generated docs at a public URL and keep the Fetch in Bruno button beside them, so a reader can start in the browser and move into Bruno the moment they want to edit a request or write a script. Both come from the same repository, so publishing one more surface costs nothing to maintain.
Either way, the collection you already keep in Git is the only thing you maintain.
Ready to share your API? Bruno is free and open source, and everything in this post works from the first install.
Download BrunoSharing with your own team instead? How to Share Bruno Collections covers Git sync, exports, and imports.
Docs