Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spall

Break free. Hit the endpoint.

spall is a dynamic CLI tool that parses OpenAPI 3.x specifications at runtime and generates fully-featured command-line interfaces for making API requests — with validation, auth, colored output, and schema-aware help.

A spall is the fragment that breaks free from a corroding metal surface and flies. Your request — shaped by the spec, launched from the terminal, sent across the gap.

Think Restish, but Rust.

Features

  • Dynamic CLI from OpenAPI specs — no codegen required.
  • Runtime spec loading from file path or URL.
  • Two-phase parsing for fast startup and rich per-operation help.
  • Schema-aware argument validation and typed flags.
  • Colored, formatted response output with TTY detection.
  • Request history, shell completions, pagination, and JMESPath filtering.

Quick Usage

# Register an API
spall api add petstore https://petstore.swagger.io/v2/swagger.json

# List operations
spall petstore --help

# Make a request
spall petstore get-pet-by-id 1

# POST with a body
spall petstore add-pet --data '{"name":"Rex","status":"available"}'

Next Steps

Installation

Spall ships as a single static binary with no runtime dependencies.

Download

Pre-built binaries are available for Linux, macOS, and Windows on the releases page.

# Linux / macOS
curl -sL https://github.com/rustpunk/spall/releases/latest/download/spall-$(uname -s)-$(uname -m) -o spall
chmod +x spall
sudo mv spall /usr/local/bin/

# Verify
spall --version

Build from Source

Requires Rust 1.75+.

git clone https://github.com/rustpunk/spall.git
cd spall
cargo build --release --workspace

# Binary will be at:
# ./target/release/spall

Shell Completion Setup

Generate completion scripts for your shell:

# Bash
spall completions bash > /etc/bash_completion.d/spall

# Zsh
spall completions zsh > "${fpath[1]}/_spall"

# Fish
spall completions fish > ~/.config/fish/completions/spall.fish

Next Steps

Your First Request

This walkthrough registers the classic Petstore API, explores its operations, and makes a few requests.

1. Register the API

spall api add petstore https://petstore.swagger.io/v2/swagger.json

Spall will fetch the spec, cache it locally, and build an internal index. You only need to do this once.

2. List registered APIs

spall api list

Output:

Registered APIs:
  petstore             https://petstore.swagger.io/v2/swagger.json

3. Explore operations

spall petstore --help

Spall loads the spec and prints every operation, grouped by OpenAPI tags. Operation names are derived from operationId (kebab-cased) or synthesized from the HTTP method and path.

4. Make a GET request

Path parameters become positional arguments. Query parameters become --flags.

# GET /pet/{petId}
spall petstore get-pet-by-id 1

If the terminal is a TTY, the response is pretty-printed JSON with syntax highlighting. If piped, raw JSON is emitted.

5. Make a POST request

Post a JSON body with --data:

spall petstore add-pet --data '{"name":"Rex","status":"available"}'

You can also read body data from a file or stdin:

spall petstore add-pet --data @new-pet.json
# or
cat new-pet.json | spall petstore add-pet --data -

6. Override the server

Point the same operation at a staging server without re-registering:

spall petstore get-pet-by-id 1 --spall-server https://staging.petstore.io

7. Dry-run and preview

See what spall will send without hitting the network:

spall petstore get-pet-by-id 1 --spall-dry-run

Or preview the fully resolved request (URL, headers, body):

spall petstore add-pet --data '{"name":"Rex"}' --spall-preview

What just happened

  1. Phase 1 — spall scanned your config registry and matched petstore as a registered API name.
  2. Phase 2 — spall loaded the cached spec, resolved all $refs, merged parameters, and built a dynamic clap command tree.
  3. Execution — spall validated your inputs against the schema, built the HTTP request, sent it, and formatted the response.

Next Steps

Key Concepts

Two-Phase Parse

Spall uses a two-phase argument parser so that --help feels instant even when the underlying spec is large.

