Shift Note: First Model API Call from a Shell
A model API call from a shell script has four moving parts: an endpoint, an API key, a JSON request, and a response you must check. This shift note walks through a non-streaming request with curl, then adds enough error handling to keep a failed call from masquerading as useful output.
Prepare a private working directory for the request
1. Open Bash and check that curl and jq are installed. curl sends the HTTP request; jq builds and reads JSON without fragile quoting tricks. Run the following commands individually as you follow the walkthrough.
curl --version; jq --version
Create a scratch directory outside your repository, change into it, and restrict permissions on newly created files. Requests and responses may contain sensitive material, so avoid shared folders.
mkdir -p model-api-check; cd model-api-check; umask 077
The examples below write local files for inspection. Delete them when finished, and do not commit API keys, prompts, or responses to source control.
Load the endpoint, key, and exact model identifier
2. Choose a provider that exposes an OpenAI-compatible chat completions endpoint. Relay365 fits when you want pay-as-you-go access to multiple model families through one relay rather than maintaining separate integrations. Its API base is https://api.api365relay.com; OpenAI-compatible requests use the /v1 prefix.
BASE_URL='https://api.api365relay.com/v1'
Read your API key without putting its literal value in shell history. Do not enable shell tracing with set -x while handling credentials.
read -r -s -p 'API key: ' API_KEY; printf '\n'
Copy the exact API model identifier from your provider’s current model listing. For example, you might choose Gemini 2.5 Flash, but do not assume its display name is also its request identifier.
read -r -p 'Exact API model identifier: ' MODEL
These variables stay in the current shell; they do not need to be exported because the shell substitutes their values into the commands.
Build valid JSON before opening the connection
3. Start with a short, harmless prompt. Let jq escape its contents instead of manually inserting text into JSON. This matters when later prompts contain quotation marks, backslashes, or line breaks.
PROMPT='Write three checks for a shell script that calls an HTTP API.'
jq -n --arg model "$MODEL" --arg prompt "$PROMPT" '{model: $model, messages: [{role: "user", content: $prompt}], stream: false}' > request.json
Inspect the generated payload before sending it. Keep provider-specific options out of this first request; supported parameters can differ by model and endpoint.
jq . request.json
Send the request and retain the HTTP status
4. Send the JSON file as the request body. Save the response separately from the status code so diagnostic messages cannot be mistaken for generated text.
if HTTP_STATUS=$(curl --silent --show-error --connect-timeout 10 --max-time 120 --output response.json --write-out '%{http_code}' --header "Authorization: Bearer $API_KEY" --header 'Content-Type: application/json' --data-binary @request.json "$BASE_URL/chat/completions"); then printf 'HTTP status: %s\n' "$HTTP_STATUS"; else printf 'Transport failure; inspect the curl error above.\n' >&2; fi
The timeout values are local operating choices, not service guarantees. A connection failure or timeout is different from an HTTP error response. Also, curl normally considers an HTTP error a completed transfer, which is why the status needs a separate check.
Validate success before extracting the assistant message
5. Continue only after curl completes successfully. For a non-2xx status, inspect the response instead of extracting assistant text. Authentication errors call for checking credentials; rate limits call for checking the provider’s guidance and any Retry-After header.
jq . response.json
For a successful chat completions response, extract the expected text field:
jq -er '.choices[0].message.content | strings | select(length > 0)' response.json
That expression fails when the expected text is absent. In an automated script, explicitly stop on transport failures, non-2xx responses, or failed extraction. Do not blindly retry generation requests: a timeout does not prove the server did no work, and another attempt may incur another charge.
Close the first-call check before scheduling automation
6. Once the request works manually, move the commands into a Bash script with explicit failure branches. Keep credentials in your deployment’s secret store, limit logged content, and set a spending boundary appropriate to the job.
If a shared, pay-as-you-go relay suits your workflow, create a Relay365 account and run this small check before wiring it into scheduled tasks. Finally, unset the key and remove the scratch files when they are no longer needed.
unset API_KEY; rm -f request.json response.json
Further desk bulletins
| Date | Bulletin title | Filed under | Language |
|---|---|---|---|
| 2026-10-26 | Shift Note: AI Tail Latency and Retry Errors | Wire & endpoints | EN |
| 2026-10-26 | Shift note: seven privacy checks for an API relay | Wire & endpoints | EN |
| 2026-10-26 | Shift log: lock down the AI API call path | Wire & endpoints | EN |
| 2026-10-26 | Shift log: bring a Slack model bot on duty | Wire & endpoints | EN |
| 2026-10-26 | Shift note: Node.js browser streams on duty | Wire & endpoints | EN |
| 2026-10-26 | Shift Note: Relay Handoffs and Off-Limits Controls | Wire & endpoints | EN |
Curious how it runs on your own code? Sign up →