resend 0.27.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,42 +6,96 @@ module Resend
6
6
  class << self
7
7
  # https://resend.com/docs/api-reference/contacts/create-contact
8
8
  def create(params)
9
- path = "audiences/#{params[:audience_id]}/contacts"
10
- Resend::Request.new(path, params, "post").perform
9
+ if params[:audience_id]
10
+ path = "audiences/#{params[:audience_id]}/contacts"
11
+ # Audience-scoped contacts don't support properties
12
+ payload = params.reject { |key, _| key == :properties }
13
+ else
14
+ path = "contacts"
15
+ payload = params
16
+ end
17
+ Resend::Request.new(path, payload, "post").perform
11
18
  end
12
19
 
13
20
  #
14
- # Retrieves a contact from an audience
21
+ # Retrieves a contact
15
22
  #
16
- # @param audience_id [String] the audience id
17
- # @param id [String] either the contact id or contact's email
23
+ # @param params [Hash] the parameters
24
+ # @option params [String] :id either the contact id or contact's email (required)
25
+ # @option params [String] :audience_id optional audience id to scope the operation
26
+ #
27
+ # @example Get contact by ID
28
+ # Resend::Contacts.get(id: "contact_123")
29
+ #
30
+ # @example Get contact scoped to an audience
31
+ # Resend::Contacts.get(id: "contact_123", audience_id: "aud_456")
18
32
  #
19
33
  # https://resend.com/docs/api-reference/contacts/get-contact
20
- def get(audience_id, id)
21
- path = "audiences/#{audience_id}/contacts/#{id}"
34
+ def get(params = {})
35
+ raise ArgumentError, "Missing `id` or `email` field" if params[:id].nil? && params[:email].nil?
36
+
37
+ audience_id = params[:audience_id]
38
+ contact_id = params[:id] || params[:email]
39
+ path = if audience_id
40
+ "audiences/#{audience_id}/contacts/#{contact_id}"
41
+ else
42
+ "contacts/#{contact_id}"
43
+ end
22
44
  Resend::Request.new(path, {}, "get").perform
23
45
  end
24
46
 
25
47
  #
26
- # List contacts in an audience
48
+ # List contacts
49
+ #
50
+ # @param params [Hash] optional parameters including pagination
51
+ # @option params [String] :audience_id optional audience id to scope the operation
52
+ # @option params [Integer] :limit number of records to return
53
+ # @option params [String] :cursor pagination cursor
54
+ #
55
+ # @example List all contacts
56
+ # Resend::Contacts.list
57
+ #
58
+ # @example List contacts with pagination
59
+ # Resend::Contacts.list(limit: 10)
60
+ #
61
+ # @example List contacts scoped to an audience
62
+ # Resend::Contacts.list(audience_id: "aud_456", limit: 10)
27
63
  #
28
- # @param audience_id [String] the audience id
29
- # @param params [Hash] optional pagination parameters
30
64
  # https://resend.com/docs/api-reference/contacts/list-contacts
31
- def list(audience_id, params = {})
32
- path = Resend::PaginationHelper.build_paginated_path("audiences/#{audience_id}/contacts", params)
65
+ def list(params = {})
66
+ audience_id = params[:audience_id]
67
+ path = if audience_id
68
+ Resend::PaginationHelper.build_paginated_path("audiences/#{audience_id}/contacts", params)
69
+ else
70
+ Resend::PaginationHelper.build_paginated_path("contacts", params)
71
+ end
33
72
  Resend::Request.new(path, {}, "get").perform
34
73
  end
35
74
 
36
75
  #
37
- # Remove a contact from an audience
76
+ # Remove a contact
77
+ #
78
+ # @param params [Hash] the parameters
79
+ # @option params [String] :id either the contact id or contact email (required)
80
+ # @option params [String] :audience_id optional audience id to scope the operation
38
81
  #
39
- # @param audience_id [String] the audience id
40
- # @param contact_id [String] either the contact id or contact email
82
+ # @example Remove contact by ID
83
+ # Resend::Contacts.remove(id: "contact_123")
41
84
  #
