# gibbon Gibbon is an API wrapper for MailChimp's [API](https://mailchimp.com/developer/marketing/api/). [![Build Status](https://github.com/amro/gibbon/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/amro/gibbon/actions/workflows/ci.yml) ## Important Notes Please read MailChimp's [Getting Started Guide](https://mailchimp.com/developer/marketing/guides/quick-start/). Gibbon returns a `Gibbon::Response`, which exposes the parsed response `body` and the response `headers`. See [Responses](#responses) below. See the [CHANGELOG](CHANGELOG.md) for release history and [Upgrading](#upgrading) for breaking changes. ## Installation $ gem install gibbon Or add it to your `Gemfile`: ```ruby 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](https://admin.mailchimp.com/account/api). Gibbon depends on [Faraday](https://github.com/lostisland/faraday) (both 1.x and 2.x are supported) and [MultiJson](https://github.com/intridea/multi_json). Connections to MailChimp negotiate TLS 1.2 or newer. ## Usage First, create a *one-time use instance* of `Gibbon::Request`: ```ruby 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](#how-call-chains-build-paths). You can set an individual request's `timeout` and `open_timeout` like this: ```ruby gibbon.timeout = 30 gibbon.open_timeout = 30 ``` You can read about `timeout` and `open_timeout` in the [Net::HTTP](https://docs.ruby-lang.org/en/master/Net/HTTP.html) doc. Now you can make requests using the resources defined in [MailChimp's docs](https://mailchimp.com/developer/marketing/api/). 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: ```ruby 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: ```ruby 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: ```ruby Gibbon::Request.lists.retrieve ``` You can also set the environment variable `MAILCHIMP_API_KEY` and Gibbon will use it when you create an instance: ```ruby gibbon = Gibbon::Request.new ``` Pass `symbolize_keys: true` to use symbols (instead of strings) as hash keys in API responses. ```ruby gibbon = Gibbon::Request.new(api_key: "your_api_key", symbolize_keys: true) ``` MailChimp's [resource documentation](https://mailchimp.com/developer/marketing/api/) 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//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: ```ruby gibbon.lists(list_id).members(subscriber_hash).actions.delete_permanent.create # POST lists//members//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: ```ruby gibbon.campaigns(campaign_id).actions.send.create # POST campaigns//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: ```ruby 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 ```ruby 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. ```ruby 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: ```ruby gibbon = Gibbon::Request.new(api_key: "your_api_key", debug: true, logger: MyLogger.new) ``` Logger can be also set by globally: ```ruby Gibbon::Request.logger = MyLogger.new ``` ## Examples ### Lists Fetch first page of lists: ```ruby gibbon.lists.retrieve ``` Retrieving a specific list looks like: ```ruby gibbon.lists(list_id).retrieve ``` Retrieving a specific list's members looks like: ```ruby gibbon.lists(list_id).members.retrieve ``` ### Subscribers Get first page of subscribers for a list: ```ruby gibbon.lists(list_id).members.retrieve ``` By default the Mailchimp API returns 10 results. To set the count to 50: ```ruby gibbon.lists(list_id).members.retrieve(params: {"count": "50"}) ``` And to retrieve the next 50 members: ```ruby gibbon.lists(list_id).members.retrieve(params: {"count": "50", "offset": "50"}) ``` And to retrieve only subscribed members ```ruby gibbon.lists(list_id).members.retrieve(params: {"count": "50", "offset": "50", "status": "subscribed"}) ``` Subscribe a member to a list: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby gibbon.lists(list_id).members(lower_case_md5_hashed_email_address).retrieve ``` Permanently delete a specific member from a list: ```ruby gibbon.lists(list_id).members(lower_case_md5_hashed_email_address).actions.delete_permanent.create ``` ### Tags [Tags](https://mailchimp.com/help/getting-started-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: ```ruby 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. ```ruby # 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 ```ruby gibbon.batches(batch_id).retrieve ``` ###### Response Body (i.e. `response.body`) ```ruby { "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](https://mailchimp.com/developer/marketing/api/batches/) for more details. ### Fields Only retrieve ids and names for fetched lists: ```ruby gibbon.lists.retrieve(params: {"fields": "lists.id,lists.name"}) ``` Only retrieve emails for fetched lists: ```ruby gibbon.lists(list_id).members.retrieve(params: {"fields": "members.email_address"}) ``` ### Campaigns Get first page of campaigns: ```ruby campaigns = gibbon.campaigns.retrieve ``` Fetch the number of opens for a campaign ```ruby email_stats = gibbon.reports(campaign_id).retrieve.body["opens"] ``` Create a new campaign: ```ruby 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](http://stackoverflow.com/questions/29366766/mailchimp-api-not-replacing-mcedit-content-sections-using-ruby-library) instead.)* ```ruby body = { template: { id: template_id, sections: { "name-of-mc-edit-area": "Content here" } } } gibbon.campaigns(campaign_id).content.upsert(body: body) ``` Send a campaign: ```ruby gibbon.campaigns(campaign_id).actions.send.create ``` Schedule a campaign: ```ruby body = { schedule_time: "2016-06-27 20:00:00" } ``` ```ruby 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: ```ruby 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: ```ruby 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: ```ruby gibbon.lists(list_id).members(member_id).retrieve ``` The response body (i.e. `response.body`) looks something like this (unrelated things removed): ```ruby {"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](https://mailchimp.com/developer/marketing/api/interests/), we need to do this: ```ruby 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`): ```ruby {"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: ```ruby 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](https://mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/)): ```ruby 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): ```ruby gibbon.proxy = 'http://your_proxy.com:80' ``` You can set a different [Faraday adapter](https://github.com/lostisland/faraday) during initialization: ```ruby 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: ```ruby 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](#other) above. ### Gibbon::MailChimpError Raised when a request fails. It exposes `title`, `detail`, `body`, `raw_body`, and `status_code`: ```ruby 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](CHANGELOG.md). 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`: ```ruby # 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](#responses). ## Thanks Thanks to everyone who has [contributed](https://github.com/amro/gibbon/contributors) to Gibbon's development. ## Copyright * Copyright (c) 2010-2026 Amro Mousa. See LICENSE.txt for details. * MailChimp (c) 2001-2026 The Rocket Science Group.