A Ruby gem providing a clean, idiomatic interface to the Fizzy API. Fizzy is a task management app from 37signals.
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-clientrequire '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')Store your API credentials securely using Rails credentials:
$ rails credentials:edit# config/credentials.yml.enc
fizzy:
api_token: your-api-token
account_slug: your-accountCreate 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?
end# 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
end# 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
end# 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
endFizzyApiClient.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 = FizzyApiClient::Client.new(
api_token: 'your-api-token',
account_slug: 'your-account'
)FIZZY_API_TOKEN- Personal Access TokenFIZZY_ACCOUNT- Default account slugFIZZY_BASE_URL- API base URL (for self-hosted instances)
The identity API returns account slugs with a leading slash (e.g., /897362094). This gem automatically normalizes slugs by stripping the leading slash.
client.identity# 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 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)# 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')# 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 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 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 return title field (not name).
client.tags
client.tags(page: 2)
client.tags(auto_paginate: true)# 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')# 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_readFor 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 file# 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)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}"
endA comprehensive demo script is included that tests every feature of the gem. See examples/README.md for usage instructions.
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.
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
The gem is available as open source under the terms of the MIT License.