ToolsTray

Star a tool to keep it here.

API Client

A REST client in a tab: build the request, press Send, and when a call gets blocked, read the CORS reason in words plus a server snippet.

Collections

Cookies

Request

Untitled request

Query parameters

Query parameters are added to the URL. Use :name in the path for a path variable.

Response

No response yet

Type a URL and press Send.

The request goes straight from this page to that address. Anything your own browser can reach with CORS enabled works with no extra setup.

What goes out

No URL yet

—sends

How to use

  1. Type the address into the URL box, or paste a curl command there and let it come apart into the panels below.
  2. Pick the method from the dropdown to the left of the URL. If you are sending something other than plain HTTP, change the protocol in the dropdown before it.
  3. Fill in whatever the call needs under Params, Authorization, Headers and Body. The last row of every grid is blank until you type in it, and a fresh blank one appears underneath.
  4. Press Send, or Ctrl+Enter. The status lands at the head of the response with its time and size under it. Pretty renders JSON as a tree you can collapse, Raw shows the bytes as they arrived, and whichever you pick is the one the next response opens in. The Sent tab shows exactly what went on the wire once variables and auth were applied.
  5. Press Save, or Ctrl+S, to name the request and drop it into a collection. The dot on its tab is how you know something is unsaved.

About this tool

Every request starts the same way. An address in the URL box, a method from the dropdown beside it, then Send. What makes an API client worth keeping open is the hundred small things around that loop, and most of them are here. Seven methods and six protocols in those two dropdowns. Params, Authorization, Headers, Body, Scripts and Settings on their own tabs, and the ones that hold rows carry a count of what is switched on inside them. Put :id in the path and a row for id appears under Params. Press / while you are not typing and the cursor lands back in the URL box. Ctrl+Enter sends.

One catch is worth knowing before you start. A page can only reach an API that answers with CORS headers, and a desktop client never runs into that, which is how a team ends up shipping broken CORS and hearing about it from a user. When a call is blocked here, the response area works out which of eight failure shapes it hit and says so in words. The preflight wasn't answered. Blocked as mixed content. Nothing answered at that host. Under the diagnosis sits a button marked Fix it on my server and a menu of ten frameworks, Express through ASP.NET Core, each with a snippet to paste.

Requests get names and live in collections. Environments hold the {{variables}} you write anywhere in the request, and the eye beside the environment picker shows what each one currently resolves to. Set a variable's type to secret and it gets masked on screen and stripped out of share links. History keeps what you sent by hand. Autosave sits at the foot of the request and response panes and writes about a second after you stop typing, if you want it on.

Past plain HTTP, the protocol menu carries WebSocket, server-sent events, gRPC, MQTT and Socket.IO, each logging frames as they arrive instead of waiting on one response body. Import reads a Postman, Insomnia or Bruno export, an OpenAPI or Swagger spec in JSON or YAML, a HAR recording from your network panel, and a .env file. Code & share writes the request out in about thirty languages, Python requests through PowerShell, and copies a link that carries the whole request in the URL fragment.

Paste a curl command, watch it come apart

Take a curl command out of your terminal and paste it into the URL box. Say it is curl -X POST https://api.example.com/v1/orders -H 'Content-Type: application/json' -d '{"sku":"AB-12","qty":2}'. The method flips to POST, Content-Type lands as a row under Headers, and the JSON drops into Body with the editor already set to JSON. It has not gone anywhere yet.

Swap the host for your staging box and press Ctrl+Enter. Copy as cURL, under What goes out, does the same trip backwards for whatever you have built, with variables and auth already resolved into the command it hands you.

Good to know

