diff --git a/source/includes/rest/_broadcasts.md b/source/includes/rest/_broadcasts.md index 030220eb6c9..4822c4d4447 100644 --- a/source/includes/rest/_broadcasts.md +++ b/source/includes/rest/_broadcasts.md @@ -157,6 +157,10 @@ with [Set the audience](#set-the-audience). derived Read-only text generated when the campaign is read. derived.audience describes who it goes to in plain English, such as People who are in the "VIP customers" segment, or All people. It is display text: do not parse it, and expect its wording to change. + + metrics + Present only when the request includes include_metrics=true. An object with the campaign's send_count, open_count, click_count and unsubscribe_count. + links An object containing the account's REST API URL. @@ -321,6 +325,10 @@ client.listBroadcasts(options) direction Optional. Filter sort direction with: asc or desc. Defaults to asc. + + include_metrics + Optional. Include include_metrics=true to add a metrics object to each Single-Email Campaign; leave it out to omit them. + @@ -546,7 +554,20 @@ client.fetchBroadcast(broadcastId) ### Arguments -None. + + + + + + + + + + + + + +
KeyDescription
include_metricsOptional. Include include_metrics=true to add a metrics object to the Single-Email Campaign; leave it out to omit it.
## Update a Single-Email Campaign @@ -692,6 +713,10 @@ Status, scheduling, and the audience are not updated here. Each has its own endp postal_address Optional. A physical mailing address for CAN-SPAM compliance. + + primary_purpose + Optional. Either commercial or transactional. A transactional Single-Email Campaign's audience also includes people who have unsubscribed. Use transactional only for messages a person needs whatever their email preferences, such as receipts or account notices. New Single-Email Campaigns are created as commercial. + @@ -924,7 +949,7 @@ Rate limits are applied per user. ## Set the audience -> To send a Single-Email Campaign to a saved segment: +> To send a Single-Email Campaign to a saved segment, leaving out people with either of two tags: ```shell curl -X PUT "https://api.getdrip.com/v2/YOUR_ACCOUNT_ID/broadcasts/BROADCAST_ID/audience" \ @@ -933,7 +958,10 @@ curl -X PUT "https://api.getdrip.com/v2/YOUR_ACCOUNT_ID/broadcasts/BROADCAST_ID/ -u YOUR_API_KEY: \ -d '{ "filter": [ - [{ "type": "segments", "properties": { "operator": "in", "segment_id": "4815162" } }] + [ + { "type": "segments", "properties": { "operator": "in", "segment_id": "4815162" } }, + { "type": "tags", "properties": { "operator": "not_include_any", "value": ["Churned", "Refunded"] } } + ] ] }' ``` @@ -950,7 +978,10 @@ request["User-Agent"] = "Your App Name (www.yourapp.com)" request["Content-Type"] = "application/vnd.api+json" request.body = { filter: [ - [{ type: "segments", properties: { operator: "in", segment_id: "4815162" } }] + [ + { type: "segments", properties: { operator: "in", segment_id: "4815162" } }, + { type: "tags", properties: { operator: "not_include_any", value: ["Churned", "Refunded"] } } + ] ] }.to_json @@ -973,7 +1004,10 @@ const response = await fetch( }, body: JSON.stringify({ filter: [ - [{ type: "segments", properties: { operator: "in", segment_id: "4815162" } }] + [ + { type: "segments", properties: { operator: "in", segment_id: "4815162" } }, + { type: "tags", properties: { operator: "not_include_any", value: ["Churned", "Refunded"] } } + ] ] }) } @@ -1037,12 +1071,71 @@ true of a person, and a person matching any inner list is included. So `[[]]` means everyone eligible to receive it, and is how you clear an audience. An empty filter, `[]`, is rejected. -Today the only condition type available is `segments`, with the operators `in` and -`not_in`. The `segment_id` is the `id` from the Segments API. More condition types will follow. +Two condition types are available: `segments` and `tags`. Each condition is a `type` and a +`properties` object holding the `operator` and its value. More condition types will follow. + +A `segments` condition takes the operator `in` or `not_in` and a `segment_id`, which is the `id` +from the Segments API. `segment_id` takes one id, never a list: `["4815162"]` is rejected. For +several segments, use one condition each. Two conditions in the same inner list mean "in both"; +two in separate inner lists mean "in either". + +A `tags` condition takes an `operator` and a `value`. A tag is named as the Tags API returns it, +and matching ignores case and repeated spaces. A tag does not have to exist on the account yet; +a condition naming a tag no one has matches no one, or everyone when negated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OperatorvalueMatches people who
includeOne tag, as a stringhave the tag
not_includeOne tag, as a stringdo not have the tag
containsText, as a stringhave a tag whose name contains the text
include_anyA list of tagshave at least one of the tags
include_allA list of tagshave every one of the tags
not_include_anyA list of tagshave none of the tags
not_include_allA list of tagsare missing at least one of the tags
no_tagsOmitted, or ""have no tags at all
-`segment_id` takes one id, never a list: `["4815162"]` is rejected. For several segments, use -one condition each. Two conditions in the same inner list mean "in both"; two in separate -inner lists mean "in either". +A blank tag, an empty list, or a blank entry in a list is rejected. Replacing an audience requires `confirm=true` whenever the current audience is anything other than everyone, because what is being replaced cannot be recovered through the API. Read the @@ -1112,7 +1205,10 @@ const body = await response.json(); { "audience": { "filter": [ - [{ "type": "segments", "properties": { "operator": "in", "segment_id": "4815162" } }] + [ + { "type": "segments", "properties": { "operator": "in", "segment_id": "4815162" } }, + { "type": "tags", "properties": { "operator": "not_include_any", "value": ["Churned", "Refunded"] } } + ] ] } } @@ -1124,8 +1220,8 @@ const body = await response.json(); { "audience": { "message": "This filter includes unsupported criteria and cannot be returned as a filter.", - "summary": "People who are tagged with \"vip\"", - "unsupported_types": ["tags"] + "summary": "People who have performed \"Purchased VIP\"", + "unsupported_types": ["actions"] } } ``` @@ -1133,10 +1229,12 @@ const body = await response.json(); `filter` comes back in the same shape it is set in, so reading an audience and writing it straight back changes nothing. -`filter` is present only when writing it again would be accepted. Most audiences built in the +`filter` is present only when writing it again would be accepted. Many audiences built in the Drip app use rules that have no form here yet, and those come back described instead: `summary` is plain-English display text, and `unsupported_types` names the condition types in -the way. Expect that until more condition types are available. +the way. Expect that until more condition types are available. `unsupported_types` is omitted +when every condition type is available but the filter is refused for another reason, such as +its size. A present `filter` is not a promise that its segments still exist. That is not checked on read, so an audience naming a segment deleted since it was set reads back intact and responds @@ -1239,6 +1337,17 @@ const body = await response.json(); } ``` +> If the campaign's audience was never chosen, responds with a 409 Conflict: + +```json +{ + "errors": [{ + "code": "conflict_error", + "message": "Choose this broadcast's audience before scheduling it: set it with PUT /v2/:account_id/broadcasts/:id/audience (set_broadcast_audience). To send to everyone eligible, set the filter [[]]." + }] +} +``` + Sets a draft Single-Email Campaign to send. There are three ways to say when, and `localize_sending_time` is required alongside `send_at` rather than defaulting, because the two readings of the same timestamp differ by hours for most people: @@ -1281,8 +1390,12 @@ Only a `draft` can be scheduled. To change when a scheduled campaign sends, unsc then schedule it again. A campaign that is already scheduled, sending, or sent, or whose account cannot send right now, responds with a `409 Conflict` that says which. -The audience is not checked here. A campaign with no audience set goes to everyone eligible to -receive it, the same as in the Drip app. Read `derived.audience` first if that matters. +The audience has to be chosen first, the same as in the Drip app. Scheduling a draft whose +audience was never set responds with a `409 Conflict`. Choose it with +[Set the audience](#set-the-audience); to send to everyone eligible to receive it, set the +filter `[[]]`. A draft whose recipients were chosen in the Drip app, or that already has a +filter, schedules as it is. [Fetch the audience](#fetch-the-audience) first to confirm who it +goes to. With `auto_resend`, a second copy goes to people who did not open the first, `resend_delay` whole days later. diff --git a/source/includes/rest/_campaigns.md b/source/includes/rest/_campaigns.md index d1d82202491..549350347ef 100644 --- a/source/includes/rest/_campaigns.md +++ b/source/includes/rest/_campaigns.md @@ -407,6 +407,207 @@ client.pauseCampaign(campaignId) None. +## List all emails in an Email Series Campaign + +> To list the emails in an Email Series Campaign: + +```shell +curl "https://api.getdrip.com/v2/YOUR_ACCOUNT_ID/campaigns/CAMPAIGN_ID/emails" \ + -H 'User-Agent: Your App Name (www.yourapp.com)' \ + -u YOUR_API_KEY: +``` + +```ruby +require "net/http" + +uri = URI("https://api.getdrip.com/v2/YOUR_ACCOUNT_ID/campaigns/CAMPAIGN_ID/emails") + +request = Net::HTTP::Get.new(uri) +request.basic_auth("YOUR_API_KEY", "") +request["User-Agent"] = "Your App Name (www.yourapp.com)" + +response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(request) +end + +puts response.body +``` + +```javascript +const response = await fetch( + "https://api.getdrip.com/v2/YOUR_ACCOUNT_ID/campaigns/CAMPAIGN_ID/emails", + { + headers: { + "User-Agent": "Your App Name (www.yourapp.com)", + "Authorization": "Basic " + Buffer.from("YOUR_API_KEY:").toString("base64") + } + } +); + +const body = await response.json(); +``` + +> The response looks like this: + +```json +{ + "links": { + "emails.account": "https://api.getdrip.com/v2/accounts/{emails.account}", + "emails.campaign": "https://api.getdrip.com/v2/{emails.account}/campaigns/{emails.campaign}" + }, + "emails": [{ + "id": "7654321", + "href": "https://api.getdrip.com/v2/9999999/campaigns/123456/emails/7654321", + "sending_delay": 0, + "sending_order": 0, + "subject": "Welcome to the course, {{ subscriber.first_name }}", + "html_body": "

Welcome!

", + "text_body": "Welcome!", + "created_at": "2026-06-21T10:31:58Z", + "open_rate": 0.42, + "click_rate": 0.085, + "links": { + "account": "9999999", + "campaign": "123456" + } + }] +} +``` + +Returns every email in the Email Series Campaign at once. Includes draft emails. The list is not +paginated. + +**Properties** + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDescription
idThe email's id. Use it to fetch the email.
hrefThe url for retrieving the email via the REST API.
sending_delayHow long Drip waits before sending this email, in the unit set for it in the campaign: minutes, hours, or days. The unit is not included.
sending_orderThe email's position in the campaign. The list is returned in this order.
subjectThe email's subject.
html_bodyThe HTML content used in the email's body.
text_bodyThe plain text content used in the email's body.
created_atA timestamp for when the email was created.
open_rateThe email's open rate, as a decimal: 0.42 is 42%.
click_rateThe email's click rate, as a decimal: 0.085 is 8.5%.
linksAn object containing the ids of the email's account and Email Series Campaign.
+ +Liquid in `subject`, `html_body`, and `text_body` is returned as written, not rendered for a +subscriber. + +### HTTP Endpoint + +`GET /v2/:account_id/campaigns/:campaign_id/emails` + +### Arguments + +None. + +## Fetch an email in an Email Series Campaign + +> To fetch one email in an Email Series Campaign: + +```shell +curl "https://api.getdrip.com/v2/YOUR_ACCOUNT_ID/campaigns/CAMPAIGN_ID/emails/EMAIL_ID" \ + -H 'User-Agent: Your App Name (www.yourapp.com)' \ + -u YOUR_API_KEY: +``` + +```ruby +require "net/http" + +uri = URI("https://api.getdrip.com/v2/YOUR_ACCOUNT_ID/campaigns/CAMPAIGN_ID/emails/EMAIL_ID") + +request = Net::HTTP::Get.new(uri) +request.basic_auth("YOUR_API_KEY", "") +request["User-Agent"] = "Your App Name (www.yourapp.com)" + +response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(request) +end + +puts response.body +``` + +```javascript +const response = await fetch( + "https://api.getdrip.com/v2/YOUR_ACCOUNT_ID/campaigns/CAMPAIGN_ID/emails/EMAIL_ID", + { + headers: { + "User-Agent": "Your App Name (www.yourapp.com)", + "Authorization": "Basic " + Buffer.from("YOUR_API_KEY:").toString("base64") + } + } +); + +const body = await response.json(); +``` + +> The response looks like this: + +```json +# The emails property is an array of one email object. +{ + "links": { ... }, + "emails": [{ ... }] +} +``` + +Returns one email, with the same properties as +[List all emails in an Email Series Campaign](#list-all-emails-in-an-email-series-campaign). +Responds with a `404 Not Found` if the Email Series Campaign does not exist or the email is not +part of it. + +### HTTP Endpoint + +`GET /v2/:account_id/campaigns/:campaign_id/emails/:email_id` + +### Arguments + +None. + ## List all subscribers subscribed to an Email Series Campaign > To list subscribers on an Email Series Campaign: diff --git a/source/includes/rest/_rate_limiting.md b/source/includes/rest/_rate_limiting.md index 75ba47010b4..bce0b990e2f 100644 --- a/source/includes/rest/_rate_limiting.md +++ b/source/includes/rest/_rate_limiting.md @@ -23,4 +23,6 @@ Content-Type: application/json You can make up to 50 requests per hour for batch endpoints and 3,600 individual requests per hour for other API endpoints. Each batch request accommodates up to 1,000 records in the payload for a total of 50,000 updates per hour. +[Fetch a workflow's details](#fetch-a-workflow-39-s-details) also has its own limit of 2,000 requests per account per hour. Past that, it responds with a `429 Too Many Requests` and a `Retry-After` header giving the number of seconds to wait. + To minimize your API requests, use the [Batch API](#batch-api). diff --git a/source/includes/rest/_tags.md b/source/includes/rest/_tags.md index b0d27062b0e..06c977a8010 100644 --- a/source/includes/rest/_tags.md +++ b/source/includes/rest/_tags.md @@ -216,7 +216,7 @@ client.removeSubscriberTag(email, tag) ### HTTP Endpoint -`DELETE /:account_id/subscribers/:email/tags/:tag` +`DELETE /v2/:account_id/subscribers/:email/tags/:tag` ### Arguments