Project

bunny_app

0.0
The project is in a healthy, maintained state
Use Bunny for SaaS subscription management, cpq, billing and CRM
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 1.3
~> 13.0
~> 3.6
~> 1.26

Runtime

~> 0.20
~> 2.6
 Project Readme

bunny-ruby

Ruby SDK for Bunny — the subscription billing and management platform.

Installation

Add to your Gemfile:

gem 'bunny_app'

Then run:

bundle install

Or install directly:

gem install bunny_app

Configuration

With client credentials (recommended)

Using client_id and client_secret enables automatic token refresh when the access token expires.

require 'bunny_app'

BunnyApp.config do |c|
  c.client_id     = 'your-client-id'
  c.client_secret = 'your-client-secret'
  c.scope         = 'standard:read standard:write'
  c.base_uri      = 'https://<subdomain>.bunny.com'
end

With a pre-existing access token

If you manage token lifecycle yourself, you can pass the token directly. An AuthorizationError will be raised if the token expires.

BunnyApp.config do |c|
  c.access_token = 'your-access-token'
  c.base_uri     = 'https://<subdomain>.bunny.com'
end

Never commit secrets to source control. Load credentials from environment variables or a secret store.

Rails initializer

Generate a config file at config/initializers/bunny_app.rb:

bin/rails g bunny_app:install

This creates a template that reads credentials from environment variables:

BunnyApp.config do |c|
  c.client_id     = ENV['BUNNY_APP_CLIENT_ID']
  c.client_secret = ENV['BUNNY_APP_CLIENT_SECRET']
  c.scope         = ENV['BUNNY_APP_SCOPE']
  c.base_uri      = 'https://<subdomain>.bunny.com'
end

Subscriptions

Create a subscription

Create a new subscription for an account. You can create the account inline or attach to an existing account by ID.

# Create with a new account
subscription = BunnyApp::Subscription.create(
  price_list_code: 'starter',
  options: {
    account_name: 'Acme Corp',
    first_name:   'Jane',
    last_name:    'Smith',
    email:        'jane@acme.com',
    trial:        true,
    tenant_code:  'acme-123',
    tenant_name:  'Acme Corp'
  }
)

# Create against an existing account
subscription = BunnyApp::Subscription.create(
  price_list_code: 'starter',
  options: {
    account_id:  '456',
    tenant_code: 'acme-123',
    tenant_name: 'Acme Corp'
  }
)

Returns a hash containing the created subscription, including id, state, trialStartDate, trialEndDate, plan, priceList, and tenant.

Update subscription quantities

Adjust the quantities for one or more charges on an existing subscription.

quote = BunnyApp::Subscription.quantity_update(
  subscription_id: '456123',
  quantities: [
    { code: 'users', quantity: 25 }
  ],
  options: {
    invoice_immediately:          true,
    start_date:                   '2024-06-01',
    name:                         'Add users — June',
    allow_quantity_limits_override: false
  }
)

Returns a quote hash with id and name.

Convert a trial to paid

# Convert using a price list code
result = BunnyApp::Subscription.trial_convert(
  subscription_id: '456123',
  price_list_code: 'starter'
)

# Convert using a price list ID and payment method
result = BunnyApp::Subscription.trial_convert(
  subscription_id: '456123',
  price_list_id:   '789',
  payment_id:      '101112'
)

Returns a hash containing invoice (with amount, id, number, subtotal) and subscription (with id, name).

Cancel a subscription

BunnyApp::Subscription.cancel(subscription_id: '456123')
# => true

Tenants

Create a tenant

tenant = BunnyApp::Tenant.create(
  name:          'Acme Corp',
  code:          'acme-123',
  account_id:    '456',
  platform_code: 'main'   # optional, defaults to 'main'
)

Returns a hash with id, code, name, and platform.

Find a tenant by code

tenant = BunnyApp::Tenant.find_by(code: 'acme-123')
# => { "id" => "1", "code" => "acme-123", "name" => "Acme Corp",
#      "subdomain" => "acme", "account" => { "id" => "456", ... } }