What this tool leaves out

  • Saved work lives in this browser profile's local database. Set a passphrase under Secrets at rest in the rail and every value marked secret is encrypted with AES-GCM behind a PBKDF2 key; skip that and your tokens and variables sit there as readable text for anyone who can open this browser. Export writes a bundle if you want the work on another machine.
  • A script gets five seconds of its own time before the sandbox stops it. None of Postman's bundled libraries are in there. Ask for lodash, chai, cheerio, crypto-js, ajv, moment, uuid or xml2js and each one names itself and says what to reach for instead, rather than failing as an undefined variable. Signing happens a layer up, on the page, which does have Web Crypto.
  • Scripts that arrive with an import or a shared link stay inert until you have read them and allowed them once. A collection run holds to the same rule. It refuses to start rather than executing code you have never looked at.
  • setNextRequest moves within the current iteration only, and passing null ends that iteration rather than the whole run. Postman ships no guard against a script that jumps in a loop forever. A run here gives up after five times its request count and names the script that caused it.
  • pm.sendRequest reaches the network, but it skips the collection's auth. Neither it nor a collection run is written to History. A two-hundred-request run would otherwise evict everything you had sent by hand.
  • Run results live in memory. Switch tools or reload and they are gone, so export the ones that mattered. The export is the shape Postman's own runner writes, which carries no response bodies and no headers.
  • A data file has to be CSV or JSON. Every value in it arrives as text, numbers included, because a variable holds a string.
  • Nothing here is scheduled, cloud-hosted, monitored or load-tested. A run happens in the tab you are looking at, for as long as that tab stays open.
  • The language list in Code & share covers plain HTTP. Anything else gets the client that really speaks it: ws, websockets or gorilla for a socket; EventSource, sseclient or curl -N for a stream; grpcurl, buf curl or curl for a gRPC call; MQTT.js, paho-mqtt or paho.mqtt.golang for a broker session; socket.io-client, python-socketio or raw frames for websocat. Each snippet states what it cannot carry rather than writing something that would fail on the first run. EventSource sends no headers and only ever sends a GET. curl has no way to write the length-prefixed frame the binary gRPC-Web wires use. paho.mqtt.golang implements 3.1.1 only, so it declines a 5.0 session and names the library that handles one. A GraphQL subscription gets no snippet at all. It is a handshake, then a payload, then an acknowledgement, then a subscribe, and no single command line describes that.
  • Fetch schema reads the endpoint's own introspection, and everything downstream comes out of it. Docs walks the schema type by type. Ctrl+Space offers whatever belongs at the caret: a type's fields, a field's argument names, an enum's values, the fields of an input object typed inside a literal, the directives the schema declares, the fragments whose type can actually overlap the one you are in. Completion is asked for rather than fired on every keystroke, and that is a choice. What the schema cannot say, the tool will not invent. An endpoint with introspection switched off says so. A fragment defined in a different request is unknown. The schema is the one read at the last Fetch rather than one that is watched. Deprecated fields and enum values sit behind a toggle, though a server that deprecates an argument keeps that argument hidden either way, because asking for those breaks older servers outright.
  • A GraphQL subscription answers the handshake's missing headers with the protocol's own device, the connection payload. There is a field for it on the request, sent as connection_init the moment the socket opens, and header rows are reported as dropped instead of quietly ignored. Both subprotocols are offered and the server picks: graphql-transport-ws, or the older graphql-ws that Apollo's subscriptions-transport-ws speaks. A server that will speak neither refuses the handshake, and a browser has to report that as code 1006 with no reason, so the tool lists the possibilities instead of picking one.
  • An MQTT session speaks 3.1.1 or 5.0 over a WebSocket, and the whole exchange lands in the log as frames. CONNECT out, CONNACK back, then SUBSCRIBE, then every PUBLISH with its topic beside it. Missing is the part a long-lived client owns: no last will and testament, no session that persists across reloads, no shared subscriptions, no topic aliases. A refused CONNACK is read for its reason code and stated in words, and it ends the session instead of retrying into the same refusal.
  • A Socket.IO connection is assembled the way a browser client assembles it: the Engine.IO path, EIO=4, the WebSocket transport, then a CONNECT into the namespace carrying the auth payload your middleware reads. Both layers show up in the log as their own frames rather than hidden inside a client. Left out is what a long-lived client owns: no automatic reconnection with its backoff, no acknowledgement callbacks waiting on a reply, no rooms, no binary attachment on an emit, though one that arrives is read and assembled.
  • Fetch schema asks the server's own reflection service, and that service is opt-in. When it cannot work the tool says which of three things happened: nothing registered, a server that refuses the single-message form a browser can send, or CORS. All three end the same way, with the .proto loaded by hand. A .proto that imports another file needs that file too. Google's well-known types resolve on their own, and anything else is named in the failure rather than surfacing later as a missing type.
  • Bruno keeps collections in three shapes and all three are read: a single .bru file, Bruno's JSON export, and the OpenCollection .yml its Export writes. Each names the same fields differently, so each is mapped by name rather than copied across. What OpenCollection cannot bring is a body that points at a file, because the path belongs to the machine that wrote the collection, and the setup screen says which request arrived empty. A recorded WebSocket in a HAR file comes across as its address and nothing else, since its frames are a transcript of one session rather than a request you could send again.
  • An OpenAPI spec is resolved against itself and no further. A reference into another file is reported instead of fetched. Where a body is described by a oneOf or an anyOf, the first branch is the one used and the import names it. A cookie parameter is reported rather than imported, because the browser owns that header. A body in a format no schema can be sampled into, XML or CSV, arrives with its media type and editor already set and the text left to you.
  • A folder or collection tab comes back after a reload the way a request tab does, but which of its four sections you were reading is not remembered. It reopens on Scripts.