42
- # see also: https://resend.com/docs/api-reference/contacts/delete-contact
43
- def remove(audience_id, contact_id)
44
- path = "audiences/#{audience_id}/contacts/#{contact_id}"
85
+ # @example Remove contact scoped to an audience
86
+ # Resend::Contacts.remove(id: "contact_123", audience_id: "aud_456")
87
+ #
88
+ # https://resend.com/docs/api-reference/contacts/delete-contact
89
+ def remove(params = {})
90
+ raise ArgumentError, "Missing `id` or `email` field" if params[:id].nil? && params[:email].nil?
91
+
92
+ audience_id = params[:audience_id]
93
+ contact_id = params[:id] || params[:email]
94
+ path = if audience_id
95
+ "audiences/#{audience_id}/contacts/#{contact_id}"
96
+ else
97
+ "contacts/#{contact_id}"
98
+ end
45
99
  Resend::Request.new(path, {}, "delete").perform
46
100
  end
47
101
 
@@ -51,10 +105,18 @@ module Resend
51
105
  # @param params [Hash] the contact params
52
106
  # https://resend.com/docs/api-reference/contacts/update-contact
53
107
  def update(params)
54
- raise ArgumentError, "id or email is required" if params[:id].nil? && params[:email].nil?
108
+ raise ArgumentError, "Missing `id` or `email` field" if params[:id].nil? && params[:email].nil?
55
109
 
56
- path = "audiences/#{params[:audience_id]}/contacts/#{params[:id] || params[:email]}"
57
- Resend::Request.new(path, params, "patch").perform
110
+ contact_id = params[:id] || params[:email]
111
+ if params[:audience_id]
112
+ path = "audiences/#{params[:audience_id]}/contacts/#{contact_id}"
113
+ # Audience-scoped contacts don't support properties
114
+ payload = params.reject { |key, _| key == :properties }
115
+ else
116
+ path = "contacts/#{contact_id}"
117
+ payload = params
118
+ end
119
+ Resend::Request.new(path, payload, "patch").perform
58
120
  end
59
121
  end
