Skip to content

amro/gibbon

Repository files navigation

gibbon

Gibbon is an API wrapper for MailChimp's API.

Build Status

Important Notes

Please read MailChimp's Getting Started Guide.

Gibbon returns a Gibbon::Response, which exposes the parsed response body and the response headers. See Responses below.

See the CHANGELOG for release history and Upgrading for breaking changes.

Installation

$ gem install gibbon

Or add it to your Gemfile:

gem "gibbon"

Requirements

  • Ruby 3.1 or newer. Gibbon 4.0 dropped support for Ruby 2.4 through 3.0.
  • A MailChimp account and API key. You can see your API keys here.

Gibbon depends on Faraday (both 1.x and 2.x are supported) and MultiJson. Connections to MailChimp negotiate TLS 1.2 or newer.

Usage

First, create a one-time use instance of Gibbon::Request:

gibbon = Gibbon::Request.new(api_key: "your_api_key")

Note Only reuse instances of Gibbon after terminating a call with a verb, which makes a request. Requests are light weight objects that update an internal path based on your call chain. When you terminate a call chain with a verb, a request instance makes a request and resets the path. See How call chains build paths.

You can set an individual request's timeout and open_timeout like this:

gibbon.timeout = 30
gibbon.open_timeout = 30

You can read about timeout and open_timeout in the Net::HTTP doc.

Now you can make requests using the resources defined in MailChimp's docs. Resource IDs are specified inline and a CRUD (create, retrieve (or get), update, upsert, or delete) verb initiates the request. upsert lets you update a record, if it exists, or insert it otherwise where supported by MailChimp's API.

Each verb maps to an HTTP method:

Gibbon verb HTTP method Accepts body
create POST yes
retrieve (or get) GET no
update PATCH yes
upsert PUT yes
delete DELETE no

You can specify headers, params, and body when calling a CRUD method. For example:

gibbon.lists.retrieve(headers: {"SomeHeader": "SomeHeaderValue"}, params: {"query_param": "query_param_value"})

Of course, body is only supported on create, update, and upsert calls. Those map to HTTP POST, PATCH, and PUT verbs respectively.

You can set api_key, timeout, open_timeout, faraday_adapter, proxy, symbolize_keys, logger, and debug globally:

Gibbon::Request.api_key = "your_api_key"
Gibbon::Request.timeout = 15
Gibbon::Request.open_timeout = 15
Gibbon::Request.symbolize_keys = true
Gibbon::Request.debug = false

For example, you could set the values above in an initializer file in your Rails app (e.g. your_app/config/initializers/gibbon.rb).

Assuming you've set an api_key on Gibbon, you can conveniently make API calls on the class itself:

Gibbon::Request.lists.retrieve

You can also set the environment variable MAILCHIMP_API_KEY and Gibbon will use it when you create an instance:

gibbon = Gibbon::Request.new

Pass symbolize_keys: true to use symbols (instead of strings) as hash keys in API responses.

gibbon = Gibbon::Request.new(api_key: "your_api_key", symbolize_keys: true)

MailChimp's resource documentation is a list of available resources.

How call chains build paths

Gibbon::Request defines no methods for MailChimp's resources. Any unknown method appends a segment to an internal path and returns the same object; a CRUD verb turns the accumulated path into a request. So gibbon.lists(list_id).members.retrieve builds lists/<list_id>/members and issues a GET.

Three consequences are worth knowing.

Underscores become hyphens. Segments containing a hyphen aren't valid Ruby method names, so write them with underscores and Gibbon converts:

gibbon.lists(list_id).members(subscriber_hash).actions.delete_permanent.create
# POST lists/<list_id>/members/<subscriber_hash>/actions/delete-permanent

send is special cased. Object#send would otherwise shadow the path segment, so Gibbon::Request#send falls through to path building when called with no arguments. That's what makes sending a campaign work:

gibbon.campaigns(campaign_id).actions.send.create
# POST campaigns/<campaign_id>/actions/send

Called with arguments, send keeps its usual Ruby meaning and invokes the named method.

An instance is reusable only after a verb. The path resets when a verb runs, not when a chain goes out of scope. Abandoning a chain leaves its segments in place and corrupts the next call:

gibbon = Gibbon::Request.new(api_key: "your_api_key")

gibbon.lists               # no verb, so nothing is sent and nothing is reset
gibbon.campaigns.retrieve  # GET lists/campaigns -- not what you wanted

Use a fresh instance per chain unless you know the previous one ended in a verb.

Responses

Every CRUD verb returns a Gibbon::Response with two attributes:

  • body — the parsed JSON response, as a Hash (or Array, depending on the endpoint)
  • headers — the response headers, keyed by lower case string
response = gibbon.lists.retrieve

response.body["lists"]           # => [{"id" => "...", "name" => "...", ...}, ...]
response.headers["content-type"] # => "application/json; charset=utf-8"

body respects symbolize_keys; headers keys are always strings.

A response with an empty body — as with a successful delete, which MailChimp answers with 204 No Content — returns nil rather than a Gibbon::Response.

Debug Logging

Pass debug: true to enable debug logging to STDOUT.

gibbon = Gibbon::Request.new(api_key: "your_api_key", debug: true)

Custom logger

Ruby Logger.new is used by default, but it can be overridden using:

gibbon = Gibbon::Request.new(api_key: "your_api_key", debug: true, logger: MyLogger.new)

Logger can be also set by globally:

Gibbon::Request.logger = MyLogger.new

Examples

Lists

Fetch first page of lists:

gibbon.lists.retrieve

Retrieving a specific list looks like:

gibbon.lists(list_id).retrieve

Retrieving a specific list's members looks like:

gibbon.lists(list_id).members.retrieve

Subscribers

Get first page of subscribers for a list:

gibbon.lists(list_id).members.retrieve

By default the Mailchimp API returns 10 results. To set the count to 50:

gibbon.lists(list_id).members.retrieve(params: {"count": "50"})

And to retrieve the next 50 members:

gibbon.lists(list_id).members.retrieve(params: {"count": "50", "offset": "50"})

And to retrieve only subscribed members

gibbon.lists(list_id).members.retrieve(params: {"count": "50", "offset": "50", "status": "subscribed"})

Subscribe a member to a list:

gibbon.lists(list_id).members.create(body: {email_address: "foo@bar.com", status: "subscribed", merge_fields: {FNAME: "First Name", LNAME: "Last Name"}})

If you want to upsert instead, you would do the following:

gibbon.lists(list_id).members(lower_case_md5_hashed_email_address).upsert(body: {email_address: "foo@bar.com", status: "subscribed", merge_fields: {FNAME: "First Name", LNAME: "Last Name"}})

You can also unsubscribe a member from a list:

gibbon.lists(list_id).members(lower_case_md5_hashed_email_address).update(body: { status: "unsubscribed" })

Get a specific member's information (open/click rates etc.) from MailChimp:

gibbon.lists(list_id).members(lower_case_md5_hashed_email_address).retrieve

Permanently delete a specific member from a list:

gibbon.lists(list_id).members(lower_case_md5_hashed_email_address).actions.delete_permanent.create

Tags

Tags are a flexible way to organize (slice and dice) your list: for example, you can send a campaign directly to one or more tags.

Add tags to a subscriber:

gibbon.lists(list_id).members(Digest::MD5.hexdigest(lower_case_email_address)).tags.create(
  body: {
    tags: [{name:"referred-from-xyz", status:"active"},{name:"pro-plan",status:"active"}]
  }
)

Batch Operations

Any API call that can be made directly can also be organized into batch operations. Performing batch operations requires you to generate a hash for each individual API call and pass them as an Array to the Batch endpoint.