What a browser cannot do

  • A page can only reach APIs that answer with CORS headers. Public APIs, your own services and anything with a web front end normally qualify; a third-party API you do not control may not. The extension and the proxy sit outside that rule, and a blocked request says which of them it needs rather than reporting a bare network error.
  • A response's headers are hidden from the page unless the server chooses to expose them, so on the Browser transport the headers table shows what it was handed and says plainly that the list is partial. Sent through the extension or the proxy, it is the whole list.
  • Host, Origin, Cookie, User-Agent and the rest of the forbidden list are set by the browser itself and cannot be overridden from a page. The tool marks those rows rather than pretending they were sent; the extension and the proxy do send them.
  • Redirect hops, phase-by-phase timings and a cookie jar are invisible to page JavaScript, so a send on the Browser transport reports total wall-clock and nothing finer. The extension adds the redirect chain, the cookies and coarse phases; the proxy adds real DNS, TCP and TLS times.
  • Digest and NTLM need a challenge-response handshake a page cannot take part in. It never sees the 401 that starts it. Both work through the extension, which answers the challenge and sends your credentials for real; without it installed the tool says so rather than sending something certain to be refused. Client certificates have no browser API at all, extension or not.
  • A WebSocket handshake opened from a page carries no headers of its own, because the constructor takes none, not Authorization and not a custom one. Whatever the request sets is reported as dropped when the socket opens, and the ws, websockets and gorilla snippets send those headers for you, because a client outside a browser can.
  • Native gRPC cannot be spoken from any browser: it needs HTTP/2 trailers and control of binary framing, and fetch exposes neither. What works here is gRPC-Web, which a proxy such as Envoy or a native gRPC-Web server answers, and Connect, which is a plain POST. A gRPC server with neither in front of it is not reachable from a tab, by this tool or any other.
  • Client-streaming and bidirectional gRPC methods are listed with their reason and left out. The gRPC-Web specification leaves client streaming pending on streamable request bodies, and Connect needs HTTP/2 for both, so offering them would be offering something broken.
  • MQTT reaches a browser only over WebSocket, so a broker listening on 1883 or 8883 alone cannot be opened from any tab. It needs a WebSocket listener, which is 8083 and 8084 by convention. The specification requires every packet to ride a binary frame and the handshake to offer the mqtt subprotocol, and both are what this sends; a broker that answers with a text frame is refused rather than guessed at.
  • Engine.IO's long-polling transport is not offered here, only the WebSocket one. Polling exists for browsers that cannot open a socket, and this tool needs one either way; a server that will not upgrade is out of reach here where its own client would have fallen back.
  • A script runs on an opaque origin, which is not a secure context, so the sandbox has no crypto.subtle and no crypto.randomUUID however the browser is configured. That is the reason the {{$guid}} dynamic variable and the built-in signers exist.

