HTTP From the Command Line — The SysAdmin's Swiss Army Knife for APIs
curl is a command-line tool for transferring data with URLs. It supports HTTP, HTTPS, FTP, SFTP, and a dozen other protocols. For SysAdmins, it is the primary tool for testing endpoints, probing APIs, scripting against REST services, and — increasingly — talking directly to AI APIs from the shell.
curl has been on Unix systems since 1997. It is not new. What is new is the role it now plays in AI workflows: hitting local ollama endpoints, calling the Anthropic or OpenAI APIs directly, testing webhook receivers, and scripting AI integrations without a Python environment in the way.
curl is almost certainly already installed on every server you manage. The question is whether you are using it to its potential.
curl is installed by default on virtually every Linux distribution. Verify first:
curl --version
If for some reason it is missing:
# RHEL / AlmaLinux / Rocky / CentOS
dnf install curl
# Debian / Ubuntu
apt install curl
# Verify after install
curl --version
--fail-with-body. curl 7.82+ added --json as a shorthand.
AlmaLinux 9 ships with 7.76+. Check your version before assuming newer flags are available.
| Flag | Meaning | Common Use |
|---|---|---|
-s |
Silent — suppress progress meter | Script output, piping to jq |
-S |
Show errors even in silent mode | Use with -s always: -sS |
-o |
Output to file | Download a file |
-O |
Output to file named by URL | Quick downloads |
-L |
Follow redirects | Almost always use this |
-I |
HEAD request — headers only | Check server, content-type, redirects |
-v |
Verbose — show request and response headers | Debugging API calls |
-X |
Specify HTTP method | -X POST, -X DELETE |
-H |
Add a request header | Auth tokens, Content-Type |
-d |
Send POST data | Form data, JSON payloads |
--json |
Send JSON body (curl 7.82+) | Shorthand for Content-Type + Accept headers |
-u |
Username:password for Basic Auth | API basic auth, private repos |
-k |
Skip TLS certificate verification | Internal servers with self-signed certs |
-w |
Write-out format string after transfer | Print HTTP status code, timing |
--max-time |
Maximum total time in seconds | Prevent hung scripts |
--retry |
Retry on transient failure | Resilient scripts |
# Simple GET — fetch a page or API endpoint
curl https://example.com/api/status
# Silent GET — pipe output, suppress progress
curl -sS https://example.com/api/status
# Follow redirects (always a good habit)
curl -sSL https://example.com/
# GET with pretty JSON output (pipe to jq)
curl -sS https://api.example.com/v1/info | jq .
# Check headers only (HEAD request)
curl -I https://example.com/
# Verbose — see full request and response headers
curl -v https://example.com/api/
# Download a file
curl -sSL -o /tmp/package.tar.gz https://example.com/downloads/package.tar.gz
# Check HTTP status code only
curl -o /dev/null -sS -w "%{http_code}\n" https://example.com/
# POST form data
curl -sS -X POST -d "user=admin&action=restart" https://example.com/api/control
# POST JSON payload (manual headers)
curl -sS -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"key":"value","count":5}' \
https://api.example.com/v1/endpoint
# POST JSON (curl 7.82+ shorthand)
curl -sS --json '{"key":"value"}' https://api.example.com/v1/endpoint
# POST JSON from a file
curl -sS -X POST \
-H "Content-Type: application/json" \
-d @/tmp/payload.json \
https://api.example.com/v1/endpoint
# Basic auth
curl -sS -u username:password https://api.example.com/v1/resource
# Bearer token (API key in header)
curl -sS -H "Authorization: Bearer YOUR_API_KEY" https://api.example.com/v1/resource
# API key as header (varies by service)
curl -sS -H "x-api-key: YOUR_API_KEY" https://api.example.com/v1/resource
# Check if a web service is responding
curl -o /dev/null -sS -w "%{http_code}\n" https://myapp.example.com/health
# Check multiple endpoints in a loop
for host in web01 web02 web03; do
code=$(curl -o /dev/null -sS -w "%{http_code}" --max-time 5 https://${host}.example.com/health)
echo "${host}: HTTP ${code}"
done
# Time the response — useful for performance baselining
curl -o /dev/null -sS -w "Connect: %{time_connect}s Total: %{time_total}s\n" \
https://myapp.example.com/
# Full timing breakdown
curl -o /dev/null -sS -w \
"DNS: %{time_namelookup}s Connect: %{time_connect}s TLS: %{time_appconnect}s Total: %{time_total}s\n" \
https://myapp.example.com/
# Verbose output shows certificate details
curl -vI https://example.com 2>&1 | grep -A5 "Server certificate"
# Check expiry — pipe to openssl
echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# Quick cert check via curl verbose headers
curl -sS --max-time 5 -vI https://example.com 2>&1 | grep "expire date"
# Download and install a binary release
curl -sSL https://github.com/project/releases/latest/download/tool-linux-amd64 \
-o /usr/local/bin/tool
chmod +x /usr/local/bin/tool
# Download with integrity check (compare sha256)
curl -sSL https://example.com/package.tar.gz -o /tmp/package.tar.gz
curl -sSL https://example.com/package.tar.gz.sha256 -o /tmp/package.tar.gz.sha256
sha256sum -c /tmp/package.tar.gz.sha256
# Test an internal REST API behind a firewall
curl -sS http://internal-api.corp:8080/v1/status | jq .
# Send a test webhook payload
curl -sS -X POST \
-H "Content-Type: application/json" \
-d '{"event":"test","source":"sysadmin-check"}' \
http://webhook-receiver.corp:9000/webhook
# Test with self-signed cert (internal servers)
curl -sSkL https://internal-secure.corp/api/health
# Test with client certificate
curl -sS --cert /etc/ssl/client.crt --key /etc/ssl/client.key \
https://mtls-api.example.com/v1/resource
#!/bin/bash
# Simple HTTP monitor with alert
URL="https://myapp.example.com/health"
THRESHOLD=5
code=$(curl -o /dev/null -sS -w "%{http_code}" --max-time ${THRESHOLD} "${URL}" 2>&1)
time=$(curl -o /dev/null -sS -w "%{time_total}" --max-time ${THRESHOLD} "${URL}" 2>&1)
if [ "${code}" != "200" ]; then
echo "ALERT: ${URL} returned HTTP ${code}" | mail -s "Service Alert" ops@example.com
fi
# Query the GitHub API for release info
curl -sS https://api.github.com/repos/BurntSushi/ripgrep/releases/latest | \
jq '.tag_name, .published_at'
# Get your public IP
curl -sS https://ifconfig.me
# Query AWS instance metadata (on EC2)
curl -sS http://169.254.169.254/latest/meta-data/instance-id
# Kubernetes API (with service account token)
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -sS -H "Authorization: Bearer ${TOKEN}" \
--cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
https://kubernetes.default.svc/api/v1/namespaces/default/pods | jq '.items[].metadata.name'
This is where curl becomes a first-class citizen in the AI toolchain. Every AI service with an API — Anthropic, OpenAI, local ollama — is reachable with curl. No Python, no SDK, no virtual environment. Just a shell and an API key.
# Basic Claude API call from the shell
curl -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-4-5",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Explain what causes high iowait on Linux in 3 sentences."}
]
}' | jq '.content[0].text'
export ANTHROPIC_API_KEY="sk-ant-..." to your ~/.bashrc
and keep that file permission 600.
#!/bin/bash
# ask_claude.sh — pipe any text to Claude from the shell
# Usage: echo "question" | ./ask_claude.sh
# cat logfile.txt | ./ask_claude.sh
INPUT=$(cat)
curl -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"claude-opus-4-5\",
\"max_tokens\": 2048,
\"messages\": [
{\"role\": \"user\", \"content\": $(echo "${INPUT}" | jq -Rs .)}
]
}" | jq -r '.content[0].text'
# Use it:
rg "OOM kill" /var/log/messages | ./ask_claude.sh
# or:
echo "What is the difference between RSS and VSZ memory?" | ./ask_claude.sh
jq -Rs . trick converts multi-line stdin into a properly escaped
JSON string. Essential for sending log output or file content as a JSON payload
without manual escaping headaches.
# ollama exposes a REST API on localhost:11434
# No API key needed — runs entirely on your server
# Basic ollama query
curl -sS http://localhost:11434/api/generate \
-d '{
"model": "llama3",
"prompt": "What are the most common causes of high load average on Linux?",
"stream": false
}' | jq '.response'
# List available models
curl -sS http://localhost:11434/api/tags | jq '.models[].name'
# Check ollama health
curl -sS http://localhost:11434/api/tags > /dev/null && echo "ollama up" || echo "ollama down"
# Full pipeline: rg extracts, curl sends to local LLM, jq formats output
rg -C 3 "segfault\|kernel panic\|OOM" /var/log/messages | \
jq -Rs '{model: "llama3", prompt: ("Analyze these kernel messages and suggest root cause: " + .), stream: false}' | \
curl -sS -X POST http://localhost:11434/api/generate \
-H "Content-Type: application/json" \
-d @- | \
jq -r '.response'
-d @- flag tells curl to read the POST body from stdin. This
completes the pipeline: rg → jq (build payload) → curl (send) →
jq (extract response). No temp files, no intermediate scripts.
# OpenAI API — same pattern as Anthropic, different structure
curl -sS https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Summarize iowait troubleshooting steps."}]
}' | jq '.choices[0].message.content'
# Any OpenAI-compatible API (LM Studio, LocalAI, vllm) uses the same pattern
# Just change the base URL:
curl -sS http://localhost:1234/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"local-model","messages":[{"role":"user","content":"Hello"}]}' | \
jq '.choices[0].message.content'
# Fetch API response and extract nested field
curl -sS https://api.example.com/v1/servers | jq '.servers[] | {name: .hostname, ip: .ip_address}'
# Filter by condition
curl -sS https://api.example.com/v1/services | jq '.[] | select(.status != "running")'
# Format into a table for humans
curl -sS https://api.github.com/repos/cli/cli/releases | \
jq -r '.[] | [.tag_name, .published_at[:10], .name] | @tsv' | \
column -t
# Fetch a remote config and search it
curl -sS https://raw.githubusercontent.com/project/repo/main/config.yaml | \
rg "timeout|retry|max_conn"
# Fetch a log from a remote API and extract errors
curl -sS https://logservice.corp/api/logs?host=web01 | \
jq -r '.entries[].message' | \
rg "ERROR|CRITICAL"
# Monitor a status page for a specific string
curl -sS https://status.example.com/ | rg "Operational\|Degraded\|Outage"
# Add to ~/.bashrc for quick AI access from any prompt
ask() {
curl -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"claude-opus-4-5\",
\"max_tokens\": 1024,
\"messages\": [{\"role\": \"user\", \"content\": $(echo "$*" | jq -Rs .)}]
}" | jq -r '.content[0].text'
}
# Usage:
ask "What does the sticky bit do on a directory?"
ask "Give me a one-liner to find files modified in the last 24 hours"
curl reads ~/.curlrc on every invocation. A sensible baseline for
SysAdmin use:
# ~/.curlrc
# Always follow redirects
location
# Show errors even in silent mode (use -s safely)
show-error
# Set a reasonable timeout (seconds)
max-time = 30
# Retry on transient failures
retry = 3
retry-delay = 2
# Compressed responses (saves bandwidth)
compressed
# Use a descriptive User-Agent
user-agent = "sysadmin-curl/1.0"
-k (insecure) in ~/.curlrc. It is tempting
for internal servers with self-signed certs, but it silently disables certificate
verification for every curl call, including ones to external services. Use -k
explicitly on the command line when you need it.
show-error option in ~/.curlrc means you can safely
use -s in scripts without losing error messages. Silent mode suppresses
the progress meter; errors still surface. This is the right default for scripting.
-w write-out format — it is
built to be embedded in shell scripts and driven programmatically.jq -Rs . pattern and -d @file flag help significantly.
For complex payloads, write JSON to a temp file.-c/-b) exists but is primitive. For multi-step
authenticated API workflows, the shell scaffolding gets complex fast. At that point,
a Python script may be the cleaner tool.--fail or check %{http_code} with -w explicitly
in scripts. Missing this is a common source of silent failures in monitoring scripts.--no-buffer
but parsing the stream in shell is awkward. For interactive AI chat workflows, sgpt
or a dedicated client is cleaner.curl is not a new tool and it is not glamorous. What it is, is universal, reliable, and already present. For SysAdmins who want to interact with AI APIs from the shell without introducing new dependencies, curl is the correct starting point.
The mental model shift is simple: AI APIs are REST APIs. You already know how to call REST APIs with curl. The API key goes in a header. The prompt goes in a JSON body. The response comes back as JSON. Parse it with jq. That's the whole pattern.
Master the jq -Rs . trick, store your API key in the environment,
and write one reusable shell function. You now have AI access from every terminal
on every server where you have a shell.