Gibbon is an API wrapper for MailChimp's API.
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.
$ gem install gibbon
Or add it to your Gemfile:
gem "gibbon"- 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.
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 = 30You 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 = falseFor 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.retrieveYou can also set the environment variable MAILCHIMP_API_KEY and Gibbon will use it when you create an instance:
gibbon = Gibbon::Request.newPass 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.
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-permanentsend 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/sendCalled 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 wantedUse a fresh instance per chain unless you know the previous one ended in a verb.
Every CRUD verb returns a Gibbon::Response with two attributes:
body— the parsed JSON response, as aHash(orArray, 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.
Pass debug: true to enable debug logging to STDOUT.
gibbon = Gibbon::Request.new(api_key: "your_api_key", debug: true)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.newFetch first page of lists:
gibbon.lists.retrieveRetrieving a specific list looks like:
gibbon.lists(list_id).retrieveRetrieving a specific list's members looks like:
gibbon.lists(list_id).members.retrieveGet first page of subscribers for a list:
gibbon.lists(list_id).members.retrieveBy 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).retrievePermanently delete a specific member from a list:
gibbon.lists(list_id).members(lower_case_md5_hashed_email_address).actions.delete_permanent.createTags 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"}]
}
)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.
gibbon.batches(batch_id).retrieve{
"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.
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"})Get first page of campaigns:
campaigns = gibbon.campaigns.retrieveFetch 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}"
endAdd 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.createSchedule a campaign:
body = {
schedule_time: "2016-06-27 20:00:00"
}gibbon.campaigns(campaign_id).actions.schedule.create(body: body)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).retrieveThe 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.retrieveTo 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.retrieveThat 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.
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_keyYou 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)Gibbon raises two error types, both subclasses of StandardError.
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 callA 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.
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}"
endSome 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.
Full release history lives in the CHANGELOG. The breaking changes are summarized here.
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.
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 to everyone who has contributed to Gibbon's development.
- Copyright (c) 2010-2026 Amro Mousa. See LICENSE.txt for details.
- MailChimp (c) 2001-2026 The Rocket Science Group.