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

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.