Frequently asked questions

The same URL works in curl. Why does it fail here?

Because curl is not a browser and skips CORS entirely, while a page has to be granted permission by the server it calls. The response area works out which part broke, whether the host never answered at all, a preflight went unanswered, or the allow-origin header is missing, and offers a server-side snippet for ten frameworks. If the API belongs to somebody else and you cannot change what it sends back, no browser tool gets past this one.

Can this replace Postman for day-to-day work?

For building, organising and firing requests, mostly. You get collections and folders, environments, pre-request and post-response scripts written against the pm API, pm.test assertions with results on their own response tab, and a runner that takes an iteration count and a CSV or JSON data file. What is missing is the platform around all that: cloud sync, team workspaces, scheduled monitors and load testing all stay with Postman.

How do I get my Postman collection in here?

Press Import and hand it a v2.1 collection or environment export. The folder tree, requests, headers, bodies, variables, auth and the saved responses Postman keeps on each request all come across. Postman names most auth fields differently, so each one is mapped by name rather than copied, and a field with no home here gets dropped instead of guessed at. That same button also takes an OpenAPI or Swagger spec in JSON or YAML, an Insomnia export, a Bruno export or a single .bru file, a HAR recording, and a .env file.

Can I really call a gRPC service from a browser tab?

Not native gRPC. No page can, because it wants HTTP/2 trailers and control of binary framing, and fetch exposes neither. What does work is gRPC-Web, the shape Envoy's grpc_web filter and native gRPC-Web servers answer, and Connect, which is an ordinary POST carrying JSON. Pick gRPC from the protocol menu, load a .proto or press Fetch schema to ask the server's own reflection service, choose the method, and the message gets encoded and framed for you. The gRPC status is shown next to the HTTP one, since a failure usually arrives as a 200 with grpc-status hiding in the trailers.

Which authentication methods are built in?

Basic, Bearer token, API key in a header or a query parameter, and JWT bearer signed with HS256, HS384 or HS512. Then the signed ones. AWS Signature v4 handles temporary session credentials and S3, OAuth 1.0 offers HMAC-SHA1, HMAC-SHA256 and PLAINTEXT, and Hawk takes an optional payload hash. OAuth 2.0 runs authorization code with PKCE or client credentials, and refreshes a stale token for you. Every signer is graded against its own published vectors, from Amazon's test suite to the worked examples in RFC 5849 and the PKCE challenge in RFC 7636. RSA-SHA1 is the gap, and the panel says why it is one: signing with it wants a PKCS#8 private key, and this tool imports no private key anywhere. Digest and NTLM are here as well, answered by the extension, because the challenge they depend on never reaches a page.

Where does my work actually get saved?

In this browser's local database, tied to this profile. It survives a reload and a restart, and it will not follow you to a different browser or a different machine. Export writes a bundle you can import wherever you need it, and you choose at export time whether secrets are stripped out, encrypted behind a passphrase, or included as they stand.

Which redirect URL do I register for OAuth 2.0?

https://toolstray.com/oauth/callback, exactly that string, for every provider. Register it in the provider's app settings before you try the flow, because most of them refuse anything that is not a character-for-character match with a URL you registered in advance. The Authorization tab shows the same URL with a copy button once you pick OAuth 2.0, and it reads the address from wherever the tool is running, so a local copy shows its own. The page it points at hands the sign-in result back to the tab that opened it, then closes.

Missing something in API Client — Test REST, GraphQL and gRPC APIs in Your Browser? Suggest a feature →

Tell someone who needs this.