60
122
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resend
4
+ module Domains
5
+ # Domain Claims API wrapper
6
+ module Claims
7
+ class << self
8
+ #
9
+ # Start a claim for a domain that another Resend account has already verified.
10
+ # The domain is recreated under your account with fresh DKIM keys, so the
11
+ # previous account's DNS records cannot be reused. Returns a TXT record to add
12
+ # to your DNS to prove ownership. Uses the same request body as creating a domain.
13
+ #
14
+ # @param params [Hash] the parameters
15
+ # @option params [String] :name the name of the domain you want to claim (required)
16
+ # @option params [String] :region the region where emails will be sent from.
17
+ # Possible values: 'us-east-1' | 'eu-west-1' | 'sa-east-1' | 'ap-northeast-1'
18
+ # @option params [String] :custom_return_path subdomain for the Return-Path address (default 'send')
19
+ # @option params [String] :tracking_subdomain the custom subdomain used for click and open tracking links
20
+ # @option params [Boolean] :click_tracking track clicks within the body of each HTML email
21
+ # @option params [Boolean] :open_tracking track the open rate of each email
22
+ #
23
+ # https://resend.com/docs/api-reference/domains/claim-domain
24
+ def create(params)
25
+ path = "domains/claim"
26
+ Resend::Request.new(path, params, "post").perform
27
+ end
28
+
29
+ #
30
+ # Retrieve the latest claim for the placeholder domain created by the claim.
31
+ #
32
+ # @param domain_id [String] the ID of the placeholder domain created by the claim
33
+ #
34
+ # https://resend.com/docs/api-reference/domains/get-domain-claim
35
+ def get(domain_id = "")
36
+ path = "domains/#{domain_id}/claim"
37
+ Resend::Request.new(path, {}, "get").perform
38
+ end
39
+
40
+ #
41
+ # Trigger asynchronous DNS verification and ownership transfer for a domain claim.
42
+ # The claim stays 'pending' while verification runs; poll `get` for status. Once
43
+ # 'completed', the transferred domain has new DKIM records that must be added to
44
+ # DNS and verified via Resend::Domains.verify.
45
+ #
46
+ # @param domain_id [String] the ID of the placeholder domain created by the claim
47
+ #
48
+ # https://resend.com/docs/api-reference/domains/verify-domain-claim
49
+ def verify(domain_id = "")
50
+ path = "domains/#{domain_id}/claim/verify"
51
+ Resend::Request.new(path, {}, "post").perform
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resend
4
+ module Emails
5
+ # Module for sent email attachments API operations
6
+ module Attachments
7
+ class << self
8
+ # Retrieve a single attachment from a sent email
9
+ #
10
+ # @param params [Hash] Parameters for retrieving the attachment
11
+ # @option params [String] :id The attachment ID (required)
12
+ # @option params [String] :email_id The email ID (required)
13
+ # @return [Hash] The attachment object
14
+ #
15
+ # @example
16
+ # Resend::Emails::Attachments.get(
17
+ # id: "2a0c9ce0-3112-4728-976e-47ddcd16a318",
18
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
19
+ # )
20
+ def get(params = {})
21
+ attachment_id = params[:id]
22
+ email_id = params[:email_id]
23
+
24
+ path = "emails/#{email_id}/attachments/#{attachment_id}"
25
+ Resend::Request.new(path, {}, "get").perform
26
+ end
27
+
28
+ # List attachments from a sent email with optional pagination
29
+ #
30
+ # @param params [Hash] Parameters for listing attachments
31
+ # @option params [String] :email_id The email ID (required)
32
+ # @option params [Integer] :limit Maximum number of attachments to return (1-100)
33
+ # @option params [String] :after Cursor for pagination (newer attachments)
34
+ # @option params [String] :before Cursor for pagination (older attachments)
35
+ # @return [Hash] List of attachments with pagination info
36
+ #
37
+ # @example List all attachments
38
+ # Resend::Emails::Attachments.list(
39
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
40
+ # )
41
+ #
42
+ # @example List with custom limit
43
+ # Resend::Emails::Attachments.list(
44
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
45
+ # limit: 50
46
+ # )
47
+ #
48
+ # @example List with pagination
49
+ # Resend::Emails::Attachments.list(
50
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
51
+ # limit: 20,
52
+ # after: "attachment_id_123"
53
+ # )
54
+ def list(params = {})
55
+ email_id = params[:email_id]
56
+ base_path = "emails/#{email_id}/attachments"
57
+
58
+ # Extract pagination parameters
59
+ pagination_params = params.slice(:limit, :after, :before)
60
+
61
+ path = Resend::PaginationHelper.build_paginated_path(base_path, pagination_params)
62
+ Resend::Request.new(path, {}, "get").perform
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resend
4
+ module Emails
5
+ module Receiving
6
+ # Module for received email attachments API operations
7
+ module Attachments
8
+ class << self
9
+ # Retrieve a single attachment from a received email
10
+ #
11
+ # @param params [Hash] Parameters for retrieving the attachment
12
+ # @option params [String] :id The attachment ID (required)
13
+ # @option params [String] :email_id The email ID (required)
14
+ # @return [Hash] The attachment object
15
+ #
16
+ # @example
17
+ # Resend::Emails::Receiving::Attachments.get(
18
+ # id: "2a0c9ce0-3112-4728-976e-47ddcd16a318",
19
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
20
+ # )
21
+ def get(params = {})
22
+ attachment_id = params[:id]
23
+ email_id = params[:email_id]
24
+
25
+ path = "emails/receiving/#{email_id}/attachments/#{attachment_id}"
26
+ Resend::Request.new(path, {}, "get").perform
27
+ end
28
+
29
+ # List attachments from a received email with optional pagination
30
+ #
31
+ # @param params [Hash] Parameters for listing attachments
32
+ # @option params [String] :email_id The email ID (required)
33
+ # @option params [Integer] :limit Maximum number of attachments to return (1-100)
34
+ # @option params [String] :after Cursor for pagination (newer attachments)
35
+ # @option params [String] :before Cursor for pagination (older attachments)
36
+ # @return [Hash] List of attachments with pagination info
37
+ #
38
+ # @example List all attachments
39
+ # Resend::Emails::Receiving::Attachments.list(
40
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
41
+ # )
42
+ #
43
+ # @example List with custom limit
44
+ # Resend::Emails::Receiving::Attachments.list(
45
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
46
+ # limit: 50
47
+ # )
48
+ #
49
+ # @example List with pagination
50
+ # Resend::Emails::Receiving::Attachments.list(
51
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
52
+ # limit: 20,
53
+ # after: "attachment_id_123"
54
+ # )
55
+ def list(params = {})
56
+ email_id = params[:email_id]
57
+ base_path = "emails/receiving/#{email_id}/attachments"
58
+
59
+ # Extract pagination parameters
60
+ pagination_params = params.slice(:limit, :after, :before)
61
+
62
+ path = Resend::PaginationHelper.build_paginated_path(base_path, pagination_params)
63
+ Resend::Request.new(path, {}, "get").perform
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resend
4
+ module Emails
5
+ # Module for receiving emails API operations
6
+ module Receiving
7
+ class << self
8
+ # Retrieve a single received email
9
+ #
10
+ # @param email_id [String] The ID of the received email
11
+ # @param params [Hash] Optional query parameters
12
+ # @option params [String] :html_format Format of the HTML content (e.g., "sanitized", "raw")
13
+ # @return [Hash] The received email object
14
+ #
15
+ # @example
16
+ # Resend::Emails::Receiving.get("4ef9a417-02e9-4d39-ad75-9611e0fcc33c")
17
+ #
18
+ # @example With html_format
19
+ # Resend::Emails::Receiving.get("4ef9a417-02e9-4d39-ad75-9611e0fcc33c", html_format: "sanitized")
20
+ def get(email_id = "", params = {})
21
+ path = "emails/receiving/#{email_id}"
22
+
23
+ path += "?html_format=#{CGI.escape(params[:html_format].to_s)}" if params[:html_format]
24
+
25
+ Resend::Request.new(path, {}, "get").perform
26
+ end
27
+
28
+ # List received emails with optional pagination
29
+ #
30
+ # @param params [Hash] Optional parameters for pagination
31
+ # @option params [Integer] :limit Maximum number of emails to return (1-100)
32
+ # @option params [String] :after Cursor for pagination (newer emails)
33
+ # @option params [String] :before Cursor for pagination (older emails)
34
+ # @return [Hash] List of received emails with pagination info
35
+ #
36
+ # @example List all received emails
37
+ # Resend::Emails::Receiving.list
38
+ #
39
+ # @example List with custom limit
40
+ # Resend::Emails::Receiving.list(limit: 50)
41
+ #
42
+ # @example List with pagination
43
+ # Resend::Emails::Receiving.list(limit: 20, after: "email_id_123")
44
+ def list(params = {})
45
+ path = Resend::PaginationHelper.build_paginated_path("emails/receiving", params)
46
+ Resend::Request.new(path, {}, "get").perform
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/resend/emails.rb CHANGED
@@ -51,12 +51,5 @@ module Resend
51
51
  Resend::Request.new(path, query_params, "get").perform
