feat(webhooks): allow manually retrying delivery of outgoing webhooks (#4176)

This commit is contained in:
Sanchith Hegde
2024-04-04 14:37:51 +05:30
committed by GitHub
parent 21e2d78117
commit 63d2b6855a
16 changed files with 449 additions and 64 deletions

View File

@ -34,6 +34,7 @@ pub enum Permission {
WebhookEventRead,
PayoutWrite,
PayoutRead,
WebhookEventWrite,
}
#[derive(Debug, serde::Serialize)]

View File

@ -94,6 +94,14 @@ pub struct EventRetrieveResponse {
pub delivery_attempt: Option<WebhookDeliveryAttempt>,
}
impl common_utils::events::ApiEventMetric for EventRetrieveResponse {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Events {
merchant_id_or_profile_id: self.event_information.merchant_id.clone(),
})
}
}
/// The request information (headers and body) sent in the webhook.
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct OutgoingWebhookRequestContent {
@ -114,20 +122,24 @@ pub struct OutgoingWebhookRequestContent {
#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct OutgoingWebhookResponseContent {
/// The response body received for the webhook sent.
#[schema(value_type = String)]
#[schema(value_type = Option<String>)]
#[serde(alias = "payload")]
pub body: Secret<String>,
pub body: Option<Secret<String>>,
/// The response headers received for the webhook sent.
#[schema(
value_type = Vec<(String, String)>,
value_type = Option<Vec<(String, String)>>,
example = json!([["content-type", "application/json"], ["content-length", "1024"]]))
]
pub headers: Vec<(String, Secret<String>)>,
pub headers: Option<Vec<(String, Secret<String>)>>,
/// The HTTP status code for the webhook sent.
#[schema(example = 200)]
pub status_code: u16,
pub status_code: Option<u16>,
/// Error message in case any error occurred when trying to deliver the webhook.
#[schema(example = 200)]
pub error_message: Option<String>,
}
#[derive(Debug, serde::Serialize)]
@ -157,3 +169,17 @@ impl common_utils::events::ApiEventMetric for WebhookDeliveryAttemptListRequestI
})
}
}
#[derive(Debug, serde::Serialize)]
pub struct WebhookDeliveryRetryRequestInternal {
pub merchant_id_or_profile_id: String,
pub event_id: String,
}
impl common_utils::events::ApiEventMetric for WebhookDeliveryRetryRequestInternal {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Events {
merchant_id_or_profile_id: self.merchant_id_or_profile_id.clone(),
})
}
}

View File

@ -177,6 +177,7 @@ Never share your secret api keys. Keep them guarded and secure.
// Routes for events
routes::webhook_events::list_initial_webhook_delivery_attempts,
routes::webhook_events::list_webhook_delivery_attempts,
routes::webhook_events::retry_webhook_delivery_attempt,
),
components(schemas(
api_models::refunds::RefundRequest,

View File

@ -68,3 +68,27 @@ pub fn list_initial_webhook_delivery_attempts() {}
security(("admin_api_key" = []))
)]
pub fn list_webhook_delivery_attempts() {}
/// Events - Manual Retry
///
/// Manually retry the delivery of the specified Event.
#[utoipa::path(
post,
path = "/events/{merchant_id_or_profile_id}/{event_id}/retry",
params(
("merchant_id_or_profile_id" = String, Path, description = "The unique identifier for the Merchant Account or Business Profile"),
("event_id" = String, Path, description = "The unique identifier for the Event"),
),
responses(
(
status = 200,
description = "The delivery of the Event was attempted. \
Check the `response` field in the response payload to identify the status of the delivery attempt.",
body = EventRetrieveResponse
),
),
tag = "Event",
operation_id = "Manually retry the delivery of an Event",
security(("admin_api_key" = []))
)]
pub fn retry_webhook_delivery_attempt() {}

View File

