Fizzy API Client
A Ruby gem providing a clean, idiomatic interface to the Fizzy API. Fizzy is a task management app from 37signals.
Installation
Add this line to your application's Gemfile:
gem 'fizzy-api-client'And then execute:
$ bundle installOr install it yourself as:
$ gem install fizzy-api-clientQuick Start
require 'fizzy_api_client'
# Configure globally
FizzyApiClient.configure do |config|
config.api_token = 'your-api-token'
config.account_slug = 'your-account'
end
# Or use environment variables:
# FIZZY_API_TOKEN, FIZZY_ACCOUNT, FIZZY_BASE_URL
client = FizzyApiClient::Client.new
# Get identity
identity = client.identity
# List boards
boards = client.boards
# Create a card (note: uses 'title' field, not 'name')
card = client.create_card(board_id: 'board_1', title: 'New Task')
# Add a step (note: uses 'content' field)
step = client.create_step(card['number'], content: 'First step')Rails Integration
Configuration with Credentials
Store your API credentials securely using Rails credentials:
$ rails credentials:edit# config/credentials.yml.enc
fizzy:
api_token: your-api-token
account_slug: your-accountInitializer
Create an initializer to configure the client:
# config/initializers/fizzy.rb
FizzyApiClient.configure do |config|
config.api_token = Rails.application.credentials.dig(:fizzy, :api_token)
config.account_slug = Rails.application.credentials.dig(:fizzy, :account_slug)
config.logger = Rails.logger if Rails.env.development?
endController Example
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
def index
@boards = fizzy_client.boards
end
def create
card = fizzy_client.create_card(
board_id: params[:board_id],
title: params[:title],
description: params[:description]
)
redirect_to tasks_path, notice: "Task ##{card['number']} created"
rescue FizzyApiClient::ApiError => e
redirect_to tasks_path, alert: "Failed to create task: #{e.message}"
end
def complete
fizzy_client.close_card(params[:card_number])
redirect_to tasks_path, notice: "Task completed"
end
private
def fizzy_client
@fizzy_client ||= FizzyApiClient::Client.new
end
endBackground Job Example
# app/jobs/sync_fizzy_cards_job.rb
class SyncFizzyCardsJob < ApplicationJob
queue_as :default
def perform(board_id)
client = FizzyApiClient::Client.new
cards = client.cards(board_ids: [board_id], auto_paginate: true)
cards.each do |card_data|
Task.find_or_initialize_by(fizzy_number: card_data['number']).update!(
title: card_data['title'],
status: card_data['closed_at'].present? ? 'completed' : 'open',
synced_at: Time.current
)
end
end
endPer-Environment Configuration
# config/initializers/fizzy.rb
FizzyApiClient.configure do |config|
config.api_token = Rails.application.credentials.dig(:fizzy, :api_token)
config.account_slug = Rails.application.credentials.dig(:fizzy, :account_slug)
# Use different base URL for staging/development
if Rails.env.staging?
config.base_url = 'https://staging.fizzy.do'
end
# Enable logging in development
config.logger = Rails.logger if Rails.env.development?
# Shorter timeouts in test environment
if Rails.env.test?
config.timeout = 5
config.open_timeout = 2
end
endConfiguration
Global Configuration
FizzyApiClient.configure do |config|
config.api_token = 'your-api-token'
config.account_slug = 'your-account'
config.base_url = 'https://app.fizzy.do' # default
config.timeout = 30 # seconds
config.open_timeout = 10 # seconds
config.logger = Logger.new($stdout) # optional
endClient Configuration
client = FizzyApiClient::Client.new(
api_token: 'your-api-token',
account_slug: 'your-account'
)Environment Variables
-
FIZZY_API_TOKEN- Personal Access Token -
FIZZY_ACCOUNT- Default account slug -
FIZZY_BASE_URL- API base URL (for self-hosted instances)
Account Slug Normalization
The identity API returns account slugs with a leading slash (e.g., /897362094). This gem automatically normalizes slugs by stripping the leading slash.
Resources
Identity
client.identityBoards
# List and retrieve
client.boards
client.boards(page: 2)
client.boards(auto_paginate: true)
client.board('board_id')
# Create
client.create_board(name: 'New Board')
client.create_board(
name: 'Team Board',
all_access: false,
auto_postpone_period: 7,
public_description: '<p>Description</p>'
)
# Update
client.update_board('board_id', name: 'Updated Name')
client.update_board('board_id', user_ids: ['user_1', 'user_2']) # when all_access: false
# Delete
client.delete_board('board_id')Cards
Cards use title field (not name).
# List with filters
client.cards
client.cards(board_ids: ['board_1', 'board_2'])
client.cards(tag_ids: ['tag_1'], assignee_ids: ['user_1'])
client.cards(terms: ['bug', 'fix'])
client.cards(auto_paginate: true)
# Retrieve
client.card(42)
# Create
client.create_card(board_id: 'board_1', title: 'New Card')
client.create_card(
board_id: 'board_1',
title: 'Full Card',
description: 'Details here',
status: 'published',
tag_ids: ['tag_1', 'tag_2']
)
# Create with image
client.create_card(
board_id: 'board_1',
title: 'Card with Image',
image: '/path/to/image.png' # or File object
)
# Update
client.update_card(42, title: 'Updated Title')
client.update_card(42, image: '/path/to/new_image.png')
# Delete
client.delete_card(42)
# State changes
client.close_card(42)
client.reopen_card(42)
client.postpone_card(42) # or client.not_now_card(42)
# Triage (move to column)
client.triage_card(42, column_id: 'col_1')
client.untriage_card(42)
# Assignments and tags
client.toggle_assignment(42, assignee_id: 'user_1')
client.toggle_tag(42, tag_title: 'urgent')
# Watch/unwatch
client.watch_card(42)
client.unwatch_card(42)
# Golden ticket (highlight/pin a card)
client.gild_card(42)
client.ungild_card(42)Columns
# List and retrieve
client.columns('board_id')
client.column('board_id', 'column_id')
# Create with named color (recommended)
client.create_column(board_id: 'board_1', name: 'In Progress')
client.create_column(board_id: 'board_1', name: 'Review', color: :lime)
client.create_column(board_id: 'board_1', name: 'Urgent', color: :pink)
# Update with named color
client.update_column('board_id', 'column_id', name: 'Done')
client.update_column('board_id', 'column_id', color: :purple)
# Available colors: :blue (default), :gray, :tan, :yellow, :lime, :aqua, :violet, :purple, :pink
# CSS variable tokens are also supported: 'var(--color-card-3)'
# Delete
client.delete_column('board_id', 'column_id')Comments
# List and retrieve
client.comments(42)
client.comment(42, 'comment_id')
# Create
client.create_comment(42, body: 'This is a comment')
client.create_comment(42, body: 'Backdated comment', created_at: '2025-01-01T00:00:00Z')
# Update
client.update_comment(42, 'comment_id', body: 'Updated comment')
# Delete
client.delete_comment(42, 'comment_id')Steps
Steps use content field (not name).
# Retrieve
client.step(42, 'step_id')
# Create
client.create_step(42, content: 'Do this first')
client.create_step(42, content: 'Already done', completed: true)
# Update
client.update_step(42, 'step_id', content: 'Updated step')
client.update_step(42, 'step_id', completed: true)
# Convenience methods
client.complete_step(42, 'step_id')
client.incomplete_step(42, 'step_id')
# Delete
client.delete_step(42, 'step_id')Reactions
Reactions use content field (not emoji).
# List reactions on a comment
client.reactions(42, 'comment_id')
# Add reaction
client.add_reaction(42, 'comment_id', content: ':+1:')
# Remove reaction
client.remove_reaction(42, 'comment_id', 'reaction_id')Tags
Tags return title field (not name).
client.tags
client.tags(page: 2)
client.tags(auto_paginate: true)Users
# List and retrieve
client.users
client.users(auto_paginate: true)
client.user('user_id')
# Update
client.update_user('user_id', name: 'New Name')
# Update with avatar
client.update_user('user_id', avatar: '/path/to/avatar.png')
client.update_user('user_id', name: 'New Name', avatar: File.open('avatar.png'))
# Deactivate
client.deactivate_user('user_id')Notifications
# List
client.notifications
client.notifications(auto_paginate: true)
# Mark as read/unread
client.mark_notification_read('notification_id')
client.mark_notification_unread('notification_id')
# Mark all as read
client.mark_all_notifications_readDirect Uploads (File Attachments)
For uploading files via ActiveStorage:
# Simple file upload (handles all steps automatically)
signed_id = client.upload_file('/path/to/document.pdf')
signed_id = client.upload_file(file_io, filename: 'report.pdf', content_type: 'application/pdf')
# Manual direct upload (for advanced use cases)
upload = client.create_direct_upload(
filename: 'document.pdf',
byte_size: 1024,
checksum: Base64.strict_encode64(Digest::MD5.digest(content)),
content_type: 'application/pdf'
)
# Then PUT to upload['direct_upload']['url'] with the file content
# Use upload['signed_id'] to reference the uploaded filePagination
# First page only (default)
boards = client.boards
# Specific page
boards = client.boards(page: 2)
# All pages (automatically follows pagination)
boards = client.boards(auto_paginate: true)Error Handling
begin
client.board('invalid')
rescue FizzyApiClient::NotFoundError => e
puts "Not found: #{e.message}"
rescue FizzyApiClient::AuthenticationError => e
puts "Auth failed: #{e.message}"
rescue FizzyApiClient::ForbiddenError => e
puts "Access denied: #{e.message}"
rescue FizzyApiClient::ValidationError => e
puts "Invalid data: #{e.message}"
rescue FizzyApiClient::ServerError => e
puts "Server error: #{e.message}"
rescue FizzyApiClient::ApiError => e
puts "API error #{e.status}: #{e.message}"
rescue FizzyApiClient::ConnectionError => e
puts "Connection failed: #{e.message}"
rescue FizzyApiClient::TimeoutError => e
puts "Request timed out: #{e.message}"
endDemo Script
A comprehensive demo script is included that tests every feature of the gem. See examples/README.md for usage instructions.
Releasing
bin/release patch # 0.1.0 → 0.1.1 (bug fixes)
bin/release minor # 0.1.0 → 0.2.0 (new features)
bin/release major # 0.1.0 → 1.0.0 (breaking changes)The script bumps the version, updates CHANGELOG.md, runs tests, commits, tags, and pushes. GitHub Actions handles publishing to RubyGems.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/robzolkos/fizzy-api-client.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Run tests (
bundle exec rake test) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a Pull Request
License
The gem is available as open source under the terms of the MIT License.