A command-line HTTP client for testing APIs, written in PHP. Inspired by ht.nvim (HTTP Toolkit for Neovim).
- Define multiple HTTP requests in a single
.htfile - Support for global and local variables
- Variable substitution with
{{variable}}syntax - Environment variables support (
$VAR) - Command execution in variables (
>command,>>commandfor cached) - Pre and Post scripts (full PHP)
- Request chaining with
$api->send() - Output control with
$output->write()and$output->append() - JSON body support
- Headers and query parameters
- Configurable timeout, insecure SSL, proxy
- Script-only requests (requests without URL)
- Immediate request chaining in loops
chmod +x ht
./ht --helpht <file.ht> [requestName] # Run specific request (body only)
ht -a <file.ht> [requestName] # Run with all info (headers, status, etc.)
ht --all <file.ht> [requestName] # Same as -aIf requestName is omitted, defaults to main.
Default (body only):
$ ht file.ht myRequest
{"success":true,"data":{"id":1}}With -a flag (all information):
$ ht -a file.ht myRequest
[INFO] Executing: GET https://api.example.com/data
=== Request ===
GET https://api.example.com/data
Headers:
Content-Type: application/json
Body:
{"key":"value"}
===============
=== Response ===
Status: 200
Headers:
HTTP/1.1 200 OK
Content-Type: application/json
Body:
{"success":true,"data":{"id":1}}
===============### #requestName
METHOD https://api.example.com/endpoint
Header-Name: header-value
{
"body": "json"
}Lines starting with # are comments:
# This is a comment
### #myRequest
GET https://api.example.com
# Another commentGlobal variables (available to all requests):
@baseUrl = https://api.example.com
@apiKey = secret-key
### #main
GET {{baseUrl}}/users
Authorization: Bearer {{apiKey}}Local variables (available only in current request, override global):
### #getUser
@userId = 123
GET https://api.example.com/users/{{userId}}Variable types:
| Syntax | Description |
|---|---|
{{variable}} |
Regular variable |
{{$ENV_VAR}} |
Environment variable |
{{>command}} |
Execute command and use output |
{{>>command}} |
Execute command, cache result |
Variables starting with @cfg. configure the request:
@cfg.timeout = 5000 # Timeout in milliseconds
@cfg.insecure = true # Skip SSL verification
@cfg.dry_run = true # Don't send request
@cfg.proxy = http://proxy:8080### #apiRequest
GET https://api.example.com/data
Content-Type: application/json
Accept: application/json
X-Custom-Header: value### #search
GET https://api.example.com/search
name=John
age=30### #create
POST https://api.example.com/users
Content-Type: application/json
{
"name": "John",
"email": "john@example.com"
}Scripts are written in PHP and executed before or after the request.
### #login
POST https://api.example.com/login
Content-Type: application/json
{
"username": "admin",
"password": "secret"
}
<?php
// Access response
$api->set('token', $response->json_body()['token']);
$output->write('Login successful!');
// Available functions:
var_dump($var);
print_r($var);
json_decode($json);
json_encode($data);
?>### #modifyRequest
GET https://api.example.com/data
<?php
#pre
// Modify request before sending
$request->method = 'POST';
$request->url = 'https://api.example.com/create';
$request->headers['X-Custom'] = 'value';
$request->body = '{"modified": true}';
$request->query['page'] = '1';
$api->set('some_var', 'value');
#post
// This runs after response
$output->append('Request completed');
?>Variables set in pre-script are available for URL and query replacement:
### #main
GET https://api.example.com/search
name=test
<?php
#pre
$api->set('name', 'modified');
$request->query['page'] = 1;
?>You can create requests without URLs that only run scripts:
### #main
<?php
$api->set('counter', 0);
while($api->get('counter') < 10) {
$api->send('loop');
$api->set('counter', (int)$api->get('counter') + 1);
}
?>
### #loop
GET https://api.example.com/data?count={{counter}}$response (post-script only):
| Property/Method | Description |
|---|---|
$response->body |
Response body as string |
$response->status_code |
HTTP status code |
$response->headers |
Headers as associative array |
$response->json_body() |
Parse JSON body (cached) |
$request (pre-script only):
| Property/Method | Description |
|---|---|
$request->url |
Request URL |
$request->method |
Request method (GET, POST, etc.) |
$request->headers |
Headers as associative array |
$request->body |
Request body |
$request->query |
Query parameters as associative array |
$api:
| Method | Description |
|---|---|
$api->set(key, value) |
Set global variable |
$api->get(key) |
Get variable (global or local) |
$api->send(name) |
Execute another request immediately |
$output:
| Method | Description |
|---|---|
$output->write(text) |
Replace response body with custom text |
$output->append(text) |
Add text after response body |
Output behavior:
| Mode | write() |
append() |
No output call |
|---|---|---|---|
Without -a |
Custom text only | Body + custom | Body only |
With -a |
Full info + custom | Full info + custom | Full info |
Global functions:
var_dump($var)- Dump variableprint_r($var)- Print readablejson_decode($json, true)- Parse JSONjson_encode($data)- Encode to JSON
### #first
GET https://api.example.com/step1
<?php
$api->set('id', $response->json_body()['id']);
$api->send('second');
?>
### #second
GET https://api.example.com/step2/{{id}}The $api->send() method executes requests immediately, enabling loops:
### #main
<?php
$i = 0;
while($i < 10) {
$api->set('i', $i);
$api->send('loop');
$i++;
}
?>
### #loop
GET https://api.example.com/count?value={{i}}@host = jsonplaceholder.org
### #main
GET https://{{host}}/users/1### #create
POST https://api.example.com/users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}@host = api.example.com
### #login
POST https://{{host}}/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "secret"
}
<?php
$api->set('token', $response->json_body()['token']);
?>
### #getData
GET https://{{host}}/data
Authorization: Bearer {{token}}### #custom
GET https://api.example.com/data
<?php
// Replace response with custom output
$output->write('Request completed successfully!');
?>
### #append
GET https://api.example.com/data
<?php
// Add info after response
$output->append('Cached: yes');
?>### #main
GET https://api.example.com/search
name=test
<?php
#pre
$request->query['page'] = 1;
$request->query['limit'] = 10;
$api->set('name', 'modified');
?>Start the mock server:
php -S 127.0.0.1:8888 api.phpRun tests:
./run-tests.sh- PHP 8.1+