A very limited alternative to Postman, powered by curl and POSIX shell.
Who want to send API requests without feeding the memory. And the one who familiar with shell.
-
Create a new script: Copy
sample.shto a new file, for example,my_api_tests.sh.cp sample.sh my_api.sh chmod +x my_api.sh
-
Set the Base URL: Edit
my_api_tests.shand change theBASE_URLvariable to your target API’s base URL.BASE_URL=https://api.example.com
-
Add Requests: Add your API requests using the
GET,POST,PUT,PATCH,DELETErequests. You can add any validcurloptions after the path.# Simple GET request GET /users/1 # POST request with a JSON body POST /users --json '{"name": "New User"}'
-
Run the script: Execute your script from the terminal.
./my_api.sh
-
Snapshot Responses: You can add
Snapshotafter the request. Run the script again, and the response will be captured and embedded directly into the script. This allows you to "record" the server’s response (like Postman’s example).GET /users/1 Snapshot
After running the script, it will become:
GET /users/1 : <<'SNAPSHOT' http_code 200 method GET size_download 509 time_total 0.479236 url_effective https://jsonplaceholder.typicode.com/users/1 date Sat, 10 Jan 2026 07:33:15 GMT content-type application/json; charset=utf-8 ------------------------- { "id": 1, "name": "Leanne Graham", "username": "Bret" } SNAPSHOT
💡The
:is the shell builtin that almost do nothing. Combined with here-document (<<'SNAPSHOT'), we can have the comment without preceding#.
Here is a more complete example of an API script:
#!/bin/sh
. ./poorman.sh
# /// configurations ///
# Base URL of each requests. Trailing slash is optional.
BASE_URL=https://jsonplaceholder.typicode.com
# Set to 1 if you only want to run requests starts with "Only"
ONLY=
# Set to 1 if you want to see the actual curl commands
DRY_RUN=
# /// curl options ///
# Add global curl option
CurlOptionGlobal --location
# /// hooks ///
AfterHook() {
# Prints response body for every request
printf '%s' "$BODY"
}
# /// requests ///
# Add option that only affect next request
CurlOptionOnce --user admin:password
# Get post by id
commentId=1
GET /comments/$commentId
# This line will replaced with the response body of previous request
Snapshot
# Like this:
# : <<'SNAPSHOT'
# http_code 200
# method GET
# size_download 268
# time_total 0.411915
# url_effective https://jsonplaceholder.typicode.com/comments/1
# date Sat, 10 Jan 2026 10:23:10 GMT
# content-type application/json; charset=utf-8
# -------------------------
# {
# "postId": 1,
# "id": 1,
# "name": "id labore ex et quam laborum",
# "email": "Eliseo@gardner.biz",
# "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
# }
# SNAPSHOT
# Create new post
POST /posts --json "$(
cat <<'PAYLOAD'
{
"title": "poorman",
"body": "A very limited alternative to postman",
"userId": 1
}
PAYLOAD
)"
Snapshot
# [SKIP] Get album's photo
album=3
Skip GET /albums/$album/photos
# Alter user name
userId=1
PATCH /users/$userId --json '{"name": "John Doe"}'
Snapshot| Name | Description | Example |
|---|---|---|
|
The prefix of all request URLs. A trailing slash ( |
|
|
Set to |
0/1 |
|
Set to |
0/1 |
| Method & Syntax | Description | Example |
|---|---|---|
|
Sends a GET request. |
postId=1
GET /posts/$postId |
|
Sends a POST request. |
POST /posts --verbose --json "$(cat <<'JSON'
{
"id": 1,
"title": "foo",
"body": "bar",
"userId": 1
}
JSON
)" |
|
Sends a PUT request. |
PUT /posts/1 --json "$(cat <<'JSON'
{
"id": 1,
"title": "foo",
"body": "bar",
"userId": 1
}
JSON
)" |
|
Sends a PATCH request. |
PATCH /albums/1/photos --json '{"date": 1767515098}' |
|
Sends a DELETE request. |
DELETE /posts --url-param 'user=1' |
| Name | Description |
|---|---|
|
The response body of latest request. |
|
All curl write-out variables in json format of latest request. ( |
|
curl write-out headers in json format of latest request. ( |
|
The content that will be used for constructing |
Sets curl options for the next request only.
Example:
# This applies only to the next GET request
CurlOptionOnce --header "X-Custom-Header: value"
GET /some/pathSets curl options for all subsequent requests.
Example:
# This applies to all subsequent requests
CurlOptionGlobal --location
GET /some/pathCaptures previous request result and replace itself.
GET /users/1
SnapshotAfter running the script, it will become:
GET /users/1
: <<'SNAPSHOT'
http_code 200
method GET
size_download 509
time_total 0.479236
url_effective https://jsonplaceholder.typicode.com/users/1
date Sat, 10 Jan 2026 07:33:15 GMT
content-type application/json; charset=utf-8
-------------------------
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret"
}
SNAPSHOTSkips the execution of a request.
Example:
# This request will not be executed
Skip GET /users/1With ONLY=1 environment variable set. Only the requests wrapped by Only will be executed.
Example:
ONLY=1
Only GET /users/1 # execute
POST /users --json '{"name": "New User"}' # skipped
Only DELETE /users/2 # executeOnly GET /users/1 # execute
POST /users --json '{"name": "New User"}' # execute, since ONLY is not equal to 1
Only DELETE /users/2 # execute