From 3745567c35a95f57a0e1c8a36d55962ce1e4782d Mon Sep 17 00:00:00 2001 From: Stephen Young Date: Thu, 7 May 2026 14:16:23 -0400 Subject: [PATCH] Add batch method for Track v2 batch API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Client#batch for POST /api/v2/batch, supporting mixed operations (identify, event, delete, etc.) for people and objects in a single request. Passes through the operations array — the API validates individual items. Closes #106 --- README.md | 29 ++++++++++++++++ lib/customerio/client.rb | 10 ++++++ spec/client_spec.rb | 73 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/README.md b/README.md index cf6a2fd..275a631 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,35 @@ Start tracking events and identifies again for a previously suppressed customer. $customerio.unsuppress(5) ``` +### Batch operations + +You can send up to 500KB of operations in a single request using the [Track v2 batch endpoint](https://customer.io/docs/api/track/#tag/Track-v2/operation/batch). Each operation can identify, track events, or delete people and objects. + +```ruby +$customerio.batch([ + { + type: "person", + identifiers: { id: "42" }, + action: "identify", + attributes: { first_name: "Jane", plan: "premium" }, + }, + { + type: "person", + identifiers: { id: "42" }, + action: "event", + name: "purchase", + data: { amount: 99 }, + }, + { + type: "person", + identifiers: { id: "99" }, + action: "delete", + }, +]) +``` + +See the [Track v2 API docs](https://customer.io/docs/api/track/#tag/Track-v2/operation/batch) for the full list of supported actions and fields. + ### Send Transactional Messages To use the Customer.io [Transactional API](https://customer.io/docs/transactional-api), create an instance of the API client using an [app key](https://customer.io/docs/managing-credentials#app-api-keys) and create a request object of your message type. diff --git a/lib/customerio/client.rb b/lib/customerio/client.rb index 078adc5..859c4c8 100644 --- a/lib/customerio/client.rb +++ b/lib/customerio/client.rb @@ -125,6 +125,12 @@ def merge_customers(primary_id_type, primary_id, secondary_id_type, secondary_id @client.request_and_verify_response(:post, merge_customers_path, body) end + def batch(operations) + raise ParamError, "operations must be a non-empty array" unless operations.is_a?(Array) && !operations.empty? + + @client.request_and_verify_response(:post, batch_path, batch: operations) + end + private def escape(val) @@ -160,6 +166,10 @@ def merge_customers_path "/api/v1/merge_customers" end + def batch_path + "/api/v2/batch" + end + def create_or_update(attributes = {}) attributes = symbolize_keys(attributes) if empty?(attributes[:id]) && empty?(attributes[:cio_id]) && empty?(attributes[:customer_id]) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index abc79a9..19f14fe 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -715,4 +715,77 @@ def json(data) client.merge_customers(Customerio::IdentifierType::ID, "ID1", Customerio::IdentifierType::EMAIL, "hello@company.com") end end + + describe "#batch" do + it "sends a POST request to the batch API with mixed operations" do + operations = [ + { + type: "person", + identifiers: { id: "42" }, + action: "identify", + attributes: { first_name: "Jane", plan: "premium" } + }, + { + type: "person", + identifiers: { id: "42" }, + action: "event", + name: "purchase", + data: { amount: 99 } + }, + { + type: "person", + identifiers: { id: "99" }, + action: "delete" + } + ] + + stub_request(:post, api_uri('/api/v2/batch')). + with(body: json({ batch: operations })). + to_return(status: 200, body: "", headers: {}) + + client.batch(operations) + end + + it "sends object operations" do + operations = [ + { + type: "object", + identifiers: { object_type_id: "1", object_id: "acme" }, + action: "identify", + attributes: { name: "Acme Corp" } + } + ] + + stub_request(:post, api_uri('/api/v2/batch')). + with(body: json({ batch: operations })). + to_return(status: 200, body: "", headers: {}) + + client.batch(operations) + end + + it "raises an error when operations is empty" do + lambda { client.batch([]) }.should raise_error(Customerio::Client::ParamError) + end + + it "raises an error when operations is not an array" do + lambda { client.batch("not an array") }.should raise_error(Customerio::Client::ParamError) + end + + it "raises an error on non-2xx response" do + operations = [ + { + type: "person", + identifiers: { id: "42" }, + action: "identify", + attributes: { first_name: "Jane" } + } + ] + + stub_request(:post, api_uri('/api/v2/batch')). + with(body: json({ batch: operations })). + to_return(status: 400, body: "Bad request", headers: {}) + + lambda { client.batch(operations) }.should raise_error(Customerio::InvalidResponse) + end + end end