52
52
  end
53
53
  end
54
-
55
- # This method is kept here for backwards compatibility
56
- # Use Resend::Emails.send instead.
57
- def send_email(params)
58
- warn "[DEPRECATION] `send_email` is deprecated. Please use `Resend::Emails.send` instead."
59
- Resend::Emails.send(params)
60
- end
61
54
  end
62
55
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resend
4
+ # events api wrapper
5
+ module Events
6
+ class << self
7
+ # https://resend.com/docs/api-reference/events/create-event
8
+ def create(params = {})
9
+ Resend::Request.new("events", params, "post").perform
10
+ end
11
+
12
+ # https://resend.com/docs/api-reference/events/get-event
13
+ # identifier can be a UUID or an event name (e.g. "user.signed_up")
14
+ def get(identifier = "")
15
+ path = "events/#{CGI.escape(identifier.to_s)}"
16
+ Resend::Request.new(path, {}, "get").perform
17
+ end
18
+
19
+ # https://resend.com/docs/api-reference/events/update-event
20
+ def update(params = {})
21
+ identifier = params[:identifier]
22
+ path = "events/#{CGI.escape(identifier.to_s)}"
23
+ payload = params.reject { |k, _| k == :identifier }
24
+ Resend::Request.new(path, payload, "patch").perform
25
+ end
26
+
27
+ # https://resend.com/docs/api-reference/events/delete-event
28
+ # identifier can be a UUID or an event name (e.g. "user.signed_up")
29
+ def remove(identifier = "")
30
+ path = "events/#{CGI.escape(identifier.to_s)}"
31
+ Resend::Request.new(path, {}, "delete").perform
32
+ end
33
+
34
+ # https://resend.com/docs/api-reference/events/send-event
35
+ def send(params = {})
36
+ Resend::Request.new("events/send", params, "post").perform
37
+ end
38
+
39
+ # https://resend.com/docs/api-reference/events/list-events
40
+ def list(params = {})
41
+ path = Resend::PaginationHelper.build_paginated_path("events", params)
42
+ Resend::Request.new(path, {}, "get").perform
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resend
4
+ # logs api wrapper
5
+ module Logs
6
+ class << self
7
+ # https://resend.com/docs/api-reference/logs/retrieve-log
8
+ def get(log_id = "")
9
+ path = "logs/#{log_id}"
10
+ Resend::Request.new(path, {}, "get").perform
11
+ end
12
+
13
+ # https://resend.com/docs/api-reference/logs/list-logs
14
+ def list(params = {})
15
+ path = Resend::PaginationHelper.build_paginated_path("logs", params)
16
+ Resend::Request.new(path, {}, "get").perform
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/resend/mailer.rb CHANGED
@@ -14,7 +14,7 @@ module Resend
14
14
  cc bcc
