diff --git a/README.md b/README.md index cf6a2fd..0b1c402 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,45 @@ rescue Customerio::InvalidResponse => e end ``` +### Trigger Broadcasts + +You can trigger [API-triggered broadcasts](https://customer.io/docs/api-triggered-broadcasts/) using the `APIClient`. Create a `TriggerBroadcastRequest` with the broadcast's numeric ID and optional audience/data parameters. + +```ruby +require "customerio" + +client = Customerio::APIClient.new("your API key", region: Customerio::Regions::US) + +request = Customerio::TriggerBroadcastRequest.new( + broadcast_id: 12, + emails: ["recipient@example.com"], + data: { + headline: "Roadrunner spotted in Albuquerque!", + date: 1511315635, + }, + email_add_duplicates: false, + email_ignore_missing: false, + id_ignore_missing: false, +) + +begin + response = client.trigger_broadcast(request) + puts response +rescue Customerio::InvalidResponse => e + puts e.code, e.message +end +``` + +You can target the broadcast audience in several ways. Only one audience option can be present per request: + +- `recipients`: a hash with filter conditions (e.g., `{ segment: { id: 7 } }`) +- `emails`: an array of email addresses +- `ids`: an array of customer IDs +- `per_user_data`: an array of per-user objects +- `data_file_url`: a URL to a JSON lines file + +If you omit the audience option, the broadcast uses its default audience configured in the UI. + ## Contributing 1. Fork it diff --git a/lib/customerio.rb b/lib/customerio.rb index a70b5e0..327b974 100644 --- a/lib/customerio.rb +++ b/lib/customerio.rb @@ -11,6 +11,7 @@ module Customerio require "customerio/requests/send_sms_request" require "customerio/requests/send_inbox_message_request" require "customerio/requests/send_in_app_request" + require "customerio/requests/trigger_broadcast_request" require "customerio/api" require "customerio/param_encoder" end diff --git a/lib/customerio/api.rb b/lib/customerio/api.rb index 651f31c..54fa007 100644 --- a/lib/customerio/api.rb +++ b/lib/customerio/api.rb @@ -46,6 +46,12 @@ def send_in_app(req) deliver(send_in_app_path, req.message) end + def trigger_broadcast(req) + validate_request!(req, TriggerBroadcastRequest) + + deliver(trigger_broadcast_path(req.broadcast_id), req.message) + end + private def deliver(path, message) @@ -87,5 +93,9 @@ def send_inbox_message_path def send_in_app_path "/v1/send/in_app" end + + def trigger_broadcast_path(broadcast_id) + "/v1/campaigns/#{broadcast_id}/triggers" + end end end diff --git a/lib/customerio/requests/trigger_broadcast_request.rb b/lib/customerio/requests/trigger_broadcast_request.rb new file mode 100644 index 0000000..6316362 --- /dev/null +++ b/lib/customerio/requests/trigger_broadcast_request.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Customerio + class TriggerBroadcastRequest + AUDIENCE_FIELDS = %i[recipients emails ids per_user_data data_file_url].freeze + + OPTIONAL_FIELDS = %i[data email_add_duplicates email_ignore_missing id_ignore_missing].freeze + + attr_reader :broadcast_id, :message + + def initialize(opts) + raise ArgumentError, "broadcast_id is required" unless opts.key?(:broadcast_id) + raise ArgumentError, "broadcast_id must be an integer" unless opts[:broadcast_id].is_a?(Integer) + + @broadcast_id = opts[:broadcast_id] + @message = opts.select { |field, _value| valid_field?(field) } + + audience = AUDIENCE_FIELDS.select { |field| @message.key?(field) } + raise ArgumentError, "only one of #{AUDIENCE_FIELDS.join(', ')} can be present" if audience.length > 1 + end + + private + + def valid_field?(field) + OPTIONAL_FIELDS.include?(field) || AUDIENCE_FIELDS.include?(field) + end + end +end diff --git a/spec/api_client_spec.rb b/spec/api_client_spec.rb index 9150592..f226df8 100644 --- a/spec/api_client_spec.rb +++ b/spec/api_client_spec.rb @@ -429,4 +429,127 @@ def json(data) ) end end + + describe "#trigger_broadcast" do + it "sends a POST request to the broadcast triggers path" do + req = Customerio::TriggerBroadcastRequest.new( + broadcast_id: 12, + data: { headline: "Test" }, + recipients: { segment: { id: 7 } }, + ) + + stub_request(:post, api_uri('/v1/campaigns/12/triggers')) + .with(headers: request_headers, body: req.message) + .to_return(status: 200, body: { trigger_id: "abc123" }.to_json, headers: {}) + + client.trigger_broadcast(req).should eq({ "trigger_id" => "abc123" }) + end + + it "sends with email list audience" do + req = Customerio::TriggerBroadcastRequest.new( + broadcast_id: 12, + emails: ["a@example.com", "b@example.com"], + email_add_duplicates: false, + email_ignore_missing: true, + ) + + stub_request(:post, api_uri('/v1/campaigns/12/triggers')) + .with(headers: request_headers, body: req.message) + .to_return(status: 200, body: { trigger_id: "abc123" }.to_json, headers: {}) + + client.trigger_broadcast(req).should eq({ "trigger_id" => "abc123" }) + end + + it "sends with id list audience" do + req = Customerio::TriggerBroadcastRequest.new( + broadcast_id: 12, + ids: [1, 2, 3], + id_ignore_missing: true, + ) + + stub_request(:post, api_uri('/v1/campaigns/12/triggers')) + .with(headers: request_headers, body: req.message) + .to_return(status: 200, body: { trigger_id: "abc123" }.to_json, headers: {}) + + client.trigger_broadcast(req).should eq({ "trigger_id" => "abc123" }) + end + + it "sends with data_file_url audience" do + req = Customerio::TriggerBroadcastRequest.new( + broadcast_id: 12, + data_file_url: "https://example.com/data.json", + ) + + stub_request(:post, api_uri('/v1/campaigns/12/triggers')) + .with(headers: request_headers, body: req.message) + .to_return(status: 200, body: { trigger_id: "abc123" }.to_json, headers: {}) + + client.trigger_broadcast(req).should eq({ "trigger_id" => "abc123" }) + end + + it "raises an error when broadcast_id is missing" do + lambda { + Customerio::TriggerBroadcastRequest.new(data: { headline: "Test" }) + }.should raise_error(ArgumentError, "broadcast_id is required") + end + + it "raises an error when broadcast_id is not an integer" do + lambda { + Customerio::TriggerBroadcastRequest.new(broadcast_id: "12") + }.should raise_error(ArgumentError, "broadcast_id must be an integer") + end + + it "raises an error when multiple audience fields are provided" do + lambda { + Customerio::TriggerBroadcastRequest.new( + broadcast_id: 12, + emails: ["a@example.com"], + ids: [1, 2], + ) + }.should raise_error(ArgumentError, /only one of/) + end + + it "raises an error when request is not a TriggerBroadcastRequest" do + lambda { + client.trigger_broadcast("not a request") + }.should raise_error(ArgumentError, /must be an instance of/) + end + + it "handles validation failures (400)" do + req = Customerio::TriggerBroadcastRequest.new( + broadcast_id: 12, + emails: ["a@example.com"], + ) + + err_json = { meta: { error: "example error" } }.to_json + + stub_request(:post, api_uri('/v1/campaigns/12/triggers')) + .with(headers: request_headers, body: req.message) + .to_return(status: 400, body: err_json, headers: {}) + + lambda { client.trigger_broadcast(req) }.should( + raise_error(Customerio::InvalidResponse) { |error| + error.message.should eq "example error" + error.code.should eq "400" + } + ) + end + + it "handles other failures (5xx)" do + req = Customerio::TriggerBroadcastRequest.new( + broadcast_id: 12, + ) + + stub_request(:post, api_uri('/v1/campaigns/12/triggers')) + .with(headers: request_headers, body: req.message) + .to_return(status: 500, body: "Server unavailable", headers: {}) + + lambda { client.trigger_broadcast(req) }.should( + raise_error(Customerio::InvalidResponse) { |error| + error.message.should eq "Server unavailable" + error.code.should eq "500" + } + ) + end + end end