# Create a new batch job that will create new list members
gibbon.batches.create(body: {
  operations: [
    {
      method: "POST",
      path: "lists/#{ list_id }/members",
      body: "{...}" # The JSON payload for PUT, POST, or PATCH
    },
    ...
  ]
})

This will create a new batch job and return a Batch response. The response will include an id attribute which can be used to check the status of a particular batch job.

Checking on a Batch Job
gibbon.batches(batch_id).retrieve
Response Body (i.e. response.body)
{
  "id"=>"0ca62e43cc",
  "status"=>"started",
  "total_operations"=>1,
  "finished_operations"=>1,
  "errored_operations"=>0,
  "submitted_at"=>"2016-04-19T01:16:58+00:00",
  "completed_at"=>"",
  "response_body_url"=>""
}

Note This response truncated for brevity. Reference the MailChimp API documentation for Batch Operations for more details.

Fields

Only retrieve ids and names for fetched lists:

gibbon.lists.retrieve(params: {"fields": "lists.id,lists.name"})

Only retrieve emails for fetched lists:

gibbon.lists(list_id).members.retrieve(params: {"fields": "members.email_address"})

Campaigns

Get first page of campaigns:

campaigns = gibbon.campaigns.retrieve

Fetch the number of opens for a campaign

email_stats = gibbon.reports(campaign_id).retrieve.body["opens"]

Create a new campaign:

recipients = {
  list_id: list_id,
  segment_opts: {
    saved_segment_id: segment_id
  }
}
settings = {
  subject_line: "Subject Line",
  title: "Name of Campaign",
  from_name: "From Name",
  reply_to: "my@email.com"
}

body = {
  type: "regular",
  recipients: recipients,
  settings: settings
}

begin
  gibbon.campaigns.create(body: body)
rescue Gibbon::MailChimpError => e
  puts "Houston, we have a problem: #{e.message} - #{e.raw_body}"
end

Add content to a campaign:

(Please note that Mailchimp does not currently support dynamic replacement of mc:edit areas in their drag-and-drop templates using their API. Custom templates can be used instead.)

body = {
  template: {
    id: template_id,
    sections: {
      "name-of-mc-edit-area": "Content here"
    }
  }
}

gibbon.campaigns(campaign_id).content.upsert(body: body)

Send a campaign:

gibbon.campaigns(campaign_id).actions.send.create

Schedule a campaign:

body = {
  schedule_time: "2016-06-27 20:00:00"
}
gibbon.campaigns(campaign_id).actions.schedule.create(body: body)

Interests

Interests are a little more complicated than other parts of the API, so here's an example of how you would set interests at subscription time or update them later. The ID of the interests you want to opt in or out of must be known ahead of time so an example of how to find interest IDs is also included.

Subscribing a member to a list with specific interests up front:

gibbon.lists(list_id).members.create(body: {email_address: user_email_address, status: "subscribed", interests: {some_interest_id: true, another_interest_id: true}})

Updating a list member's interests:

gibbon.lists(list_id).members(member_id).update(body: {interests: {some_interest_id: true, another_interest_id: false}})

So how do we get the interest IDs? When you query the API for a specific list member's information:

gibbon.lists(list_id).members(member_id).retrieve

The response body (i.e. response.body) looks something like this (unrelated things removed):

{"id"=>"...", "email_address"=>"...", ..., "interests"=>{"3def637141"=>true, "f7cc4ee841"=>false, "fcdc951b9f"=>false, "3daf3cf27d"=>true, "293a3703ed"=>false, "72370e0d1f"=>false, "d434d21a1c"=>false, "bdb1ff199f"=>false, "a54e78f203"=>false, "c4527fd018"=>false} ...}

The API returns a map of interest ID to boolean value. Now we need to get interest details so we know what these interest IDs map to. Looking at this doc page, we need to do this:

gibbon.lists(list_id).interest_categories.retrieve

