smily — BookingSync (Smily) API v3 CLI
smily is an ergonomic, comprehensive command-line client for the
BookingSync (Smily) API v3.
It aims to be the tool you reach for when you want to talk to API v3 from a terminal or a script:
- Per-resource commands —
smily rentals list,smily bookings get 42,smily clients create …— for every documented endpoint. - A raw escape hatch —
smily api get <path>reaches anything the API exposes, with pagination and wrapping out of your way. - OAuth built in — client-credentials, refresh, and authorization-code helpers; no need to hand-roll token requests.
- Config profiles — keep tokens for multiple accounts/environments and
switch with
--profile. - Output that fits your workflow — a readable table on a TTY, clean JSON when piped, plus YAML, CSV and NDJSON.
- Pagination, filtering, sparse fieldsets, sideloading — first-class flags.
It is built on top of the official
bookingsync-api gem, so the
HTTP, JSON:API and error-handling behavior matches the reference client.
Installation
gem install smily_cli
Or with Bundler:
gem "smily_cli"
This installs the smily executable.
Setup (first run)
Once the gem is installed, get from zero to your first authenticated call in four steps:
# 1. Confirm the executable is on your PATH
smily version
# 2. Get an API token. For scripts/automation the Client Credentials flow is
# simplest: you need your application's Client ID and Secret from the
# BookingSync developer portal (https://www.bookingsync.com/oauth/applications).
# --save writes the token to the default profile at
# ~/.config/smily/config.yml (created 0600, since it holds secrets).
smily auth client-credentials --client-id "$CLIENT_ID" --client-secret "$CLIENT_SECRET" --save
# 3. Verify the token actually works (calls /me)
smily auth status --check
# 4. Make your first real call
smily rentals list --limit 5
A Client-Credentials token can read across every account that authorized your
app; pin one account with --account-id <ID> per call, or store it once:
smily config set account_id <ID>
Don't want to store anything? Every command also reads --token / SMILY_TOKEN
directly, so SMILY_TOKEN=… smily rentals list works with no config file at
all. See Authentication for the Authorization-Code (per-user)
flow and Configuration & profiles for multiple
environments.
Quick start
# 1. Get a token (Client Credentials flow) and save it to the default profile
smily auth client-credentials --client-id "$CLIENT_ID" --client-secret "$CLIENT_SECRET" --save
# 2. Explore
smily resources # what can I talk to?
smily rentals list --limit 5 # a readable table
smily bookings list --filter status=booked from=2026-01-01
# 3. Fetch one, as JSON
smily rentals get 42 -o json
# 4. Anything the resource commands don't cover
smily api get rentals/42/bookings --paginate -o ndjson
Authentication
API v3 uses OAuth 2.0 Bearer tokens. A token can be supplied (in order of precedence) by:
--tokenflagSMILY_TOKEN(orBOOKINGSYNC_TOKEN) environment variable- the active config profile
Getting a token
# Client Credentials — app-level, public scope, reads across all authorized
# accounts. Great for scripts. (Tokens last ~2h.)
smily auth client-credentials --client-id ID --client-secret SECRET --save
# Authorization Code — per-account access.
smily auth authorize-url --client-id ID --redirect-uri https://app.example.com/cb --scope "rentals_read bookings_read"
# (open the URL, approve, copy the ?code=… back)
smily auth exchange --client-id ID --client-secret SECRET --code CODE --redirect-uri https://app.example.com/cb --save
# Refresh an expiring token
smily auth refresh --client-id ID --client-secret SECRET --refresh-token RT --save
# Check what's configured (optionally validate against /me)
smily auth status --check
--save stores the token (and any refresh token / client credentials) in the
active profile so subsequent commands just work.
Scoping a Client-Credentials token to one account
A Client-Credentials token spans every account that authorized your app. Pin a
single account with --account-id (sent as the account_id query parameter),
via SMILY_ACCOUNT_ID, or by storing it in a profile:
smily rentals list --account-id 12345
smily config set account_id 12345 --profile acme
For Authorization-Code tokens the account is implicit, so this is a no-op.
Configuration & profiles
Configuration lives in ~/.config/smily/config.yml (override with
SMILY_CONFIG; honors XDG_CONFIG_HOME). The file is written with 0600
permissions because it holds secrets.
smily config init # interactive: store a token + base URL
smily config set token TOKEN --profile prod
smily config set account_id 123 --profile prod
smily config use prod # make "prod" the default profile
smily config list # profiles + settings (secrets masked)
smily config path # where's the file?
Use a specific profile per command with --profile NAME, or set
SMILY_PROFILE.
Working with resources
Every registered resource supports the same verbs:
smily <resource> list [options]
smily <resource> get ID [options]
smily <resource> create --data <json> # not on read-only resources
smily <resource> update ID --data <json>
smily <resource> delete ID [--yes]
Run smily resources for the full catalog, or smily <resource> help.
Listing, filtering, pagination
smily bookings list --filter status=booked --filter from=2026-01-01
smily rentals list --fields id name --limit 10
smily rentals list --all -o ndjson # every page, one JSON object per line
smily rentals list --page 2 --per-page 50 # a specific page
smily rentals list --include photos # sideload associations
--filter k=v/--query k=v— API query parameters (repeat or space-separate). These are sent verbatim, so a resource's documented filters work directly (--filter status=booked from=2026-01-01), and operator query params are forwarded as-is where the endpoint supports them (--filter 'final_price[gteq]=600'). For the validated operator/range DSL, see MCP mode.--fields a b c— sparse fieldset (also limits table/CSV columns).--include a b— sideload associations (returned underlinked).--limit N— stop after N records (follows pages as needed; also sizes the page request tomin(N, 100)).--all— fetch every page.--page/--per-page— fetch one specific page.--max-pages N— hard backstop on auto-pagination (default 1000); the CLI warns if it stops early.--retry N— retry429 Too Many Requestsup to N times, waiting for the rate-limit window (Retry-After/X-RateLimit-Reset). Off by default.
Creating & updating
Provide attributes as a JSON object; the { "<resource>": [ … ] } envelope
that v3 expects is added for you:
smily rentals create --data '{"name":"Villa Sunset","sleeps":6}'
smily clients create --data @client.json # from a file
echo '{"sleeps":8}' | smily rentals update 42 --data - # from stdin
For nested creates (e.g. a booking under a rental), point --path at the
parent collection (the envelope key stays the resource's):
smily bookings create --path rentals/42/bookings --data @booking.json
The raw api command
smily api <method> <path> is the universal escape hatch — it can reach any
endpoint, with any verb, and sends your body verbatim:
smily api get me
smily api get rentals --query per_page=5
smily api get bookings/123
smily api post rentals --data '{"rentals":[{"name":"X"}]}'
smily api get rentals --paginate -o json # combine all pages into one array
By default it prints the raw JSON response envelope. With --paginate it
follows pagination and prints the combined records.
MCP mode (smily mcp)
Besides the REST API, smily can talk to a BookingSync MCP server (Model
Context Protocol) — the same servers an AI agent connects to. This is a separate
mode with its own credential (mcp_token) and endpoint (mcp_url, defaulting to
<base-url>/mcp).
# One-time setup: store the MCP token (and optionally a non-default endpoint)
smily mcp login "$MCP_TOKEN"
smily mcp login "$MCP_TOKEN" --url https://www.bookingsync.com/mcp --profile prod
# Handshake + capabilities, and discover the tools the server exposes
smily mcp info
smily mcp tools
# Convenience wrappers that mirror the REST commands, over the generic
# list / get / resources / resource_schema tools:
smily mcp resources
smily mcp list rentals --limit 5
smily mcp list bookings --filter status=booked --account-id 42
smily mcp get rentals 42
smily mcp schema bookings
# Low-level escape hatch — call any tool with a raw JSON argument object:
smily mcp call list --args '{"resource":"rentals","limit":5}'
smily mcp call resource_schema --args @args.json
The convenience wrappers resolve the generic tool names for you — they work
against a server that exposes list/get/… as well as one that prefixes them
(api_v3_list, api_v3_get, …).
Advanced filtering (operators & ranges)
smily mcp list speaks the API v3 filter DSL. A plain attribute=value is an
exact match; attribute[op]=value applies an operator; repeating an attribute
combines the conditions with AND (e.g. a range). Operators: eq, not_eq,
gt, gteq, lt, lteq, in, matches, does_not_match (exactly the set a
resource reports via smily mcp schema <resource>).
# final_price > 1000
smily mcp list bookings --filter 'final_price[gt]=1000'
# 600 <= final_price < 800 (compound AND range)
smily mcp list bookings --filter 'final_price[gteq]=600' --filter 'final_price[lt]=800'
# starts on/after a date, and one of several references
smily mcp list bookings --filter 'start_at[gteq]=2026-01-01T00:00:00Z' --filter 'reference[in]=A123,B456'
Numeric and boolean values are coerced automatically; ISO 8601 timestamps are
kept as strings. Use smily mcp schema <resource> to see which attributes are
filterable and the operators each accepts.
Credentials/endpoint resolve like everything else — --mcp-token /
SMILY_MCP_TOKEN / profile mcp_token, and --mcp-url / SMILY_MCP_URL /
profile mcp_url. --account-id scopes tool calls to a single account (as the
account_id tool argument), which is how a multi-account MCP token is pinned.
Output honors -o just like the REST commands.
Transport details: smily speaks the Streamable-HTTP JSON-RPC 2.0 transport,
performing the initialize handshake (capturing the Mcp-Session-Id) and
accepting either application/json or SSE-framed responses.
Output formats
-o, --output selects the format. The default is a human-readable table on
an interactive terminal, and json when the output is piped — so scripts get
machine-readable data automatically.
| Format | Notes |
|---|---|
table |
Aligned grid (collections) or key/value (single). |
json |
Pretty JSON — array for lists, object for get. |
yaml |
YAML. |
csv |
Header + rows; nested values become JSON. |
ndjson |
One compact JSON object per line (a.k.a. jsonl). |
smily rentals list -o csv --fields id name status > rentals.csv
smily bookings list --all -o ndjson | jq 'select(.status=="booked")'
Global options
Single-value global flags work before or after the subcommand
(smily -o json rentals list and smily rentals list -o json are equivalent).
Array-valued flags (--fields, --filter) must follow the subcommand.
| Flag | Env | Meaning |
|---|---|---|
--profile NAME |
SMILY_PROFILE |
Config profile to use |
--token TOKEN |
SMILY_TOKEN |
OAuth access token (REST) |
--mcp-token TOKEN |
SMILY_MCP_TOKEN |
MCP access token (smily mcp) |
--mcp-url URL |
SMILY_MCP_URL |
MCP endpoint (default <base-url>/mcp) |
--base-url URL |
SMILY_BASE_URL |
API base (default https://www.bookingsync.com) |
--account-id ID |
SMILY_ACCOUNT_ID |
Scope to an account (client-credentials tokens) |
--max-pages N |
SMILY_MAX_PAGES |
Auto-pagination backstop (default 1000) |
--retry N |
SMILY_RETRY |
Retry 429s N times, honoring the rate-limit reset |
-o, --output |
SMILY_OUTPUT |
Output format |
--fields a b c |
Field/column selection | |
--no-color |
NO_COLOR |
Disable ANSI color |
-q, --quiet |
Suppress status messages | |
-v, --verbose |
SMILY_VERBOSE |
Log HTTP traffic to stderr (secrets redacted) |
Security notes
- Config files are written
0600in a0700directory; secrets are masked insmily config list/smily auth status. --verbosescrubsAuthorization/client_secret/ tokens from the logged traffic, so verbose output is safe to share.- The CLI warns (like the GitHub CLI) if a credential would be sent over a non-HTTPS endpoint.
Shell completion
smily completion bash > /usr/local/etc/bash_completion.d/smily
smily completion zsh > "${fpath[1]}/_smily"
smily completion fish > ~/.config/fish/completions/smily.fish
Exit codes
0 success · 1 generic/API error · 2 usage error · 3 auth error
(401/403) · 4 not found (404) · 5 rate limited (429).
Using it as a library
The library layer is usable without Thor:
require "smily_cli"
client = SmilyCli::Client.new(token: ENV["SMILY_TOKEN"])
result = client.list("rentals", limit: 10)
result.records # => [ { "id" => 1, ... }, ... ]
puts SmilyCli::Formatters.render(result, format: "json")
Development
bin/setup # install dependencies
bundle exec rake # run the specs and RuboCop
bundle exec rspec # just the specs
bin/console # an IRB session with the gem loaded
CI (GitHub Actions) runs the specs across Ruby 3.2–3.4, RuboCop, and Brakeman.
License
Available as open source under the terms of the MIT License.