Skip to main content

resend_rs/
webhooks.rs

1use std::sync::Arc;
2
3use reqwest::Method;
4
5use crate::{
6    Config, Result,
7    list_opts::{ListOptions, ListResponse},
8    types::{
9        CreateWebhookOptions, CreateWebhookResponse, DeleteWebhookResponse, UpdateWebhookOptions,
10        UpdateWebhookResponse, Webhook,
11    },
12};
13
14/// `Resend` APIs for `/webhooks` endpoints.
15#[derive(Clone, Debug)]
16pub struct WebhookSvc(pub(crate) Arc<Config>);
17
18impl WebhookSvc {
19    /// Create a webhook to receive real-time notifications about email events.
20    ///
21    /// <https://resend.com/docs/api-reference/webhooks/create-webhook>
22    #[maybe_async::maybe_async]
23    pub async fn create(&self, webhook: CreateWebhookOptions) -> Result<CreateWebhookResponse> {
24        let request = self.0.build(Method::POST, "/webhooks");
25        let response = self.0.send(request.json(&webhook)).await?;
26        let content = response.json::<CreateWebhookResponse>().await?;
27
28        Ok(content)
29    }
30
31    /// Retrieve a single webhook for the authenticated user.
32    ///
33    /// <https://resend.com/docs/api-reference/webhooks/get-webhook>
34    #[maybe_async::maybe_async]
35    pub async fn get(&self, webhook_id: &str) -> Result<Webhook> {
36        let path = format!("/webhooks/{webhook_id}");
37
38        let request = self.0.build(Method::GET, &path);
39        let response = self.0.send(request).await?;
40        let content = response.json::<Webhook>().await?;
41
42        Ok(content)
43    }
44
45    /// Update an existing webhook configuration.
46    #[maybe_async::maybe_async]
47    pub async fn update(
48        &self,
49        webhook_id: &str,
50        update: UpdateWebhookOptions,
51    ) -> Result<UpdateWebhookResponse> {
52        let path = format!("/webhooks/{webhook_id}");
53
54        let request = self.0.build(Method::PATCH, &path);
55        let response = self.0.send(request.json(&update)).await?;
56        let content = response.json::<UpdateWebhookResponse>().await?;
57
58        Ok(content)
59    }
60
61    /// Retrieve a list of webhooks for the authenticated user.
62    ///
63    /// <https://resend.com/docs/api-reference/webhooks/list-webhooks>
64    #[maybe_async::maybe_async]
65    pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<Webhook>> {
66        let request = self.0.build(Method::GET, "/webhooks").query(&list_opts);
67        let response = self.0.send(request).await?;
68        let content = response.json::<ListResponse<Webhook>>().await?;
69
70        Ok(content)
71    }
72
73    /// Remove an existing webhook.
74    ///
75    /// <https://resend.com/docs/api-reference/webhooks/delete-webhook>
76    #[maybe_async::maybe_async]
77    pub async fn delete(&self, webhook_id: &str) -> Result<bool> {
78        let path = format!("/webhooks/{webhook_id}");
79
80        let request = self.0.build(Method::DELETE, &path);
81        let response = self.0.send(request).await?;
82        let content = response.json::<DeleteWebhookResponse>().await?;
83
84        Ok(content.deleted)
85    }
86}
87
88#[allow(unreachable_pub)]
89pub mod types {
90    use serde::{Deserialize, Serialize};
91
92    use crate::events::EventType;
93
94    crate::define_id_type!(WebhookId);
95
96    #[must_use]
97    #[derive(Debug, Clone, Serialize)]
98    pub struct CreateWebhookOptions {
99        endpoint: String,
100        events: Vec<EventType>,
101    }
102
103    impl CreateWebhookOptions {
104        pub fn new(
105            endpoint: impl Into<String>,
106            events: impl IntoIterator<Item = impl Into<EventType>>,
107        ) -> Self {
108            Self {
109                endpoint: endpoint.into(),
110                events: events.into_iter().map(Into::into).collect::<Vec<_>>(),
111            }
112        }
113    }
114
115    #[must_use]
116    #[derive(Debug, Clone, Serialize, Deserialize)]
117    pub struct CreateWebhookResponse {
118        pub id: WebhookId,
119        pub signing_secret: String,
120    }
121
122    #[must_use]
123    #[derive(Debug, Clone, Serialize, Deserialize)]
124    pub struct Webhook {
125        pub id: WebhookId,
126        pub created_at: String,
127        pub status: String,
128        pub endpoint: String,
129        #[serde(default)]
130        pub events: Vec<EventType>,
131    }
132
133    #[derive(Debug, Clone, Copy, Serialize)]
134    #[serde(rename_all = "lowercase")]
135    pub enum WebhookStatus {
136        Enabled,
137        Disabled,
138    }
139
140    #[must_use]
141    #[derive(Debug, Clone, Serialize, Default)]
142    pub struct UpdateWebhookOptions {
143        #[serde(skip_serializing_if = "Option::is_none")]
144        endpoint: Option<String>,
145        #[serde(skip_serializing_if = "Option::is_none")]
146        events: Option<Vec<EventType>>,
147        #[serde(skip_serializing_if = "Option::is_none")]
148        status: Option<WebhookStatus>,
149    }
150
151    impl UpdateWebhookOptions {
152        #[inline]
153        pub fn with_endpoint(mut self, endpoint: &str) -> Self {
154            self.endpoint = Some(endpoint.to_owned());
155            self
156        }
157
158        #[inline]
159        pub fn with_events(
160            mut self,
161            events: impl IntoIterator<Item = impl Into<EventType>>,
162        ) -> Self {
163            self.events = Some(events.into_iter().map(Into::into).collect());
164            self
165        }
166
167        #[inline]
168        pub fn with_status(mut self, status: WebhookStatus) -> Self {
169            self.status = Some(status);
170            self
171        }
172    }
173
174    #[derive(Debug, Clone, Serialize, Deserialize)]
175    pub struct UpdateWebhookResponse {
176        /// Unique identifier for the updated webhook.
177        pub id: WebhookId,
178    }
179
180    #[derive(Debug, Clone, Serialize, Deserialize)]
181    pub struct DeleteWebhookResponse {
182        /// The ID of the webhook.
183        #[allow(dead_code)]
184        pub id: WebhookId,
185        /// Indicates whether the webhook was deleted successfully.
186        pub deleted: bool,
187    }
188}
189
190#[cfg(test)]
191mod test {
192    use crate::{
193        events::EmailEventType,
194        types::Webhook,
195        types::{CreateWebhookOptions, CreateWebhookResponse},
196    };
197    #[cfg(not(feature = "blocking"))]
198    use crate::{
199        list_opts::ListOptions,
200        test::{CLIENT, DebugResult},
201        types::{UpdateWebhookOptions, WebhookStatus},
202    };
203
204    #[tokio_shared_rt::test(shared = true)]
205    #[serial_test::serial]
206    #[cfg(not(feature = "blocking"))]
207    async fn all() -> DebugResult<()> {
208        let resend = &*CLIENT;
209
210        let events = [
211            EmailEventType::EmailSent,
212            EmailEventType::EmailDelivered,
213            EmailEventType::EmailBounced,
214        ];
215        let webhook = CreateWebhookOptions::new("https://webhook.example.com/handler", events);
216        let id = resend.webhooks.create(webhook).await?.id;
217
218        std::thread::sleep(std::time::Duration::from_secs(1));
219
220        let webhook = resend.webhooks.get(&id).await?;
221        assert_eq!(webhook.events.len(), 3);
222        let webhooks = resend.webhooks.list(ListOptions::default()).await?;
223        assert!(!webhooks.is_empty());
224
225        let update = UpdateWebhookOptions::default()
226            .with_endpoint("https://new-webhook.example.com/handler")
227            .with_events([EmailEventType::EmailSent, EmailEventType::EmailDelivered])
228            .with_status(WebhookStatus::Enabled);
229        let _webhook = resend.webhooks.update(&id, update).await?;
230        std::thread::sleep(std::time::Duration::from_secs(1));
231        let webhook = resend.webhooks.get(&id).await?;
232        assert_eq!(webhook.events.len(), 2);
233
234        let deleted = resend.webhooks.delete(&id).await?;
235        assert!(deleted);
236        std::thread::sleep(std::time::Duration::from_secs(1));
237
238        let webhooks = resend.webhooks.list(ListOptions::default()).await?;
239        assert!(webhooks.is_empty());
240
241        Ok(())
242    }
243
244    #[test]
245    fn serialize_test() {
246        let events = [EmailEventType::EmailSent];
247
248        let webhook =
249            CreateWebhookOptions::new("https://webhook.example.com/handler".to_owned(), events);
250
251        let res = serde_json::to_string(&webhook);
252        assert!(res.is_ok());
253    }
254
255    #[test]
256    fn deserialize_test() {
257        let create_webhook_res = r#"{
258  "object": "webhook",
259  "id": "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
260  "signing_secret": "whsec_xxxxxxxxxx"
261}"#;
262
263        let res = serde_json::from_str::<CreateWebhookResponse>(create_webhook_res);
264        assert!(res.is_ok());
265
266        let webhook = r#"{
267  "object": "webhook",
268  "id": "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
269  "created_at": "2023-08-22 15:28:00+00",
270  "status": "enabled",
271  "endpoint": "https://webhook.example.com/handler",
272  "events": ["email.sent", "email.received"],
273  "signing_secret": "whsec_xxxxxxxxxx"
274}"#;
275
276        let res = serde_json::from_str::<Webhook>(webhook);
277        assert!(res.is_ok());
278    }
279}