15
15
  from reply-to to subject mime-version
16
16
  html text
17
- content-type tags scheduled_at
17
+ content-type content-transfer-encoding tags scheduled_at
18
18
  headers options
19
19
  ].freeze
20
20
 
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Resend
6
+ # Handles multipart/form-data requests (file uploads).
7
+ # Builds the multipart body directly instead of relying on HTTParty's
8
+ # duck-typing file detection, which requires a :path method.
9
+ class MultipartRequest < Request
10
+ NEWLINE = "\r\n"
11
+ private_constant :NEWLINE
12
+
13
+ private
14
+
15
+ def build_request_options
16
+ boundary = SecureRandom.hex(16)
17
+ headers = @headers.merge("Content-Type" => "multipart/form-data; boundary=#{boundary}")
18
+ { headers: headers, body: build_multipart_body(boundary) }
19
+ end
20
+
21
+ def build_multipart_body(boundary)
22
+ body = "".b
23
+
24
+ body << part(boundary, "file", read_file(@body[:file]), filename: "import.csv", content_type: "text/csv")
25
+ optional_fields.each { |name, value| body << part(boundary, name, value) }
26
+ body << "--#{boundary}--#{NEWLINE}".b
27
+
28
+ body
29
+ end
30
+
31
+ def part(boundary, name, value, filename: nil, content_type: nil)
32
+ disposition = %(Content-Disposition: form-data; name="#{name}")
33
+ disposition += %(; filename="#{filename}") if filename
34
+
35
+ header = "--#{boundary}#{NEWLINE}#{disposition}#{NEWLINE}"
36
+ header += "Content-Type: #{content_type}#{NEWLINE}" if content_type
37
+ header += NEWLINE
38
+
39
+ header.b + value.to_s.b + NEWLINE.b
40
+ end
41
+
42
+ def read_file(file_data)
43
+ if file_data.respond_to?(:read)
44
+ content = file_data.read
45
+ file_data.rewind if file_data.respond_to?(:rewind)
46
+ content.b
47
+ else
48
+ file_data.to_s.b
49
+ end
50
+ end
51
+
52
+ def optional_fields
53
+ {
54
+ column_map: @body[:column_map] && serialize_json(@body[:column_map]),
55
+ on_conflict: @body[:on_conflict],
56
+ segments: @body[:segments] && serialize_json(@body[:segments]),
57
+ topics: @body[:topics] && serialize_json(@body[:topics])
58
+ }.compact
59
+ end
60
+
61
+ def serialize_json(value)
62
+ return value if value.is_a?(String)
63
+
64
+ value.to_json
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resend
4
+ # oauth grants api wrapper
5
+ module OAuthGrants
6
+ class << self
7
+ # https://resend.com/docs/api-reference/oauth/list-grants
8
+ def list(params = {})
9
+ path = Resend::PaginationHelper.build_paginated_path("oauth/grants", params)
10
+ Resend::Request.new(path, {}, "get").perform
11
+ end
12
+
13
+ # https://resend.com/docs/api-reference/oauth/revoke-grant
14
+ def revoke(oauth_grant_id = "")
15
+ path = "oauth/grants/#{oauth_grant_id}"
16
+ Resend::Request.new(path, {}, "delete").perform
17
+ end
18
+ end
19
+ end
20
+ end
@@ -34,13 +34,16 @@ module Resend
34
34
  resp = HTTParty.send(@verb.to_sym, "#{BASE_URL}#{@path}", options)