Phase 1 — Index scan (~1ms) reads only your config files (~/.config/spall/config.toml, apis/*.toml, and any spec_dirs). It registers each API as a clap subcommand stub with disable_help_flag(true) so that --help falls through.

Phase 2 — Spec load (~50–200ms) happens only when you actually invoke an API. The spec is loaded from cache or fetched from its source, $refs are resolved, and a full clap command tree is built. If the spec is unreachable, spall falls back to a lightweight cached SpecIndex so you still see the operation list.

Dynamic Command Building

Unlike tools that generate static Rust code from a spec, spall constructs clap Command and Arg objects at runtime. This means:

  • No recompilation when an API changes.
  • Schema enums become clap possible_values.
  • Defaults from the spec become clap default_value.
  • No generated code bloat.

Parameter Namespacing

OpenAPI allows a path param id and a query param id on the same operation. Spall namespaces them internally while preserving user-facing names:

OpenAPI inInternal IDUser-facing
pathpath-idpositional argument
queryquery-id--id
headerheader-id--header-id
cookiecookie-id--cookie-id

IR Cache

After the first parse, spall serializes the resolved spec to a compact binary format (postcard) keyed by SHA-256 of the raw spec bytes. On subsequent runs it skips YAML/JSON parsing and $ref resolution entirely. Cache invalidation is automatic when the spec content changes or when spall upgrades its IR format.

Credential Stack

Auth resolution follows a strict priority chain so that scripts, CI, and interactive sessions can all coexist:

  1. --spall-auth CLI override
  2. Per-API config [auth] section
  3. Environment variable (SPALL_<API>_TOKEN)
  4. Interactive prompt (Basic auth only, TTY)

All credentials are wrapped in secrecy::SecretString — they are zeroized on drop and redacted from debug output and history.

Exit Codes

Spall returns structured exit codes so scripts can branch on outcome:

CodeMeaning
0Success (2xx response)
1CLI usage error
2Network / connection error
3Spec loading / parsing error
4HTTP 4xx response
5HTTP 5xx response
10Request body / parameter validation failed

Next Steps

Registering APIs

Before you can call an API, spall needs to know where its OpenAPI 3.x spec lives.

Add an API

spall api add <name> <source>

The source can be a local file path or a URL:

# Local file
spall api add internal ./specs/internal-api.yaml

# Remote URL
spall api add petstore https://petstore.swagger.io/v2/swagger.json

This creates ~/.config/spall/apis/{name}.toml. The spec is fetched, cached, and indexed immediately.

List registered APIs

spall api list

Output:

Registered APIs:
  petstore             https://petstore.swagger.io/v2/swagger.json
  internal             ./specs/internal-api.yaml

Remove an API

spall api remove petstore

This deletes ~/.config/spall/apis/petstore.toml and invalidates the IR cache.

Refresh a cached spec

Remote specs are cached with a TTL and conditional GET (ETag). If the spec has changed on the server, refresh it:

# Refresh one API
spall api refresh petstore

# Refresh everything
spall api refresh --all

Refresh also invalidates the IR cache so the next request rebuilds it from the new spec.

Discover an API from a base URL

If a server advertises its OpenAPI spec via RFC 8631 service-desc link relation, spall can probe and auto-register:

spall api discover https://api.example.com

Spall will follow Link: <...spec...>; rel="service-desc" headers, derive a name from the spec title, and register it just like api add.

Auto-scanning spec directories

You can point spall at a directory full of spec files instead of registering each one manually:

# ~/.config/spall/config.toml
spec_dirs = [
    "~/.config/spall/specs",
]

Files in these directories are auto-registered. Names are derived from the filename minus extension:

FilenameRegistered name
petstore.jsonpetstore
my-internal-api.yamlmy-internal-api
v2_billing.ymlv2-billing

Priority (highest → lowest):

  1. apis/*.toml files
  2. [[api]] inline entries in config.toml
  3. spec_dirs scanned files

Lower-priority entries with duplicate names are discarded.

Next Steps

Making Requests

Once an API is registered, every OpenAPI operation becomes a subcommand. Parameter types, enums, and defaults are preserved from the spec.

Path Parameters

Path parameters are supplied as positional arguments in the order they appear in the path template.

Given a path /pets/{petId}:

spall petstore get-pet-by-id 1

Path parameters are always required. If you omit one, clap prints a usage error before any network activity occurs.

Query Parameters

Query parameters become --flags. Given an operation with in: query params status and limit:

spall petstore find-pets-by-status --status available --limit 10

If the spec declares an enum for status, spall registers it as clap possible_values so typos are caught immediately:

spall petstore find-pets-by-status --status availbale
# error: invalid value 'availbale' for '--status'
#   [possible values: available, pending, sold]

Defaults from the spec are honored:

# If limit defaults to 20 in the spec, this is equivalent:
spall petstore find-pets-by-status --status available

Header Parameters

Header parameters become --header-{name} flags:

spall petstore create-order --header-x-request-id abc-123

Cookie parameters become --cookie-{name} flags:

spall petstore login --cookie-session abc123

Spall collects all cookie params and sends them as a single Cookie header.

Injecting Arbitrary Headers

For headers not declared in the spec, use --spall-header (repeatable):

spall petstore get-pet-by-id 1 --spall-header "X-Custom: value" --spall-header "X-Another: 42"

Overriding the Server URL

Use --spall-server to target a different base URL for a single request:

spall petstore get-pet-by-id 1 --spall-server https://staging.petstore.io

The resolution order is:

  1. --spall-server CLI flag
  2. Per-API config base_url
  3. Operation-level servers from the spec
  4. Spec-level servers
  5. Fallback /

Deprecation Warnings

If an operation is marked deprecated: true in the spec, spall prints a [DEPRECATED] banner in the help text. The operation still works — this is a heads-up, not a gate.

Next Steps

Request Bodies

When an operation declares a request body, spall adds --data, --form, and --field arguments. They are mutually exclusive.

JSON Body with --data

The default for JSON APIs:

spall petstore add-pet --data '{"name":"Rex","status":"available"}'

From a file

Prefix the path with @:

spall petstore add-pet --data @new-pet.json

From stdin

Use -:

cat new-pet.json | spall petstore add-pet --data -

Optional body

If the spec says the body is not required, spall also registers --no-data so you can skip it explicitly:

spall petstore update-pet --no-data

Content-Type override

If the spec supports multiple content types, spall defaults to application/json. Override with --spall-content-type:

spall petstore upload-spec --data @spec.yaml --spall-content-type text/yaml

Multipart Upload with --form

For multipart/form-data uploads, use --form (repeatable):

spall petstore upload-file --form file=@image.png --form description="avatar"

File values are auto-detected by the @ prefix and streamed as binary parts.

URL-Encoded with --field

For application/x-www-form-urlencoded, use --field (repeatable):

spall oauth token --field grant_type=client_credentials --field client_id=abc

Validation

Before the request is sent, spall validates the body against the operation’s request schema (when application/json is declared). Validation errors are printed to stderr and spall exits with code 10.

spall petstore add-pet --data '{"status":"invalid"}'
# Validation failed:
#   /body/name: required property 'name' is missing

Next Steps

Response Output

Spall automatically chooses an output format based on whether stdout is a TTY.

Default Behavior

ContextFormat
TTY (interactive terminal)Pretty-printed JSON with syntax highlighting
Pipe / file redirectRaw JSON
# Pretty JSON (TTY)
spall petstore get-pet-by-id 1

# Raw JSON (piped)
spall petstore get-pet-by-id 1 | jq '.name'

Output Modes

Use --spall-output to override:

# Raw JSON
spall petstore get-pet-by-id 1 --spall-output raw

# YAML
spall petstore get-pet-by-id 1 --spall-output yaml

# Table (requires a JSON array of objects)
spall petstore find-pets-by-status --status available --spall-output table

# CSV (requires a JSON array of objects)
spall petstore find-pets-by-status --status available --spall-output csv

Table and CSV modes walk the array and collect all unique top-level keys as headers. If the response is not a JSON array, spall warns and falls back to pretty JSON.

Saving Responses

Save the response body to a file without touching stdout:

spall petstore get-pet-by-id 1 --spall-download pet.json

Or use the @file syntax with --spall-output:

spall petstore get-pet-by-id 1 --spall-output @pet.json

Binary responses are streamed raw to the file. When writing to stdout on a TTY, spall emits a warning and suggests --spall-download.

Filtering with JMESPath

Extract fields without installing jq:

spall petstore find-pets-by-status --status available --filter "[].name"
# ["Rex","Fluffy"]

If the filter expression is invalid, spall warns and falls back to the unfiltered response.

Verbose Mode

Print request and response headers to stderr:

spall petstore get-pet-by-id 1 --spall-verbose

With --spall-time, the duration is included:

spall petstore get-pet-by-id 1 --spall-verbose --spall-time

Sensitive headers (Authorization, Cookie, X-Api-Key, etc.) are redacted.

Next Steps

Authentication

Spall supports Bearer tokens, Basic auth, API keys, and OAuth2 pass-through. Auth resolution follows a strict priority chain so that scripts, CI, and interactive sessions can coexist.

Quick Pass-Through with --spall-auth

For one-off testing, pass a token or credentials directly:

# Bearer token
spall github get-user octocat --spall-auth "Bearer ghp_xxxxxxxx"

# Basic auth (user:pass)
spall internal get-data --spall-auth "Basic alice:secret"

# Shorthand basic (no space, one colon)
spall internal get-data --spall-auth "alice:secret"

# Bare token (treated as Bearer)
spall github get-user octocat --spall-auth "ghp_xxxxxxxx"

--spall-auth is the highest-priority auth source. It overrides everything else for that single request.

Environment Variables

If --spall-auth is omitted, spall looks for SPALL_<API>_TOKEN (hyphens become underscores):

export SPALL_GITHUB_TOKEN=ghp_xxxxxxxx
spall github get-user octocat

Per-API Config

Store auth settings in ~/.config/spall/apis/{name}.toml:

source = "https://api.example.com/openapi.json"
base_url = "https://api.example.com"

[auth]
kind = "Bearer"
token_env = "MY_API_TOKEN"

Supported kind values: Bearer, Basic, ApiKey, OAuth2.

API Key

[auth]
kind = "ApiKey"
token_env = "MY_API_KEY"
location = "header"        # or "query"
header_name = "X-Api-Key"  # ignored when location = "query"
query_name = "api_key"     # ignored when location = "header"

Basic Auth

[auth]
kind = "Basic"
username = "alice"
password_env = "ALICE_PASSWORD"

If password_env is not set and stdin is a TTY, spall prompts for the password interactively.

Inline Tokens (Discouraged)

You can embed a token directly in the config file:

[auth]
kind = "Bearer"
token = "ghp_xxxxxxxx"

Spall will accept it but prints a warning recommending token_env or a keyring instead.

Credential Hygiene

All credentials are wrapped in secrecy::SecretString:

  • Memory is zeroized on drop.
  • Debug output prints [REDACTED].
  • History redacts sensitive headers (Authorization, Cookie, X-Api-Key, etc.).
  • --spall-debug wire logs also redact secrets.

Next Steps

Pagination

Many APIs paginate large result sets via Link headers (RFC 5988). Spall can auto-follow these links and concatenate the results into a single JSON array.

Basic Usage

spall github list-repos --spall-paginate

Spall sends the initial request, inspects the Link header for rel="next", and follows it until:

  • No next link is present.
  • The maximum page limit (default 100) is reached.
  • A non-2xx response is returned (spall exits with the appropriate code).

Result Concatenation

If every page is a JSON array, all elements are flattened into one array:

// Page 1: [{"id":1},{"id":2}]
// Page 2: [{"id":3},{"id":4}]
// Output:   [{"id":1},{"id":2},{"id":3},{"id":4}]

If a page is not an array, it is wrapped as a single item:

// Page 1: [{"id":1}]
// Page 2: {"meta": {...}}
// Output:   [{"id":1},{"meta": {...}}]

Combining with Output and Filtering

Pagination works with all output modes and filtering:

spall github list-repos --spall-paginate --spall-output csv
spall github list-repos --spall-paginate --filter "[].full_name"

Limitations

  • --spall-paginate cannot be combined with --form (multipart uploads).
  • Pagination requires JSON responses. If a page returns non-JSON, spall exits with a usage error.

Next Steps

Global Flags

All internal spall flags use the --spall-* prefix so they never collide with API parameters.

Output and Formatting

FlagShortDescription
--spall-output-OOutput format: json/pretty, raw, yaml, table, csv, or @file
--spall-download-oSave response body to a file
--spall-verbose-vPrint request/response headers to stderr
--spall-debugWire-level debug logging (redacts secrets)
--spall-timeInclude request/response timing in verbose output
--filterJMESPath filter expression for JSON responses

Network Control

FlagShortDescription
--spall-server-sOverride base URL for this request
--spall-timeout-tTimeout in seconds (default: 30)
--spall-retryRetry count for failed requests (default: 1, max: 3)
--spall-follow-LFollow HTTP redirects (default: off)
--spall-max-redirectsMaximum redirects (default: 10)
--spall-insecureSkip TLS certificate verification
--spall-ca-certPath to custom CA certificate
--spall-proxyHTTP/SOCKS proxy URL

Request Modification

FlagShortDescription
--spall-header-HInject a non-sensitive header (repeatable)
--spall-auth-APass-through auth token/header
--spall-content-type-cOverride request content type

Execution Control

FlagShortDescription
--spall-dry-runPrint curl equivalent without executing
--spall-previewShow resolved URL, headers, and body without sending
--spall-paginateAuto-follow Link header pagination
--spall-repeatReplay the most recent request from history
--profileActive config profile (e.g., staging, production)

Examples

Verbose request with timing

spall petstore get-pet-by-id 1 --spall-verbose --spall-time

Staging override with custom header

spall petstore get-pet-by-id 1 \
  --spall-server https://staging.petstore.io \
  --spall-header "X-Debug: true"

Retry with redirect following

spall petstore get-pet-by-id 1 --spall-retry 3 --spall-follow

Replay last request

spall --spall-repeat

Next Steps

Config Layout

Spall stores all configuration under your platform’s config directory (typically ~/.config/spall/ on Linux, ~/Library/Application Support/spall/ on macOS, %APPDATA%\spall\ on Windows).

Directory Structure

~/.config/spall/
├── config.toml          # Global settings
├── apis/
│   ├── github.toml      # Per-API overrides
│   └── petstore.toml
├── specs/               # Optional auto-scan directory
│   ├── internal-api.yaml
│   └── partner-api.json
└── cache/
    ├── <hash>.raw       # Cached remote spec bytes
    ├── <hash>.raw-meta  # TTL + ETag metadata
    ├── <hash>.ir        # Compiled IR (postcard)
    ├── <hash>.idx       # Lightweight SpecIndex
    ├── <hash>.meta      # IR cache metadata (SHA-256 + version)
    └── history.db        # SQLite request history

Global Config (config.toml)

# Register APIs inline
[[api]]
name = "github"
spec = "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json"

[[api]]
name = "petstore"
spec = "https://petstore.swagger.io/v2/swagger.json"

# Auto-scan directories
spec_dirs = [
    "~/.config/spall/specs",
]

[defaults]
output = "json"    # json | yaml | table | raw
color = "auto"     # auto | always | never

Per-API Config (apis/{name}.toml)

Created automatically by spall api add. You can edit these to add auth or overrides:

source = "https://petstore.swagger.io/v2/swagger.json"
base_url = "https://staging.petstore.io"

[auth]
kind = "Bearer"
token_env = "PETSTORE_TOKEN"

[default_headers]
X-Client = "spall-cli"

[profiles]

[profiles.staging]
base_url = "https://staging.petstore.io"

[profiles.production]
base_url = "https://petstore.io"

Per-API fields

FieldTypeDescription
sourcestringSpec file path or URL (required)
base_urlstringOverride the spec’s server URL
authtableAuth configuration (see Authentication)
default_headerstableHeaders added to every request
profilestableNamed environment overlays

Cache Files

Spall manages the cache/ directory automatically. You should not need to edit these files directly.

  • Raw cache (*.raw, *.raw-meta) stores fetched spec bytes with TTL and ETag for conditional GET.
  • IR cache (*.ir) stores the resolved spec in postcard format for instant reload.
  • Index cache (*.idx) stores a lightweight SpecIndex for degraded --help when the spec is unreachable.
  • History (history.db) is a SQLite database of recent requests.

Next Steps

Profiles

Profiles let you switch between environments (staging, production, local) without re-registering the API or editing files before every command.

Defining Profiles

Add a [profiles.{name}] section to any per-API config file:

# ~/.config/spall/apis/petstore.toml
source = "https://petstore.swagger.io/v2/swagger.json"
base_url = "https://petstore.io"

[auth]
kind = "Bearer"
token_env = "PETSTORE_TOKEN"

[profiles.staging]
base_url = "https://staging.petstore.io"
auth = { kind = "Bearer", token_env = "PETSTORE_STAGING_TOKEN" }

[profiles.production]
base_url = "https://petstore.io"

Using Profiles

Activate a profile with --profile:

# Hit staging
spall petstore get-pet-by-id 1 --profile staging

# Hit production
spall petstore get-pet-by-id 1 --profile production

Profile Overlay Rules

When a profile is active, its values override the base config:

  • base_url replaces the base config value entirely.
  • auth replaces the base config auth entirely.
  • headers are merged: profile headers with the same key override base headers; new keys are appended.

If the requested profile does not exist, spall exits with a usage error.

Next Steps

CLI Reference

Top-Level Commands

spall [OPTIONS] <COMMAND>
CommandDescription
spall api ...Manage registered APIs
spall auth ...Authentication commands
spall history ...Request/response history
spall completions ...Generate shell completion scripts
spall <api> <operation> [args]Execute an API operation

spall api

SubcommandDescription
spall api add <name> <source>Register a new API
spall api listList registered APIs
spall api remove <name>Unregister an API
spall api refresh [<name>]Refresh cached remote spec
spall api refresh --allRefresh all cached remote specs
spall api discover <url>Discover and register an API via RFC 8631

spall auth

SubcommandDescription
spall auth status <api>Show auth status for an API
spall auth login <api>Initiate OAuth2 PKCE login (stub)

spall history

SubcommandDescription
spall history listList recent requests
spall history show <id>Show full request details
spall history clearErase all history

Global Flags

See Global Flags for the full list of --spall-* options.

Next Steps

Request History

Spall records every request to a local SQLite database (cache/history.db). This is useful for debugging, auditing, and replay.

Listing History

spall history list

Output:

  42  2025-04-25 14:32  GET     200   124ms  petstore  get-pet-by-id
  41  2025-04-25 14:30  POST    201   312ms  petstore  add-pet
  40  2025-04-25 14:28  GET     404    89ms  github    get-repo

Viewing a Single Request

spall history show 42

This prints the method, URL, status code, duration, request headers, and response headers. Sensitive headers are redacted.

Replaying a Request

# Replay the most recent request
spall --spall-repeat

# Replay a specific request by ID
spall history show 42 --spall-repeat

Replay reconstructs the exact method, URL, headers, and body from the history record, then re-executes it.

Clearing History

spall history clear

This deletes all rows and vacuums the database.

Privacy Notes

  • Request and response headers are recorded, but sensitive headers (Authorization, Cookie, X-Api-Key, etc.) are stored as [REDACTED].
  • Request bodies are not stored in history.
  • The history database is local to your machine and never transmitted.

Next Steps

Shell Completions

Spall can generate completion scripts for bash, zsh, and fish. Because operations are loaded dynamically from specs, the completion scripts query spall itself at runtime to suggest API names and operation IDs.

Bash

spall completions bash > /etc/bash_completion.d/spall
# or user-local:
spall completions bash > ~/.local/share/bash-completion/completions/spall

Zsh

spall completions zsh > "${fpath[1]}/_spall"

Fish

spall completions fish > ~/.config/fish/completions/spall.fish

How It Works

The generated scripts are thin wrappers around spall’s __complete hidden subcommand:

spall __complete <api> <partial-word>

This loads the spec (or its cached index if offline) and prints matching operation IDs and parameter names. Completion is fast even for large APIs because it uses the lightweight SpecIndex cache.

Next Steps

Exit Codes

Spall returns structured exit codes so shell scripts and CI pipelines can branch on outcome without parsing stderr.

CodeMeaningWhen It Happens
0Success2xx HTTP response
1Usage errorMissing required argument, unknown API/operation, bad flag, config parse failure
2Network errorDNS failure, TCP timeout, TLS error, proxy failure, stale cache with no fallback
3Spec errorYAML/JSON parse failure, dangling $ref, invalid OpenAPI structure, cache corruption that cannot be rebuilt
4HTTP 4xxClient error responses (400, 401, 403, 404, etc.)
5HTTP 5xxServer error responses (500, 502, 503, etc.)
10Validation failedPreflight parameter or body schema validation failed

Scripting Examples

Retry on network failure

spall petstore get-pet-by-id 1 || [ $? -eq 2 ] && sleep 5 && spall petstore get-pet-by-id 1

Skip downstream steps on 4xx

spall github get-repo rustpunk/spall || {
  code=$?
  if [ "$code" -eq 4 ]; then
    echo "Repo not found, skipping build..."
    exit 0
  fi
  exit "$code"
}

Fail CI on validation errors

spall internal create-order --data @order.json || {
  code=$?
  if [ "$code" -eq 10 ]; then
    echo "Validation failed — check your payload."
  fi
  exit "$code"
}

Next Steps