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