To get a list of interest categories. That gives us something like (again, this is the response.body):

{"list_id"=>"...", "categories"=>[{"list_id"=>"...", "id"=>"0ace7aa498", "title"=>"Food Preferences", ...}] ...}

In this case, we're interested in the ID of the "Food Preferences" interest, which is 0ace7aa498. Now we can fetch the details for this interest group:

gibbon.lists(list_id).interest_categories("0ace7aa498").interests.retrieve

That response gives the interest data, including the ID for the interests themselves, which we can use to update a list member's interests or set them when we call the API to subscribe her or him to a list.

Other

Overriding Gibbon's API endpoint (i.e. if using an access token from OAuth and have the api_endpoint from the metadata):

Gibbon::Request.api_endpoint = "https://us1.api.mailchimp.com"
Gibbon::Request.api_key = your_access_token_or_api_key

You can set an optional proxy url like this (or with an environment variable MAILCHIMP_PROXY):

gibbon.proxy = 'http://your_proxy.com:80'

You can set a different Faraday adapter during initialization:

gibbon = Gibbon::Request.new(api_key: "your_api_key", faraday_adapter: :net_http)

Error handling

Gibbon raises two error types, both subclasses of StandardError.

Gibbon::GibbonError

Raised before a request goes out, when Gibbon can't tell where to send it. In practice that means the API key is missing, or it carries no data center suffix and no api_endpoint was set:

Gibbon::Request.new.lists.retrieve
# => Gibbon::GibbonError: You must set an api_key prior to making a call

Gibbon::Request.new(api_key: "your_api_key").lists.retrieve
# => Gibbon::GibbonError: You must set an api_key prior to making a call

A key like your_api_key-us1 carries its data center (us1) and works on its own. Access tokens obtained through OAuth do not, so pass api_endpoint alongside them — see Other above.

Gibbon::MailChimpError

Raised when a request fails. It exposes title, detail, body, raw_body, and status_code:

begin
  gibbon.lists(list_id).members.create(body: body)
rescue Gibbon::MailChimpError => e
  puts "Houston, we have a problem: #{e.message} - #{e.raw_body}"
end

Some or all of those attributes may be nil depending on the nature of the failure. MailChimp returning a 4xx or 5xx populates all of them from the error document. A transport level failure — a timeout, a refused connection — has no response to read, so only message is set.

Gibbon also raises Gibbon::MailChimpError when a successful response body isn't valid JSON. That error has a title of "UNPARSEABLE_RESPONSE" and a status_code of 500, and its message includes the raw body.

Upgrading

Full release history lives in the CHANGELOG. The breaking changes are summarized here.

Upgrading to 4.0

Gibbon 4.0 contains no API changes. It changes what it runs on:

  • Ruby 3.1 or newer is required. Rubies 2.4 through 3.0 are EOL and are no longer supported. If you are on an older Ruby, stay on Gibbon 3.5.0.
  • TLS 1.2 is now a minimum rather than an exact pin. Connections previously negotiated exactly TLS 1.2; they now accept TLS 1.3 as well. No code change is needed, but if you inspect the negotiated protocol in tests or middleware, expect 1.3.

Upgrading to 3.0

create, retrieve/get, update, upsert, and delete return a Gibbon::Response rather than the parsed body. Add .body:

# Gibbon 2.x
gibbon.lists.retrieve["lists"]

# Gibbon 3.x and newer
gibbon.lists.retrieve.body["lists"]

In exchange you also get .headers, which carries MailChimp's rate limit and content metadata. See Responses.

Thanks

Thanks to everyone who has contributed to Gibbon's development.

Copyright

  • Copyright (c) 2010-2026 Amro Mousa. See LICENSE.txt for details.
  • MailChimp (c) 2001-2026 The Rocket Science Group.

About

Gibbon is an API wrapper for MailChimp's API

Resources

Code of conduct

Stars

Watchers

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages