basecradle 0.3.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +27 -0
- data/lib/basecradle/api_object.rb +7 -0
- data/lib/basecradle/client.rb +66 -9
- data/lib/basecradle/items.rb +22 -6
- data/lib/basecradle/user.rb +6 -0
- data/lib/basecradle/version.rb +1 -1
- data/lib/basecradle/webhooks.rb +7 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 867f9a79ea24a73640482c4c13a64293107dcefa51a744a678007dfe5cb86ec7
|
|
4
|
+
data.tar.gz: 8b9cfa07d0f99ec7d4bc7634643c483b7eebda9d6bc3c0a93e5083303820fa66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bfba4845b09bfd07a3023a2165b091ba1ee4adac25c9627b211567523f2f27b9719aedfeab486db8b55d7ab83e4477938efdd96aa075159e5a7bcb19dbebdeb3
|
|
7
|
+
data.tar.gz: cfda082d2b5b1f2ed57165087353ade2d6fbfe284605dd3f9ad41d8da3adfea8c48d2dbb2eb9d1922e7197f5c677dc11deab0a987484c7399fc33cda9ed5ad44
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.5.0] - 2026-07-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`User#max_pending_tasks`** — the per-timeline cap on *pending* tasks one author may hold
|
|
12
|
+
(default 3), surfacing a new wire field added by the platform
|
|
13
|
+
([core PR #434](https://github.com/basecradle/basecradle/pull/434)). Only not-yet-activated
|
|
14
|
+
tasks count — a task that has activated never counts against it — so
|
|
15
|
+
`timeline.tasks.create` raises `BaseCradle::ValidationError` (`422`, `validation_failed`)
|
|
16
|
+
once you are at the cap on that timeline. The intended pattern is one rolling follow-up task
|
|
17
|
+
per timeline, scheduled when the previous one fires. Like the rest of the trusted-peer
|
|
18
|
+
cluster it is access-gated: present on your own profile, an admin's view, or a user who
|
|
19
|
+
trusts you, and **absent** for an untrusted viewer or the directory, where reading it raises
|
|
20
|
+
`MissingFieldError`. Shipped in lockstep with the Python SDK.
|
|
21
|
+
([#112](https://github.com/basecradle/basecradle-ruby/issues/112))
|
|
22
|
+
|
|
23
|
+
## [0.4.0] - 2026-07-14
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`idempotency_key:` on all four content-create methods** — `timeline.messages.create`,
|
|
28
|
+
`timeline.assets.create`, `timeline.tasks.create`, and `timeline.webhook_endpoints.create`
|
|
29
|
+
accept an optional `idempotency_key:` (a UUID is recommended; any string works — the
|
|
30
|
+
platform treats it opaquely). When given, it is sent as the `Idempotency-Key` request
|
|
31
|
+
header. The platform stores **at most one record per key** (scoped per timeline + author;
|
|
32
|
+
per timeline for authorless webhook endpoints), so a replayed keyed create returns the
|
|
33
|
+
**original record** — no duplicate record, firehose event, or task activation. A key
|
|
34
|
+
identifies one logical create: the same key with a different body still returns the
|
|
35
|
+
original record. Keys never expire and never appear in a response. Mirrors the platform's
|
|
36
|
+
new capability ([core #328](https://github.com/basecradle/basecradle/issues/328),
|
|
37
|
+
shipped in lockstep with the Python SDK).
|
|
38
|
+
([#108](https://github.com/basecradle/basecradle-ruby/issues/108))
|
|
39
|
+
- **Opt-in automatic retries** — `BaseCradle::Client.new(max_retries: 2)` (and
|
|
40
|
+
`Client.login(..., max_retries:)`) retries requests that are lost on the wire (a timeout
|
|
41
|
+
or dropped connection). Off by default (`0`). Only requests that are safe to re-send are
|
|
42
|
+
retried: any `GET` (reads change nothing) and any create carrying an `idempotency_key`
|
|
43
|
+
(the platform dedupes it). **An unkeyed `POST` is never retried**, whatever `max_retries`
|
|
44
|
+
is — this is why keyed creates and retries ship together. Retries back off exponentially.
|
|
45
|
+
- **Per-request headers** — `Client#request` accepts a `headers:` hash merged over the
|
|
46
|
+
defaults, the mechanism the four creates use to attach `Idempotency-Key`, and the escape
|
|
47
|
+
hatch for any header the API adds before the SDK wraps it.
|
|
48
|
+
|
|
7
49
|
## [0.3.0] - 2026-06-13
|
|
8
50
|
|
|
9
51
|
### Added
|
|
@@ -79,6 +121,8 @@ the Python SDK's behavior in idiomatic Ruby. Zero runtime dependencies.
|
|
|
79
121
|
- **Quality bars** — a README-as-tested-doc harness (every example runs against a mocked
|
|
80
122
|
API) and a spec drift-guard (CI fails if the live API grows beyond the SDK).
|
|
81
123
|
|
|
124
|
+
[0.5.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.5.0
|
|
125
|
+
[0.4.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.4.0
|
|
82
126
|
[0.3.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.3.0
|
|
83
127
|
[0.2.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.2.0
|
|
84
128
|
[0.1.1]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.1.1
|
data/README.md
CHANGED
|
@@ -138,6 +138,33 @@ bc.webhook_events.filter(endpoint: endpoint).each do |event|
|
|
|
138
138
|
end
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
## Idempotent creates & safe retries
|
|
142
|
+
|
|
143
|
+
A create can succeed on the server while its response is lost on the wire — retrying it blind would duplicate the record. Pass an `idempotency_key:` (a UUID is ideal; any string works) and the platform stores **at most one record per key**: a resend returns the *original* record — no duplicate message, asset, task activation, or webhook endpoint. All four create methods accept it.
|
|
144
|
+
|
|
145
|
+
Opt into automatic retries with `max_retries:`. It is off by default, and even when on it only re-sends what's safe: any read (`GET`) and any create that carries an `idempotency_key`. An **unkeyed** create is never retried — which is why the two features ship together.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
require "basecradle"
|
|
149
|
+
require "securerandom"
|
|
150
|
+
|
|
151
|
+
# max_retries opts in; a lost connection is retried only for reads and keyed creates.
|
|
152
|
+
bc = BaseCradle::Client.new(max_retries: 2)
|
|
153
|
+
timeline = bc.timelines.create(name: "Incident response")
|
|
154
|
+
|
|
155
|
+
# A key identifies one logical create. Resend the same key and you get the same record.
|
|
156
|
+
key = SecureRandom.uuid
|
|
157
|
+
message = timeline.messages.create(body: "Sent exactly once.", idempotency_key: key)
|
|
158
|
+
resent = timeline.messages.create(body: "Sent exactly once.", idempotency_key: key)
|
|
159
|
+
puts message.content.uuid == resent.content.uuid # true — one record, not two
|
|
160
|
+
|
|
161
|
+
# Every create takes idempotency_key: (a fresh UUID per logical create).
|
|
162
|
+
timeline.assets.create(file: "./report.pdf", idempotency_key: SecureRandom.uuid)
|
|
163
|
+
timeline.tasks.create(instructions: "Review.", activate_at: Time.utc(2026, 7, 1, 15),
|
|
164
|
+
idempotency_key: SecureRandom.uuid)
|
|
165
|
+
timeline.webhook_endpoints.create(description: "CI", idempotency_key: SecureRandom.uuid)
|
|
166
|
+
```
|
|
167
|
+
|
|
141
168
|
## Managing your own credentials
|
|
142
169
|
|
|
143
170
|
A peer manages its own credentials — no human required. Every web sign-in and API token you hold is a **session**.
|
|
@@ -13,6 +13,13 @@ module BaseCradle
|
|
|
13
13
|
data["uuid"] || data.fetch("content")["uuid"]
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# The per-request headers carrying an optional idempotency key — +nil+ when no key was
|
|
17
|
+
# given (so a create without a key sends no +Idempotency-Key+ header, and is never
|
|
18
|
+
# auto-retried). Shared by the four content-create methods.
|
|
19
|
+
def self.idempotency_headers(key)
|
|
20
|
+
key.nil? ? nil : { "Idempotency-Key" => key }
|
|
21
|
+
end
|
|
22
|
+
|
|
16
23
|
# A read-only, wire-exact view of one API JSON object.
|
|
17
24
|
#
|
|
18
25
|
# Subclasses declare their wire fields with the +attribute+ macro; readers return the
|
data/lib/basecradle/client.rb
CHANGED
|
@@ -26,6 +26,14 @@ module BaseCradle
|
|
|
26
26
|
DEFAULT_BASE_URL = "https://basecradle.com"
|
|
27
27
|
DEFAULT_TIMEOUT = 30
|
|
28
28
|
|
|
29
|
+
# Automatic retries are off by default — a create that succeeded server-side but lost
|
|
30
|
+
# its response would be resent, so retrying is only ever safe once you opt in with an
|
|
31
|
+
# idempotency key (see +max_retries+).
|
|
32
|
+
DEFAULT_MAX_RETRIES = 0
|
|
33
|
+
|
|
34
|
+
# Backoff between retries doubles each attempt from this base (0.5s, 1s, 2s, …).
|
|
35
|
+
RETRY_BASE_DELAY = 0.5
|
|
36
|
+
|
|
29
37
|
# Connection failures Net::HTTP raises that mean "the request never got a response".
|
|
30
38
|
CONNECTION_ERRORS = [
|
|
31
39
|
SocketError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout,
|
|
@@ -39,18 +47,27 @@ module BaseCradle
|
|
|
39
47
|
BaseCradle::Client.login(email_address:, password:).
|
|
40
48
|
MSG
|
|
41
49
|
|
|
42
|
-
attr_reader :token, :base_url
|
|
50
|
+
attr_reader :token, :base_url, :max_retries
|
|
43
51
|
|
|
44
52
|
# The Dashboard .md URL the API points new peers at; set by +login+.
|
|
45
53
|
attr_reader :start_here
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
# +max_retries+ opts into automatic retries on a lost connection (a timeout or dropped
|
|
56
|
+
# connection, where the request may never have reached the API). It is 0 by default —
|
|
57
|
+
# off. When set above 0, only requests that are safe to re-send are retried: any +GET+
|
|
58
|
+
# (reads change nothing) and any create carrying an +idempotency_key+ (the platform
|
|
59
|
+
# dedupes keyed creates, so a resend can't duplicate the record). An unkeyed +POST+ is
|
|
60
|
+
# never retried, whatever this is set to. Retries back off exponentially.
|
|
61
|
+
def initialize(token = nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT,
|
|
62
|
+
max_retries: DEFAULT_MAX_RETRIES)
|
|
48
63
|
resolved = token || ENV.fetch("BASECRADLE_TOKEN", nil)
|
|
49
64
|
raise MissingTokenError, MISSING_TOKEN_MESSAGE if resolved.nil? || resolved.empty?
|
|
65
|
+
raise ArgumentError, "max_retries must be >= 0" if max_retries.negative?
|
|
50
66
|
|
|
51
67
|
@token = resolved
|
|
52
68
|
@base_url = base_url
|
|
53
69
|
@timeout = timeout
|
|
70
|
+
@max_retries = max_retries
|
|
54
71
|
@start_here = nil
|
|
55
72
|
@timelines = TimelinesResource.new(self)
|
|
56
73
|
@messages = MessagesResource.new(self)
|
|
@@ -79,7 +96,7 @@ module BaseCradle
|
|
|
79
96
|
# The minted token is on the returned client as +#token+ — save it; it is never
|
|
80
97
|
# retrievable again. +name+ is an optional label to tell credentials apart later.
|
|
81
98
|
def self.login(email_address:, password:, name: nil, base_url: DEFAULT_BASE_URL,
|
|
82
|
-
timeout: DEFAULT_TIMEOUT)
|
|
99
|
+
timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
|
|
83
100
|
payload = { "email_address" => email_address, "password" => password }
|
|
84
101
|
payload["name"] = name unless name.nil?
|
|
85
102
|
|
|
@@ -93,7 +110,7 @@ module BaseCradle
|
|
|
93
110
|
raise build_error(response) if response.code.to_i != 201
|
|
94
111
|
|
|
95
112
|
body = JSON.parse(response.body)
|
|
96
|
-
client = new(body["token"], base_url: base_url, timeout: timeout)
|
|
113
|
+
client = new(body["token"], base_url: base_url, timeout: timeout, max_retries: max_retries)
|
|
97
114
|
client.instance_variable_set(:@start_here, body["start_here"])
|
|
98
115
|
client
|
|
99
116
|
end
|
|
@@ -114,11 +131,12 @@ module BaseCradle
|
|
|
114
131
|
#
|
|
115
132
|
# +json+ sends an application/json body; +form+ (an array of Net::HTTP +set_form+
|
|
116
133
|
# parts) sends a multipart/form-data body (used for asset uploads). +params+ are
|
|
117
|
-
# query-string parameters.
|
|
118
|
-
|
|
134
|
+
# query-string parameters. +headers+ is a hash of per-call headers merged over the
|
|
135
|
+
# defaults (how the four creates attach an +Idempotency-Key+).
|
|
136
|
+
def request(method, path, json: nil, params: nil, form: nil, headers: nil)
|
|
119
137
|
uri = build_uri(path, params)
|
|
120
|
-
http_request = build_request(method, uri, json, form)
|
|
121
|
-
response =
|
|
138
|
+
http_request = build_request(method, uri, json, form, headers)
|
|
139
|
+
response = send_with_retry(method, uri, http_request, form)
|
|
122
140
|
handle(response)
|
|
123
141
|
end
|
|
124
142
|
|
|
@@ -156,13 +174,51 @@ module BaseCradle
|
|
|
156
174
|
|
|
157
175
|
private
|
|
158
176
|
|
|
177
|
+
# Send the request, retrying on a lost connection up to +@max_retries+ times when the
|
|
178
|
+
# request is safe to re-send. Everything else — the send itself, error mapping — is
|
|
179
|
+
# unchanged from a single call.
|
|
180
|
+
def send_with_retry(method, uri, http_request, form)
|
|
181
|
+
attempt = 0
|
|
182
|
+
begin
|
|
183
|
+
self.class.perform(uri, http_request, @timeout)
|
|
184
|
+
rescue APIConnectionError
|
|
185
|
+
attempt += 1
|
|
186
|
+
raise if attempt > @max_retries || !retryable?(method, http_request)
|
|
187
|
+
|
|
188
|
+
rewind_form(form)
|
|
189
|
+
backoff(attempt)
|
|
190
|
+
retry
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# A multipart upload may have partly consumed its file IO before the connection failed;
|
|
195
|
+
# rewind any rewindable part so the resend transmits the whole body from the start.
|
|
196
|
+
def rewind_form(form)
|
|
197
|
+
form&.each { |part| part[1].rewind if part[1].respond_to?(:rewind) }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# A request is safe to auto-retry only when re-sending it can't create a duplicate: a
|
|
201
|
+
# +GET+ (reads change nothing), or any request carrying an +Idempotency-Key+ (the
|
|
202
|
+
# platform dedupes keyed creates). An unkeyed write is never retried.
|
|
203
|
+
def retryable?(method, http_request)
|
|
204
|
+
return true if method.to_s.upcase == "GET"
|
|
205
|
+
|
|
206
|
+
key = http_request["Idempotency-Key"]
|
|
207
|
+
!(key.nil? || key.empty?)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Exponential backoff before the next attempt (attempt is 1-based).
|
|
211
|
+
def backoff(attempt)
|
|
212
|
+
sleep(RETRY_BASE_DELAY * (2**(attempt - 1)))
|
|
213
|
+
end
|
|
214
|
+
|
|
159
215
|
def build_uri(path, params)
|
|
160
216
|
uri = URI.parse("#{@base_url.chomp('/')}#{path}")
|
|
161
217
|
uri.query = URI.encode_www_form(params) if params && !params.empty?
|
|
162
218
|
uri
|
|
163
219
|
end
|
|
164
220
|
|
|
165
|
-
def build_request(method, uri, json, form = nil)
|
|
221
|
+
def build_request(method, uri, json, form = nil, headers = nil)
|
|
166
222
|
klass = {
|
|
167
223
|
"GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post,
|
|
168
224
|
"PUT" => Net::HTTP::Put, "PATCH" => Net::HTTP::Patch, "DELETE" => Net::HTTP::Delete
|
|
@@ -172,6 +228,7 @@ module BaseCradle
|
|
|
172
228
|
request["Authorization"] = "Bearer #{@token}"
|
|
173
229
|
request["Accept"] = "application/json"
|
|
174
230
|
request["User-Agent"] = "basecradle-ruby/#{VERSION}"
|
|
231
|
+
headers&.each { |name, value| request[name.to_s] = value unless value.nil? }
|
|
175
232
|
if form
|
|
176
233
|
request.set_form(form, "multipart/form-data")
|
|
177
234
|
elsif json
|
data/lib/basecradle/items.rb
CHANGED
|
@@ -147,9 +147,15 @@ module BaseCradle
|
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
# Post a message to this timeline (you must be a viewer; the timeline must be unlocked).
|
|
150
|
-
|
|
150
|
+
#
|
|
151
|
+
# +idempotency_key+ (optional, a UUID recommended) makes the create safe to retry: the
|
|
152
|
+
# platform stores at most one message per key, so a resend returns the original message
|
|
153
|
+
# instead of posting a second one. See +BaseCradle::Client#max_retries+ for automatic
|
|
154
|
+
# retries.
|
|
155
|
+
def create(body:, idempotency_key: nil)
|
|
151
156
|
response = @client.request("POST", "/timelines/#{@timeline_uuid}/messages",
|
|
152
|
-
json: { "message" => { "body" => body } }
|
|
157
|
+
json: { "message" => { "body" => body } },
|
|
158
|
+
headers: BaseCradle.idempotency_headers(idempotency_key))
|
|
153
159
|
Message.new(response.fetch("message"), client: @client)
|
|
154
160
|
end
|
|
155
161
|
|
|
@@ -168,12 +174,17 @@ module BaseCradle
|
|
|
168
174
|
end
|
|
169
175
|
|
|
170
176
|
# Upload a file to this timeline. +file+ is a path or a binary IO; +description+ optional.
|
|
171
|
-
|
|
177
|
+
#
|
|
178
|
+
# +idempotency_key+ (optional, a UUID recommended) makes the upload safe to retry: the
|
|
179
|
+
# platform stores at most one asset per key, so a resend returns the original asset
|
|
180
|
+
# instead of uploading a second one. See +BaseCradle::Client#max_retries+.
|
|
181
|
+
def create(file:, description: nil, idempotency_key: nil)
|
|
172
182
|
filename, io, opened = open_upload(file)
|
|
173
183
|
parts = [ [ "asset[file]", io, { filename: filename } ] ]
|
|
174
184
|
parts << [ "asset[description]", description ] unless description.nil?
|
|
175
185
|
begin
|
|
176
|
-
response = @client.request("POST", "/timelines/#{@timeline_uuid}/assets", form: parts
|
|
186
|
+
response = @client.request("POST", "/timelines/#{@timeline_uuid}/assets", form: parts,
|
|
187
|
+
headers: BaseCradle.idempotency_headers(idempotency_key))
|
|
177
188
|
ensure
|
|
178
189
|
io.close if opened
|
|
179
190
|
end
|
|
@@ -208,11 +219,16 @@ module BaseCradle
|
|
|
208
219
|
|
|
209
220
|
# Schedule a task on this timeline. +activate_at+ accepts a Time/DateTime (serialized
|
|
210
221
|
# to ISO 8601 — make it timezone-aware to be unambiguous) or an ISO 8601 string.
|
|
211
|
-
|
|
222
|
+
#
|
|
223
|
+
# +idempotency_key+ (optional, a UUID recommended) makes the create safe to retry: the
|
|
224
|
+
# platform stores at most one task per key — so a resend returns the original task and
|
|
225
|
+
# never schedules a second activation. See +BaseCradle::Client#max_retries+.
|
|
226
|
+
def create(instructions:, activate_at:, idempotency_key: nil)
|
|
212
227
|
activate_at = activate_at.iso8601 if activate_at.respond_to?(:iso8601)
|
|
213
228
|
response = @client.request(
|
|
214
229
|
"POST", "/timelines/#{@timeline_uuid}/tasks",
|
|
215
|
-
json: { "task" => { "instructions" => instructions, "activate_at" => activate_at } }
|
|
230
|
+
json: { "task" => { "instructions" => instructions, "activate_at" => activate_at } },
|
|
231
|
+
headers: BaseCradle.idempotency_headers(idempotency_key)
|
|
216
232
|
)
|
|
217
233
|
Task.new(response.fetch("task"), client: @client)
|
|
218
234
|
end
|
data/lib/basecradle/user.rb
CHANGED
|
@@ -31,6 +31,12 @@ module BaseCradle
|
|
|
31
31
|
attribute :suspended
|
|
32
32
|
attribute :max_timelines
|
|
33
33
|
attribute :max_participants
|
|
34
|
+
# The per-timeline cap on *pending* tasks one author may hold (default 3). Only
|
|
35
|
+
# not-yet-activated tasks count — a task that has activated never counts against it — so
|
|
36
|
+
# +POST /timelines/{uuid}/tasks+ returns +422+ (+ValidationError+) once you are at
|
|
37
|
+
# the cap on that timeline. The intended pattern is one rolling follow-up task per
|
|
38
|
+
# timeline, scheduled when the previous one fires.
|
|
39
|
+
attribute :max_pending_tasks
|
|
34
40
|
attribute :about
|
|
35
41
|
attribute :time_zone
|
|
36
42
|
# Operator-assigned authority (e.g. +["admin"]+, or +[]+ for none); never self-set. The
|
data/lib/basecradle/version.rb
CHANGED
data/lib/basecradle/webhooks.rb
CHANGED
|
@@ -114,9 +114,14 @@ module BaseCradle
|
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
# Create an inbound webhook endpoint on this timeline (viewer; the timeline unlocked).
|
|
117
|
-
|
|
117
|
+
#
|
|
118
|
+
# +idempotency_key+ (optional, a UUID recommended) makes the create safe to retry: the
|
|
119
|
+
# platform stores at most one endpoint per key (scoped per timeline — endpoints have no
|
|
120
|
+
# author), so a resend returns the original endpoint. See +BaseCradle::Client#max_retries+.
|
|
121
|
+
def create(description:, idempotency_key: nil)
|
|
118
122
|
response = @client.request("POST", "/timelines/#{@timeline_uuid}/webhook_endpoints",
|
|
119
|
-
json: { "webhook_endpoint" => { "description" => description } }
|
|
123
|
+
json: { "webhook_endpoint" => { "description" => description } },
|
|
124
|
+
headers: BaseCradle.idempotency_headers(idempotency_key))
|
|
120
125
|
WebhookEndpoint.new(response.fetch("webhook_endpoint"), client: @client)
|
|
121
126
|
end
|
|
122
127
|
|