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 commandssmily rentals list, smily bookings get 42, smily clients create … — for every documented endpoint.
  • A raw escape hatchsmily 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:

  1. --token flag
  2. SMILY_TOKEN (or BOOKINGSYNC_TOKEN) environment variable
  3. 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 under linked).
  • --limit N — stop after N records (follows pages as needed; also sizes the page request to min(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 — retry 429 Too Many Requests up 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 0600 in a 0700 directory; secrets are masked in smily config list / smily auth status.
  • --verbose scrubs Authorization / 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.