The project is in a healthy, maintained state
A clean, idiomatic Ruby interface to the Fizzy API with minimal dependencies
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

 Project Readme

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 install

Or install it yourself as:

$ gem install fizzy-api-client

Quick 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-account

Initializer

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?
end

Controller 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
end

Background 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
end

Per-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
end

Configuration

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
end

Client 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.identity

Boards

# 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_read

Direct 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 file

Pagination

# 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}"
end

Demo 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.

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Run tests (bundle exec rake test)
  4. Commit your changes (git commit -am 'Add some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create a Pull Request

License

The gem is available as open source under the terms of the MIT License.