35
35
 
36
36
  check_json!(resp)
37
- process_response(resp)
37
+ data = process_response(resp)
38
+ headers = extract_headers(resp)
39
+
40
+ Resend::Response.new(data, headers)
38
41
  end
39
42
 
40
- def handle_error!(resp)
41
- code = resp[:statusCode]
42
- body = resp[:message]
43
- headers = resp.respond_to?(:headers) ? resp.headers : (resp[:headers] || {})
43
+ def handle_error!(data, resp = nil)
44
+ code = data[:statusCode]
45
+ body = data[:message]
46
+ headers = resp.respond_to?(:headers) ? resp.headers : (data[:headers] || {})
44
47
 
45
48
  # get error from the known list of errors
46
49
  error_class = Resend::Error::ERRORS[code] || Resend::Error
@@ -66,20 +69,23 @@ module Resend
66
69
  end
67
70
 
68
71
  def process_response(resp)
69
- resp.transform_keys!(&:to_sym) unless resp.body.empty?
70
- handle_error!(resp) if error_response?(resp)
71
- resp
72
+ # Extract the parsed data from HTTParty response or use the hash directly (for tests/mocks)
73
+ data = resp.respond_to?(:parsed_response) ? resp.parsed_response : resp
74
+ data ||= {}
75
+ data.transform_keys!(&:to_sym) unless resp.body.empty?
76
+ handle_error!(data, resp) if error_response?(data)
77
+ data
72
78
  end
73
79
 
74
80
  def error_response?(resp)
75
- resp[:statusCode] && (resp[:statusCode] != 200 && resp[:statusCode] != 201)
81
+ resp[:statusCode] && resp[:statusCode] != 200 && resp[:statusCode] != 201
76
82
  end
77
83
 
78
84
  def set_idempotency_key
79
85
  # Only set idempotency key if the verb is POST for now.
80
86
  #
81
87
  # Does not set it if the idempotency_key is nil or empty
82
- if @verb.downcase == "post" && (!@options[:idempotency_key].nil? && !@options[:idempotency_key].empty?)
88
+ if @verb.downcase == "post" && !@options[:idempotency_key].nil? && !@options[:idempotency_key].empty?
83
89
  @headers["Idempotency-Key"] = @options[:idempotency_key]
84
90
  end
85
91
  end
@@ -101,5 +107,12 @@ module Resend
101
107
  rescue JSON::ParserError, TypeError
102
108
  raise Resend::Error::InternalServerError.new("Resend API returned an unexpected response", nil)
103
109
  end
110
+
111
+ # Extract and normalize headers from the HTTParty response
112
+ def extract_headers(resp)
113
+ return {} unless resp.respond_to?(:headers)
114
+
115
+ resp.headers.to_h.transform_keys { |k| k.to_s.downcase }
116
+ end
104
117
  end
105
118
  end