up_api/v1/
webhooks.rs

1use crate::v1::{Client, error, BASE_URL, standard};
2
3use serde::{Deserialize, Serialize};
4
5// ----------------- Response Objects -----------------
6
7#[derive(Deserialize, Debug)]
8pub struct ListWebhooksResponse {
9    /// The list of webhooks returned in this response.
10    pub data : Vec<WebhookResource>,
11    pub links : ResponseLinks,
12}
13
14#[derive(Deserialize, Debug)]
15pub struct GetWebhookResponse {
16    /// The webhook returned in the response.
17    pub data : WebhookResource,
18}
19
20#[derive(Deserialize, Debug)]
21pub struct CreateWebhookResponse {
22    /// The webhook that was created.
23    pub data : WebhookResource,
24}
25
26#[derive(Deserialize, Debug)]
27pub struct WebhookResource {
28    /// The type of this resource: `webhooks`
29    pub r#type : String,
30    /// The unique identifier for this webhook.
31    pub id : String,
32    pub attributes : Attributes,
33    pub relationships : Relationships,
34    pub links : WebhookResourceLinks,
35}
36
37#[derive(Deserialize, Debug)]
38#[serde(rename_all = "camelCase")]
39pub struct Attributes {
40    /// The URL that this webhook is configured to `POST` events to.
41    pub url : String,
42    /// An optional description that was provided at the time the webhook was created.
43    pub description : Option<String>,
44    /// A shared secret key used to sign all webhook events sent to the configured webhook URL. This field is returned only once, upon the initial creation of the webhook. If lost, create a new webhook and delete this webhook.
45    /// The webhook URL receives a request with a `X-Up-Authenticity-Signature` header, which is the SHA-256 HMAC of the entire raw request body signed using this `secretKey`. It is advised to compute and check this signature to verify the authenticity of requests sent to the webhook URL. See Handling webhook events for full details.
46    pub secret_key : Option<String>,
47    /// The date-time at which this webhook was created.
48    pub created_at : String,
49}
50
51#[derive(Deserialize, Debug)]
52pub struct Relationships {
53    pub logs : Logs,
54}
55
56#[derive(Deserialize, Debug)]
57pub struct Logs {
58    pub links : Option<LogsLinks>,
59}
60
61#[derive(Deserialize, Debug)]
62pub struct LogsLinks {
63    /// The link to retrieve the related resource(s) in this relationship.
64    pub related : String,
65}
66
67#[derive(Deserialize, Debug)]
68pub struct WebhookResourceLinks {
69    /// The canonical link to this resource within the API.
70    #[serde(rename = "self")]
71    pub this : String,
72}
73
74#[derive(Deserialize, Debug)]
75pub struct ResponseLinks {
76    /// The link to the previous page in the results. If this value is `None` there is no previous page.
77    pub prev : Option<String>,
78    /// The link to the next page in the results. If this value is `None` there is no next page.
79    pub next : Option<String>,
80}
81
82#[derive(Deserialize, Debug)]
83pub struct PingWebhookResponse {
84    /// The webhook event data sent to the subscribed webhook.
85    pub data : WebhookEventResource,
86}
87
88#[derive(Deserialize, Debug)]
89pub struct WebhookEventResource {
90    /// The type of this resource: `webhook-events`
91    pub r#type : String,
92    /// The unique identifier for this event. This will remain constant across delivery retries.
93    pub id : String,
94    pub attributes : EventAttributes,
95    pub relationships : EventRelationships,
96}
97
98#[derive(Deserialize, Debug)]
99pub struct EventRelationships {
100    pub webhook : Webhook,
101    pub transaction : Option<Transaction>,
102}
103
104#[derive(Deserialize, Debug)]
105pub struct Transaction {
106    pub data : TransactionData,
107    pub links : Option<TransactionLinks>,
108}
109
110#[derive(Deserialize, Debug)]
111pub struct Webhook {
112    pub data : WebhookData,
113    pub links : Option<WebhookLinks>,
114}
115
116#[derive(Deserialize, Debug)]
117pub struct WebhookData {
118    /// The type of this resource: `webhooks`
119    pub r#type : String,
120    /// The unique identifier of the resource within its type.
121    pub id : String,
122}
123
124#[derive(Deserialize, Debug)]
125pub struct WebhookLinks {
126    /// The link to retrieve the related resource(s) in this relationship.
127    pub related : String,
128}
129
130#[derive(Deserialize, Debug)]
131pub struct TransactionData {
132    /// The type of this resource: `transactions`
133    pub r#type : String,
134    /// The unique identifier of the resource within its type.
135    pub id : String,
136}
137
138#[derive(Deserialize, Debug)]
139pub struct TransactionLinks {
140    /// The link to retrieve the related resource(s) in this relationship.
141    pub related : String,
142}
143
144#[derive(Deserialize, Debug)]
145#[serde(rename_all = "camelCase")]
146pub struct EventAttributes {
147    /// The type of this event. This can be used to determine what action to take in response to the event.
148    pub event_type : standard::WebhookEventTypeEnum,
149    /// The date-time at which this event was generated.
150    pub created_at : String,
151}
152
153
154#[derive(Deserialize, Debug)]
155pub struct ListWebhookLogsResponse {
156    /// The list of delivery logs returned in this response.
157    pub data : Vec<WebhookDeliveryLogResource>,
158    pub links : LogsResponseLinks,
159}
160
161#[derive(Deserialize, Debug)]
162pub struct WebhookDeliveryLogResource {
163    /// The type of this resource: `webhook-delivery-logs`
164    pub r#type : String,
165    /// The unique identifier for this log entry.
166    pub id : String,
167    pub attributes : DeliveryLogAttributes,
168    pub relationships : DeliveryLogRelationships,
169}
170
171#[derive(Deserialize, Debug)]
172#[serde(rename_all = "camelCase")]
173pub struct DeliveryLogRelationships {
174    pub webhook_event : WebhookEvent,
175}
176
177#[derive(Deserialize, Debug)]
178pub struct WebhookEvent {
179    pub data : WebhookEventData
180}
181
182#[derive(Deserialize, Debug)]
183pub struct WebhookEventData {
184    /// The type of this resource: `webhook-events`
185    pub r#type : String,
186    /// The unique identifier of the resource within its type.
187    pub id : String,
188}
189
190#[derive(Deserialize, Debug)]
191#[serde(rename_all = "camelCase")]
192pub struct DeliveryLogAttributes {
193    /// Information about the request that was sent to the webhook URL.
194    pub request : Request,
195    /// Information about the response that was received from the webhook URL.
196    pub response : Option<Response>,
197    /// The success or failure status of this delivery attempt.
198    pub delivery_status : standard::WebhookDeliveryStatusEnum,
199    /// The date-time at which this log entry was created.
200    pub created_at : String,
201}
202
203#[derive(Deserialize, Debug)]
204pub struct Request {
205    /// The payload that was sent in the request body.
206    pub body : String,
207}
208
209#[derive(Deserialize, Debug)]
210#[serde(rename_all = "camelCase")]
211pub struct Response {
212    /// The HTTP status code received in the response.
213    pub status_code : i64,
214    /// The payload that was received in the response body.
215    pub body : String,
216}
217
218#[derive(Deserialize, Debug)]
219pub struct LogsResponseLinks {
220    /// The link to the previous page in the results. If this value is `None` there is no previous page.
221    pub prev : Option<String>,
222    /// The link to the next page in the results. If this value is `None` there is no next page.
223    pub next : Option<String>,
224}
225
226
227// ----------------- Input Objects -----------------
228
229#[derive(Default)]
230pub struct ListWebhooksOptions {
231    /// The number of records to return in each page. 
232    page_size : Option<u8>,
233}
234
235impl ListWebhooksOptions {
236    /// Sets the page size.
237    pub fn page_size(&mut self, value : u8) {
238        self.page_size = Some(value);
239    }
240
241    fn add_params(&self, url : &mut reqwest::Url) {
242        let mut query = String::new();
243
244        if let Some(value) = &self.page_size {
245            if !query.is_empty() {
246                query.push('&');
247            }
248            query.push_str(&format!("page[size]={}", value));
249        }
250
251        if !query.is_empty() {
252            url.set_query(Some(&query));
253        }
254    }
255}
256
257#[derive(Default)]
258pub struct ListWebhookLogsOptions {
259    /// The number of records to return in each page. 
260    page_size : Option<u8>,
261}
262
263impl ListWebhookLogsOptions {
264    /// Sets the page size.
265    pub fn page_size(&mut self, value : u8) {
266        self.page_size = Some(value);
267    }
268
269    fn add_params(&self, url : &mut reqwest::Url) {
270        let mut query = String::new();
271
272        if let Some(value) = &self.page_size {
273            if !query.is_empty() {
274                query.push('&');
275            }
276            query.push_str(&format!("page[size]={}", value));
277        }
278
279        if !query.is_empty() {
280            url.set_query(Some(&query));
281        }
282    }
283}
284
285// ----------------- Request Objects -----------------
286
287#[derive(Serialize)]
288pub struct CreateWebhookRequest {
289    /// The webhook resource to create.
290    pub data : WebhookInputResource,
291}
292
293#[derive(Serialize)]
294pub struct WebhookInputResource {
295    pub attributes : InputAttributes,
296}
297
298#[derive(Serialize)]
299pub struct InputAttributes {
300    /// The URL that this webhook should post events to. This must be a valid HTTP or HTTPS URL that does not exceed 300 characters in length.
301    pub url : String,
302    /// An optional description for this webhook, up to 64 characters in length.
303    pub description : Option<String>,
304}
305
306impl Client {
307    ///  Retrieve a list of configured webhooks. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. Results are ordered oldest first to newest last.
308    pub async fn list_webhooks(&self, options : &ListWebhooksOptions) -> Result<ListWebhooksResponse, error::Error> {
309        let mut url = reqwest::Url::parse(&format!("{}/webhooks", BASE_URL)).map_err(error::Error::UrlParse)?;
310        options.add_params(&mut url);
311
312        let res = reqwest::Client::new()
313            .get(url)
314            .header("Authorization", self.auth_header())
315            .send()
316            .await
317            .map_err(error::Error::Request)?;
318
319        match res.status() {
320            reqwest::StatusCode::OK => {
321                let body = res.text().await.map_err(error::Error::BodyRead)?;
322                let webhook_response : ListWebhooksResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
323
324                Ok(webhook_response)
325            },
326            _ => {
327                let body = res.text().await.map_err(error::Error::BodyRead)?;
328                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
329
330                Err(error::Error::Api(error))
331            }
332        }
333    }
334
335    /// Retrieve a specific webhook by providing its unique identifier.
336    pub async fn get_webhook(&self, id : &str) -> Result<GetWebhookResponse, error::Error> {
337        // This assertion is because without an ID the request is thought to be a request for
338        // many webhooks, and therefore the error messages are very unclear.
339        if id.is_empty() {
340            panic!("The provided webhook ID must not be empty.");
341        }
342
343        let url = reqwest::Url::parse(&format!("{}/webhooks/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?;
344
345        let res = reqwest::Client::new()
346            .get(url)
347            .header("Authorization", self.auth_header())
348            .send()
349            .await
350            .map_err(error::Error::Request)?;
351
352        match res.status() {
353            reqwest::StatusCode::OK => {
354                let body = res.text().await.map_err(error::Error::BodyRead)?;
355                let webhook_response : GetWebhookResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
356
357                Ok(webhook_response)
358            },
359            _ => {
360                let body = res.text().await.map_err(error::Error::BodyRead)?;
361                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
362
363                Err(error::Error::Api(error))
364            }
365        }
366    }
367
368    /// Create a new webhook with a given URL. The URL will receive webhook events as JSON-encoded `POST` requests. The URL must respond with a HTTP `200` status on success.
369    /// There is currently a limit of 10 webhooks at any given time. Once this limit is reached, existing webhooks will need to be deleted before new webhooks can be created.
370    /// Event delivery is retried with exponential backoff if the URL is unreachable or it does not respond with a `200` status. The response includes a `secretKey` attribute, which is used to sign requests sent to the webhook URL. It will not be returned from any other endpoints within the Up API. If the `secretKey` is lost, simply create a new webhook with the same URL, capture its `secretKey` and then delete the original webhook. See Handling webhook events for details on how to process webhook events.
371    /// It is probably a good idea to test the webhook by sending it a `PING` event after creating it.
372    pub async fn create_webhook(&self, webhook_url : &str, description : Option<String>) -> Result<CreateWebhookResponse, error::Error> {
373        let url = reqwest::Url::parse(&format!("{}/webhooks", BASE_URL)).map_err(error::Error::UrlParse)?;
374
375        let body = CreateWebhookRequest {
376            data : WebhookInputResource {
377                attributes : InputAttributes { url : String::from(webhook_url), description }
378            }
379        };
380
381        let body = serde_json::to_string(&body).map_err(error::Error::Serialize)?;
382
383        let res = reqwest::Client::new()
384            .post(url)
385            .header("Authorization", self.auth_header())
386            .header("Content-Type", "application/json")
387            .body(body)
388            .send()
389            .await
390            .map_err(error::Error::Request)?;
391
392        match res.status() {
393            reqwest::StatusCode::CREATED => {
394                let body = res.text().await.map_err(error::Error::BodyRead)?;
395                let webhook_response : CreateWebhookResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
396
397                Ok(webhook_response)
398            },
399            _ => {
400                let body = res.text().await.map_err(error::Error::BodyRead)?;
401                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
402
403                Err(error::Error::Api(error))
404            }
405        }
406    }
407
408    /// Delete a specific webhook by providing its unique identifier. Once deleted, webhook events will no longer be sent to the configured URL.
409    pub async fn delete_webhook(&self, id : &str) -> Result<(), error::Error> {
410        let url = reqwest::Url::parse(&format!("{}/webhooks/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?;
411
412        let res = reqwest::Client::new()
413            .delete(url)
414            .header("Authorization", self.auth_header())
415            .send()
416            .await
417            .map_err(error::Error::Request)?;
418
419        match res.status() {
420            reqwest::StatusCode::NO_CONTENT => {
421                Ok(())
422            },
423            _ => {
424                let body = res.text().await.map_err(error::Error::BodyRead)?;
425                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
426
427                Err(error::Error::Api(error))
428            }
429        }
430    }
431
432    /// Send a `PING` event to a webhook by providing its unique identifier. This is useful for testing and debugging purposes. The event is delivered asynchronously and its data is returned in the response to this request.
433    pub async fn ping_webhook(&self, id : &str) -> Result<PingWebhookResponse, error::Error> {
434        let url = reqwest::Url::parse(&format!("{}/webhooks/{}/ping", BASE_URL, id)).map_err(error::Error::UrlParse)?;
435
436        let res = reqwest::Client::new()
437            .post(url)
438            .header("Authorization", self.auth_header())
439            .header("Content-Type", "application/json")
440            .header("Content-Length", "0")
441            .send()
442            .await
443            .map_err(error::Error::Request)?;
444
445        match res.status() {
446            reqwest::StatusCode::CREATED => {
447                let body = res.text().await.map_err(error::Error::BodyRead)?;
448                let webhook_response : PingWebhookResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
449
450                Ok(webhook_response)
451            },
452            _ => {
453                let body = res.text().await.map_err(error::Error::BodyRead)?;
454                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
455
456                Err(error::Error::Api(error))
457            }
458        }
459    }
460
461    /// Retrieve a list of delivery logs for a webhook by providing its unique identifier. This is useful for analysis and debugging purposes. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. Results are ordered newest first to oldest last. Logs may be automatically purged after a period of time.
462    pub async fn list_webhook_logs(&self, id : &str, options : &ListWebhookLogsOptions) -> Result<ListWebhookLogsResponse, error::Error> {
463        let mut url = reqwest::Url::parse(&format!("{}/webhooks/{}/logs", BASE_URL, id)).map_err(error::Error::UrlParse)?;
464        options.add_params(&mut url);
465
466        let res = reqwest::Client::new()
467            .get(url)
468            .header("Authorization", self.auth_header())
469            .send()
470            .await
471            .map_err(error::Error::Request)?;
472
473        match res.status() {
474            reqwest::StatusCode::OK => {
475                let body = res.text().await.map_err(error::Error::BodyRead)?;
476                let webhook_response : ListWebhookLogsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
477
478                Ok(webhook_response)
479            },
480            _ => {
481                let body = res.text().await.map_err(error::Error::BodyRead)?;
482                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;
483
484                Err(error::Error::Api(error))
485            }
486        }
487    }
488}
489
490// ----------------- Page Navigation -----------------
491
492implement_pagination_v1!(ListWebhooksResponse);