@ -887,9 +887,69 @@ async fn trigger_webhook_to_merchant(
);
logger::debug!(outgoing_webhook_response=?response);
let update_event_if_client_error =
|state: AppState,
merchant_key_store: domain::MerchantKeyStore,
merchant_id: String,
event_id: String,
error_message: String| async move {
let is_webhook_notified = false;
let response_to_store = OutgoingWebhookResponseContent {
body: None,
headers: None,
status_code: None,
error_message: Some(error_message),
};
let event_update = domain::EventUpdate::UpdateResponse {
is_webhook_notified,
response: Some(
domain_types::encrypt(
response_to_store
.encode_to_string_of_json()
.change_context(
errors::WebhooksFlowError::OutgoingWebhookResponseEncodingFailed,
)
.map(Secret::new)?,
merchant_key_store.key.get_inner().peek(),
)
.await
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
.attach_printable("Failed to encrypt outgoing webhook response content")?,
),
};
state
.store
.update_event_by_merchant_id_event_id(
&merchant_id,
&event_id,
event_update,
&merchant_key_store,
)
.await
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
};
let api_client_error_handler =
|client_error: error_stack::Report<errors::ApiClientError>,
delivery_attempt: enums::WebhookDeliveryAttempt| {
|state: AppState,
merchant_key_store: domain::MerchantKeyStore,
merchant_id: String,
event_id: String,
client_error: error_stack::Report<errors::ApiClientError>,
delivery_attempt: enums::WebhookDeliveryAttempt| async move {
// Not including detailed error message in response information since it contains too
// much of diagnostic information to be exposed to the merchant.
update_event_if_client_error(
state,
merchant_key_store,
merchant_id,
event_id,
"Unable to send request to merchant server".to_string(),
)
.await?;
let error =
client_error.change_context(errors::WebhooksFlowError::CallToMerchantFailed);
logger::error!(
@ -897,6 +957,8 @@ async fn trigger_webhook_to_merchant(
?delivery_attempt,
"An error occurred when sending webhook to merchant"
);
Ok::<_, error_stack::Report<errors::WebhooksFlowError>>(())
};
let update_event_in_storage = |state: AppState,
merchant_key_store: domain::MerchantKeyStore,
@ -934,9 +996,10 @@ async fn trigger_webhook_to_merchant(
Secret::from(String::from("Non-UTF-8 response body"))
});
let response_to_store = OutgoingWebhookResponseContent {
body: response_body,
headers: response_headers,
status_code: status_code.as_u16(),
body: Some(response_body),
headers: Some(response_headers),
status_code: Some(status_code.as_u16()),
error_message: None,
};
let event_update = domain::EventUpdate::UpdateResponse {
@ -953,7 +1016,7 @@ async fn trigger_webhook_to_merchant(
)
.await
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
.attach_printable("Failed to encrypt outgoing webhook request content")?,
.attach_printable("Failed to encrypt outgoing webhook response content")?,
),
};
state
@ -967,16 +1030,19 @@ async fn trigger_webhook_to_merchant(
.await
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
};
let increment_webhook_outgoing_received_count = |merchant_id: String| {
metrics::WEBHOOK_OUTGOING_RECEIVED_COUNT.add(
&metrics::CONTEXT,
1,
&[metrics::KeyValue::new(MERCHANT_ID, merchant_id)],
)
};
let success_response_handler =
|state: AppState,
merchant_id: String,
process_tracker: Option<storage::ProcessTracker>,
business_status: &'static str| async move {
metrics::WEBHOOK_OUTGOING_RECEIVED_COUNT.add(
&metrics::CONTEXT,
1,
&[metrics::KeyValue::new(MERCHANT_ID, merchant_id)],
);
increment_webhook_outgoing_received_count(merchant_id);
match process_tracker {
Some(process_tracker) => state
@ -1006,7 +1072,17 @@ async fn trigger_webhook_to_merchant(
match delivery_attempt {
enums::WebhookDeliveryAttempt::InitialAttempt => match response {
Err(client_error) => api_client_error_handler(client_error, delivery_attempt),
Err(client_error) => {
api_client_error_handler(
state.clone(),
merchant_key_store.clone(),
business_profile.merchant_id.clone(),
event_id.clone(),
client_error,
delivery_attempt,
)
.await?
}
Ok(response) => {
let status_code = response.status();
let _updated_event = update_event_in_storage(
@ -1043,7 +1119,15 @@ async fn trigger_webhook_to_merchant(
.attach_printable("`process_tracker` is unavailable in automatic retry flow")?;
match response {
Err(client_error) => {
api_client_error_handler(client_error, delivery_attempt);
api_client_error_handler(
state.clone(),
merchant_key_store.clone(),
business_profile.merchant_id.clone(),
event_id.clone(),
client_error,
delivery_attempt,
)
.await?;
// Schedule a retry attempt for webhook delivery
outgoing_webhook_retry::retry_webhook_delivery_task(
&*state.store,
@ -1095,10 +1179,41 @@ async fn trigger_webhook_to_merchant(
}
}
}
enums::WebhookDeliveryAttempt::ManualRetry => {
// Will be updated when manual retry is implemented
Err(errors::WebhooksFlowError::NotReceivedByMerchant)?
enums::WebhookDeliveryAttempt::ManualRetry => match response {
Err(client_error) => {
api_client_error_handler(
state.clone(),
merchant_key_store.clone(),
business_profile.merchant_id.clone(),
event_id.clone(),
client_error,
delivery_attempt,
)
.await?
}
Ok(response) => {
let status_code = response.status();
let _updated_event = update_event_in_storage(
state.clone(),
merchant_key_store.clone(),
business_profile.merchant_id.clone(),
event_id.clone(),
response,
)
.await?;
if status_code.is_success() {
increment_webhook_outgoing_received_count(business_profile.merchant_id.clone());
} else {
error_response_handler(
business_profile.merchant_id,
delivery_attempt,
status_code.as_u16(),
"Ignoring error when sending webhook to merchant",
);
}
}
},
}
Ok(())

View File

@ -1,19 +1,21 @@
use error_stack::ResultExt;
use masking::PeekInterface;
use router_env::{instrument, tracing};
use crate::{
core::errors::{self, RouterResponse, StorageErrorExt},
routes::AppState,
services::ApplicationResponse,
types::{api, domain, transformers::ForeignTryFrom},
types::{api, domain, storage, transformers::ForeignTryFrom},
utils::{OptionExt, StringExt},
};
const INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_LIMIT: i64 = 100;
#[derive(Debug)]
enum MerchantIdOrProfileId {
MerchantId(String),
ProfileId(String),
enum MerchantAccountOrBusinessProfile {
MerchantAccount(domain::MerchantAccount),
BusinessProfile(storage::BusinessProfile),
}
#[instrument(skip(state))]
@ -27,22 +29,22 @@ pub async fn list_initial_delivery_attempts(
let store = state.store.as_ref();
let (identifier, key_store) =
let (account, key_store) =
determine_identifier_and_get_key_store(state.clone(), merchant_id_or_profile_id).await?;
let events = match constraints {
api_models::webhook_events::EventListConstraintsInternal::ObjectIdFilter { object_id } => {
match identifier {
MerchantIdOrProfileId::MerchantId(merchant_id) => store
match account {
MerchantAccountOrBusinessProfile::MerchantAccount(merchant_account) => store
.list_initial_events_by_merchant_id_primary_object_id(
&merchant_id,
&merchant_account.merchant_id,
&object_id,
&key_store,
)
.await,
MerchantIdOrProfileId::ProfileId(profile_id) => store
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile) => store
.list_initial_events_by_profile_id_primary_object_id(
&profile_id,
&business_profile.profile_id,
&object_id,
&key_store,
)
@ -69,10 +71,10 @@ pub async fn list_initial_delivery_attempts(
_ => None,
};
match identifier {
MerchantIdOrProfileId::MerchantId(merchant_id) => store
match account {
MerchantAccountOrBusinessProfile::MerchantAccount(merchant_account) => store
.list_initial_events_by_merchant_id_constraints(
&merchant_id,
&merchant_account.merchant_id,
created_after,
created_before,
limit,
@ -80,9 +82,9 @@ pub async fn list_initial_delivery_attempts(
&key_store,
)
.await,
MerchantIdOrProfileId::ProfileId(profile_id) => store
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile) => store
.list_initial_events_by_profile_id_constraints(
&profile_id,
&business_profile.profile_id,
created_after,
created_before,
limit,
@ -112,23 +114,23 @@ pub async fn list_delivery_attempts(
) -> RouterResponse<Vec<api::webhook_events::EventRetrieveResponse>> {
let store = state.store.as_ref();
let (identifier, key_store) =
let (account, key_store) =
determine_identifier_and_get_key_store(state.clone(), merchant_id_or_profile_id).await?;
let events = match identifier {
MerchantIdOrProfileId::MerchantId(merchant_id) => {
let events = match account {
MerchantAccountOrBusinessProfile::MerchantAccount(merchant_account) => {
store
.list_events_by_merchant_id_initial_attempt_id(
&merchant_id,
&merchant_account.merchant_id,
&initial_attempt_id,
&key_store,
)
.await
}
MerchantIdOrProfileId::ProfileId(profile_id) => {
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile) => {
store
.list_events_by_profile_id_initial_attempt_id(
&profile_id,
&business_profile.profile_id,
&initial_attempt_id,
&key_store,
)
@ -153,10 +155,108 @@ pub async fn list_delivery_attempts(
}
}
#[instrument(skip(state))]
pub async fn retry_delivery_attempt(
state: AppState,
merchant_id_or_profile_id: String,
event_id: String,
) -> RouterResponse<api::webhook_events::EventRetrieveResponse> {
let store = state.store.as_ref();
let (account, key_store) =
determine_identifier_and_get_key_store(state.clone(), merchant_id_or_profile_id).await?;
let event_to_retry = store
.find_event_by_merchant_id_event_id(&key_store.merchant_id, &event_id, &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::EventNotFound)?;
let business_profile = match account {
MerchantAccountOrBusinessProfile::MerchantAccount(_) => {
let business_profile_id = event_to_retry
.business_profile_id
.get_required_value("business_profile_id")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to read business profile ID from event to retry")?;
store
.find_business_profile_by_profile_id(&business_profile_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to find business profile")
}
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile) => Ok(business_profile),
}?;
let delivery_attempt = storage::enums::WebhookDeliveryAttempt::ManualRetry;
let new_event_id = super::utils::generate_event_id();
let idempotent_event_id = super::utils::get_idempotent_event_id(
&event_to_retry.primary_object_id,
event_to_retry.event_type,
delivery_attempt,
);
let now = common_utils::date_time::now();
let new_event = domain::Event {
event_id: new_event_id.clone(),
event_type: event_to_retry.event_type,
event_class: event_to_retry.event_class,
is_webhook_notified: false,
primary_object_id: event_to_retry.primary_object_id,
primary_object_type: event_to_retry.primary_object_type,
created_at: now,
merchant_id: Some(business_profile.merchant_id.clone()),
business_profile_id: Some(business_profile.profile_id.clone()),
primary_object_created_at: event_to_retry.primary_object_created_at,
idempotent_event_id: Some(idempotent_event_id),
initial_attempt_id: event_to_retry.initial_attempt_id,
request: event_to_retry.request,
response: None,
delivery_attempt: Some(delivery_attempt),
};
let event = store
.insert_event(new_event, &key_store)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to insert event")?;
// We only allow retrying deliveries for events with `request` populated.
let request_content = event
.request
.as_ref()
.get_required_value("request")
.change_context(errors::ApiErrorResponse::InternalServerError)?
.peek()
.parse_struct("OutgoingWebhookRequestContent")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse webhook event request information")?;
super::trigger_webhook_and_raise_event(
state.clone(),
business_profile,
&key_store,
event,
request_content,
delivery_attempt,
None,
None,
)
.await;
let updated_event = store
.find_event_by_merchant_id_event_id(&key_store.merchant_id, &new_event_id, &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::EventNotFound)?;
Ok(ApplicationResponse::Json(
api::webhook_events::EventRetrieveResponse::try_from(updated_event)?,
))
}
async fn determine_identifier_and_get_key_store(
state: AppState,
merchant_id_or_profile_id: String,
) -> errors::RouterResult<(MerchantIdOrProfileId, domain::MerchantKeyStore)> {
) -> errors::RouterResult<(MerchantAccountOrBusinessProfile, domain::MerchantKeyStore)> {
let store = state.store.as_ref();
match store
.get_merchant_key_store_by_merchant_id(
@ -165,13 +265,25 @@ async fn determine_identifier_and_get_key_store(
)
.await
{
// Valid merchant ID
Ok(key_store) => Ok((
MerchantIdOrProfileId::MerchantId(merchant_id_or_profile_id),
key_store,
)),
// Since a merchant key store was found with `merchant_id` = `merchant_id_or_profile_id`,
// `merchant_id_or_profile_id` is a valid merchant ID.
// Find a merchant account having `merchant_id` = `merchant_id_or_profile_id`.
Ok(key_store) => {
let merchant_account = store
.find_merchant_account_by_merchant_id(&merchant_id_or_profile_id, &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
// Invalid merchant ID, check if we can find a business profile with the identifier
Ok((
MerchantAccountOrBusinessProfile::MerchantAccount(merchant_account),
key_store,
))
}
// Since no merchant key store was found with `merchant_id` = `merchant_id_or_profile_id`,
// `merchant_id_or_profile_id` is not a valid merchant ID.
// Assuming that `merchant_id_or_profile_id` is a business profile ID, try to find a
// business profile having `profile_id` = `merchant_id_or_profile_id`.
Err(error) if error.current_context().is_db_not_found() => {
router_env::logger::debug!(
?error,
@ -195,7 +307,7 @@ async fn determine_identifier_and_get_key_store(
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
Ok((
MerchantIdOrProfileId::ProfileId(business_profile.profile_id),
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile),
key_store,
))
}

View File

@ -1247,8 +1247,15 @@ impl WebhookEvents {
.app_data(web::Data::new(config))
.service(web::resource("").route(web::get().to(list_initial_webhook_delivery_attempts)))
.service(
web::resource("/{event_id}/attempts")
web::scope("/{event_id}")
.service(
web::resource("attempts")
.route(web::get().to(list_webhook_delivery_attempts)),
)
.service(
web::resource("retry")
.route(web::post().to(retry_webhook_delivery_attempt)),
),
)
}
}

View File

@ -137,7 +137,8 @@ impl From<Flow> for ApiIdentifier {
Flow::FrmFulfillment
| Flow::IncomingWebhookReceive
| Flow::WebhookEventInitialDeliveryAttemptList
| Flow::WebhookEventDeliveryAttemptList => Self::Webhooks,
| Flow::WebhookEventDeliveryAttemptList
| Flow::WebhookEventDeliveryRetry => Self::Webhooks,
Flow::ApiKeyCreate
| Flow::ApiKeyRetrieve

View File

@ -7,6 +7,7 @@ use crate::{
services::{api, authentication as auth, authorization::permissions::Permission},
types::api::webhook_events::{
EventListConstraints, EventListRequestInternal, WebhookDeliveryAttemptListRequestInternal,
WebhookDeliveryRetryRequestInternal,
},
};
@ -89,3 +90,42 @@ pub async fn list_webhook_delivery_attempts(
)
.await
}
#[instrument(skip_all, fields(flow = ?Flow::WebhookEventDeliveryRetry))]
pub async fn retry_webhook_delivery_attempt(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<(String, String)>,
) -> impl Responder {
let flow = Flow::WebhookEventDeliveryRetry;
let (merchant_id_or_profile_id, event_id) = path.into_inner();
let request_internal = WebhookDeliveryRetryRequestInternal {
merchant_id_or_profile_id: merchant_id_or_profile_id.clone(),
event_id,
};
api::server_wrap(
flow,
state,
&req,
request_internal,
|state, _, request_internal| {
webhook_events::retry_delivery_attempt(
state,
request_internal.merchant_id_or_profile_id,
request_internal.event_id,
)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantOrProfileFromRoute {
merchant_id_or_profile_id,
required_permission: Permission::WebhookEventWrite,
},
req.headers(),
),
api_locking::LockAction::NotApplicable,
)
.await
}

View File

@ -75,12 +75,13 @@ pub static USERS_MANAGE: [Permission; 2] =
pub static MERCHANT_DETAILS_VIEW: [Permission; 1] = [Permission::MerchantAccountRead];
pub static MERCHANT_DETAILS_MANAGE: [Permission; 5] = [
pub static MERCHANT_DETAILS_MANAGE: [Permission; 6] = [
Permission::MerchantAccountWrite,
Permission::ApiKeyRead,
Permission::ApiKeyWrite,
Permission::MerchantAccountRead,
Permission::WebhookEventRead,
Permission::WebhookEventWrite,
];
pub static ORGANIZATION_MANAGE: [Permission; 2] = [

View File

@ -31,6 +31,7 @@ pub enum Permission {
UsersWrite,
MerchantAccountCreate,
WebhookEventRead,
WebhookEventWrite,
PayoutRead,
PayoutWrite,
}
@ -71,6 +72,7 @@ impl Permission {
Self::UsersWrite => "Invite users, assign and update roles",
Self::MerchantAccountCreate => "Create merchant account",
Self::WebhookEventRead => "View webhook events",
Self::WebhookEventWrite => "Trigger retries for webhook events",
Self::PayoutRead => "View all payouts",
Self::PayoutWrite => "Create payout, download payout data",
}

View File

@ -2,4 +2,5 @@ pub use api_models::webhook_events::{
EventListConstraints, EventListConstraintsInternal, EventListItemResponse,
EventListRequestInternal, EventRetrieveResponse, OutgoingWebhookRequestContent,
OutgoingWebhookResponseContent, WebhookDeliveryAttemptListRequestInternal,
WebhookDeliveryRetryRequestInternal,
};

View File

@ -44,6 +44,7 @@ impl From<Permission> for user_role_api::Permission {
Permission::UsersWrite => Self::UsersWrite,
Permission::MerchantAccountCreate => Self::MerchantAccountCreate,
Permission::WebhookEventRead => Self::WebhookEventRead,
Permission::WebhookEventWrite => Self::WebhookEventWrite,
Permission::PayoutRead => Self::PayoutRead,
Permission::PayoutWrite => Self::PayoutWrite,
}

View File

@ -120,7 +120,7 @@ impl ProcessTrackerWorkflow<AppState> for OutgoingWebhookRetryWorkflow {
&key_store,
event,
request_content,
storage::enums::WebhookDeliveryAttempt::AutomaticRetry,
delivery_attempt,
None,
Some(process),
)
@ -172,7 +172,7 @@ impl ProcessTrackerWorkflow<AppState> for OutgoingWebhookRetryWorkflow {
&key_store,
event,
request_content,
storage::enums::WebhookDeliveryAttempt::AutomaticRetry,
delivery_attempt,
Some(content),
Some(process),
)

View File

@ -398,6 +398,8 @@ pub enum Flow {
WebhookEventInitialDeliveryAttemptList,
/// List delivery attempts for a webhook event
WebhookEventDeliveryAttemptList,
/// Manually retry the delivery for a webhook event
WebhookEventDeliveryRetry,
}
///

View File

@ -4469,6 +4469,53 @@
}
]
}
},
"/events/{merchant_id_or_profile_id}/{event_id}/retry": {
"post": {
"tags": [
"Event"
],
"summary": "Events - Manual Retry",
"description": "Events - Manual Retry\n\nManually retry the delivery of the specified Event.",
"operationId": "Manually retry the delivery of an Event",
"parameters": [
{
"name": "merchant_id_or_profile_id",
"in": "path",
"description": "The unique identifier for the Merchant Account or Business Profile",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "event_id",
"in": "path",
"description": "The unique identifier for the Event",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The delivery of the Event was attempted. Check the `response` field in the response payload to identify the status of the delivery attempt.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EventRetrieveResponse"
}
}
}
}
},
"security": [
{
"admin_api_key": []
}
]
}
}
},
"components": {
@ -11661,15 +11708,11 @@
"OutgoingWebhookResponseContent": {
"type": "object",
"description": "The response information (headers, body and status code) received for the webhook sent.",
"required": [
"body",
"headers",
"status_code"
],
"properties": {
"body": {
"type": "string",
"description": "The response body received for the webhook sent."
"description": "The response body received for the webhook sent.",
"nullable": true
},
"headers": {
"type": "array",
@ -11696,14 +11739,22 @@
"content-length",
"1024"
]
]
],
"nullable": true
},
"status_code": {
"type": "integer",
"format": "int32",
"description": "The HTTP status code for the webhook sent.",
"example": 200,
"nullable": true,
"minimum": 0
},
"error_message": {
"type": "string",
"description": "Error message in case any error occurred when trying to deliver the webhook.",
"example": 200,
"nullable": true
}
}
},