Returns nil if no tenant is found.


Tenant Metrics

Push usage and engagement metrics to Bunny for a tenant. Useful for health scoring and churn signals.

BunnyApp::TenantMetrics.update(
  code:       'acme-123',
  last_login: '2024-06-01T12:00:00Z',
  user_count: 42,
  utilization_metrics: {
    projects_created: 10,
    exports_run:      3
  }
)
# => true

utilization_metrics is optional and accepts any key/value pairs you want to track.


Feature Usage

Track usage of individual features for usage-based billing or analytics.

# Track usage now
usage = BunnyApp::FeatureUsage.create(
  quantity:        5,
  feature_code:    'api_calls',
  subscription_id: '456123'
)

# Track usage for a specific date
usage = BunnyApp::FeatureUsage.create(
  quantity:        5,
  feature_code:    'api_calls',
  subscription_id: '456123',
  usage_at:        '2024-03-10'
)

Returns a hash with id, quantity, usageAt, subscription, and feature.


Portal Sessions

Generate a short-lived token to embed the Bunny billing portal in your app using Bunny.js.

# Basic session
token = BunnyApp::PortalSession.create(tenant_code: 'acme-123')

# With a return URL and custom expiry (in hours, default: 24)
token = BunnyApp::PortalSession.create(
  tenant_code:  'acme-123',
  return_url:   'https://yourapp.com/billing',
  expiry_hours: 4
)

Returns the session token string.


Platforms

Platforms allow you to group tenants. Create a platform before assigning tenants to it.

platform = BunnyApp::Platform.create(
  name: 'My SaaS Platform',
  code: 'my-saas'
)
# => { "id" => "1", "name" => "My SaaS Platform", "code" => "my-saas" }

Webhooks

Bunny sends webhooks for events like subscription state changes. Verify the x-bunny-signature header to confirm the payload is authentic.

payload    = request.raw_post
signature  = request.headers['x-bunny-signature']
signing_key = ENV['BUNNY_WEBHOOK_SECRET']

if BunnyApp::Webhook.verify(signature, payload, signing_key)
  # payload is authentic — process the event
  event = JSON.parse(payload)
  case event['type']
  when 'SubscriptionProvisioningChange'
    # handle provisioning change
  end
else
  head :unauthorized
end

Custom GraphQL Queries

You can send any GraphQL query or mutation directly if you need fields not covered by the convenience methods.

Synchronous query

query = <<~GRAPHQL
  query GetTenant($code: String!) {
    tenant(code: $code) {
      id
      name
      account {
        id
        name
      }
    }
  }
GRAPHQL

response = BunnyApp.query(query, { code: 'acme-123' })
tenant = response['data']['tenant']

Asynchronous query (fire-and-forget)

Runs the request in a background thread. Useful for non-critical tracking calls where you don't want to block the request cycle.

BunnyApp.query_async(query, variables)

Error Handling

All convenience methods raise on error. Two exception classes are provided:

Exception When raised
BunnyApp::AuthorizationError Invalid or expired credentials
BunnyApp::ResponseError API returned errors in the response body
begin
  BunnyApp::Subscription.cancel(subscription_id: '456123')
rescue BunnyApp::AuthorizationError => e
  # Re-authenticate or alert
  Rails.logger.error "Bunny auth failed: #{e.message}"
rescue BunnyApp::ResponseError => e
  # The API rejected the request
  Rails.logger.error "Bunny error: #{e.message}"
end

Requirements

Ruby 2.5+


Development

bundle install          # install dependencies
bundle exec rake spec   # run tests
bin/console             # interactive console

Set IGNORE_SSL=true when running locally to suppress SSL warnings:

IGNORE_SSL=true bin/console

Publishing

Update lib/bunny_app/version.rb, then:

gem build
gem push bunny_app-x.x.x.gem

The RubyGems account is protected by MFA and managed by @richet.