{
  "openapi": "3.1.0",
  "info": {
    "title": "Ospree Webhooks API",
    "description": "API for managing webhooks within the Ospree platform.",
    "contact": {
      "name": "Ospree",
      "url": "https://www.ospree.io/",
      "email": "support@ospree.io"
    },
    "version": "2.0.0"
  },
  "paths": {
    "/api/v2/webhooks": {
      "get": {
        "tags": [
          "Webhook Management"
        ],
        "summary": "List Webhooks",
        "operationId": "list_webhooks_api_v2_webhooks_get",
        "responses": {
          "200": {
            "description": "Every live endpoint this business holds, across all event families.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiSuccessResponse_List_WebhookConfigurationView__"
                },
                "examples": {
                  "two_endpoints": {
                    "summary": "Two endpoints, in different families",
                    "description": "Note `event_type` is the family NAME here, while the configure call takes and returns its integer id. No signing secret is ever listed.",
                    "value": {
                      "status": "success",
                      "data": [
                        {
                          "id": "018f3c1e-6b2a-7d48-9f3c-1e6b2a7d4f80",
                          "business_id": "018e2b0d-5a19-7c37-8e2b-0d5a197c3760",
                          "event_type": "travel_rule",
                          "webhook_url": "https://partner.example.com/ospree/webhooks",
                          "event_names": [
                            "INCOMING_TRANSFER",
                            "TRANSFER_COMPLETED"
                          ],
                          "ip_allowlist": [
                            "203.0.113.7"
                          ],
                          "modified_by": "ops@partner.example.com",
                          "last_modified": "2026-08-31T11:02:05Z",
                          "webhook_desc": "Travel rule events"
                        },
                        {
                          "id": "018f3c1e-6b2a-7d48-9f3c-1e6b2a7d4f81",
                          "business_id": "018e2b0d-5a19-7c37-8e2b-0d5a197c3760",
                          "event_type": "address_verification",
                          "webhook_url": "https://partner.example.com/ospree/address-verification",
                          "webhook_desc": "Address verification events"
                        }
                      ]
                    }
                  },
                  "no_endpoints": {
                    "summary": "No endpoints configured",
                    "value": {
                      "status": "success",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                },
                "example": {
                  "status": "fail",
                  "message": "Authentication required.",
                  "error_code": "auth.unauthenticated"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                },
                "example": {
                  "status": "fail",
                  "message": "Payment required.",
                  "error_code": "billing.payment_required"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                },
                "example": {
                  "status": "error",
                  "code": 500,
                  "message": "Something went wrong.",
                  "error_code": "internal_error"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      },
      "post": {
        "tags": [
          "Webhook Management"
        ],
        "summary": "Configure Webhook",
        "description": "Upsert this business's webhook endpoint for one event family.\n\nAn upsert with no id, and deliberately not a create: it updates the family's\nendpoint, or creates one when the family has none. A family may hold several\nendpoints, up to a maximum of 5, but this call never adds the second one — it\nanswers 409 rather than overwriting an arbitrary one of them.",
        "operationId": "configure_webhook_api_v2_webhooks_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfigureWebhookRequest"
              },
              "examples": {
                "subscribe_to_named_events": {
                  "summary": "Subscribe to specific events in a family",
                  "description": "Only the named events are delivered. A name the family does not publish is rejected at configure time -- a typo here would otherwise be a silent, permanent mute.",
                  "value": {
                    "event_type": 1,
                    "webhook_url": "https://partner.example.com/ospree/webhooks",
                    "event_names": [
                      "INCOMING_TRANSFER",
                      "TRANSFER_COMPLETED"
                    ],
                    "ip_allowlist": [
                      "203.0.113.7"
                    ]
                  }
                },
                "subscribe_to_whole_family": {
                  "summary": "Subscribe to every event in a family",
                  "description": "An omitted or empty `event_names` subscribes to all events the family publishes.",
                  "value": {
                    "event_type": 3,
                    "webhook_url": "https://partner.example.com/ospree/address-verification"
                  }
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "The endpoint was created or updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiSuccessResponse_WebhookConfiguration_"
                },
                "examples": {
                  "created_with_secret": {
                    "summary": "First create -- carries the signing secret",
                    "description": "The plaintext signing secret is returned by the call that minted it and by no other. Store it now.",
                    "value": {
                      "status": "success",
                      "data": {
                        "id": "018f3c1e-6b2a-7d48-9f3c-1e6b2a7d4f80",
                        "event_type": 1,
                        "business_id": "018e2b0d-5a19-7c37-8e2b-0d5a197c3760",
                        "webhook_url": "https://partner.example.com/ospree/webhooks",
                        "event_names": [
                          "INCOMING_TRANSFER",
                          "TRANSFER_COMPLETED"
                        ],
                        "ip_allowlist": [
                          "203.0.113.7"
                        ],
                        "created_by": "018d1a9c-4908-7b26-9d1a-9c49087b2650",
                        "created": "2026-08-31T09:14:22Z",
                        "webhook_secret": "the plaintext signing secret, returned by this call only"
                      }
                    }
                  },
                  "updated_without_secret": {
                    "summary": "Update -- the secret key is absent, not null",
                    "description": "An update mints nothing, so `webhook_secret` is omitted from the body entirely. A null would wrongly read as 'this endpoint has no secret'.",
                    "value": {
                      "status": "success",
                      "data": {
                        "id": "018f3c1e-6b2a-7d48-9f3c-1e6b2a7d4f80",
                        "event_type": 1,
                        "business_id": "018e2b0d-5a19-7c37-8e2b-0d5a197c3760",
                        "webhook_url": "https://partner.example.com/ospree/webhooks-v2",
                        "event_names": [
                          "INCOMING_TRANSFER"
                        ],
                        "ip_allowlist": [
                          "203.0.113.7"
                        ],
                        "created_by": "018d1a9c-4908-7b26-9d1a-9c49087b2650",
                        "created": "2026-08-31T09:14:22Z",
                        "modified_by": "018d1a9c-4908-7b26-9d1a-9c49087b2650",
                        "last_modified": "2026-08-31T11:02:05Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                },
                "example": {
                  "status": "fail",
                  "message": "Authentication required.",
                  "error_code": "auth.unauthenticated"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                },
                "example": {
                  "status": "fail",
                  "message": "Payment required.",
                  "error_code": "billing.payment_required"
                }
              }
            }
          },
          "409": {
            "description": "The event family holds more than one endpoint, so there is no single endpoint to update; or it already holds the maximum of 5",
            "content": {
              "application/json": {
                "examples": {
                  "ambiguous_family": {
                    "summary": "More than one endpoint in the family",
                    "description": "This upsert never picks one of several endpoints to overwrite. Address one by id instead: GET /webhooks then DELETE /webhooks/{id}.",
                    "value": {
                      "status": "fail",
                      "message": "Event family already holds more than one endpoint"
                    }
                  },
                  "family_at_capacity": {
                    "summary": "The family already holds 5 endpoints",
                    "value": {
                      "status": "fail",
                      "message": "The event family already holds the maximum of 5 live endpoints"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity - Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                },
                "example": {
                  "status": "fail",
                  "message": "Validation error",
                  "data": [
                    {
                      "loc": [
                        "body",
                        "event_names"
                      ],
                      "msg": "Field required",
                      "type": "missing"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                },
                "example": {
                  "status": "error",
                  "code": 500,
                  "message": "Something went wrong.",
                  "error_code": "internal_error"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v2/webhooks/{id}/secret/rotate": {
      "post": {
        "tags": [
          "Webhook Management"
        ],
        "summary": "Rotate Webhook Secret",
        "description": "Mint a replacement signing secret and hold it pending — phase one of two.\n\nThe response is the only time this secret is ever disclosed. Ospree keeps\nsigning deliveries with the current secret until the activate call below, so\nnothing breaks while the new value is deployed to the tenant's verifier.",
        "operationId": "rotate_webhook_secret_api_v2_webhooks__id__secret_rotate_post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A replacement secret was minted and is pending activation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiSuccessResponse_WebhookSecretRotation_"
                },
                "example": {
                  "status": "success",
                  "data": {
                    "id": "018f3c1e-6b2a-7d48-9f3c-1e6b2a7d4f80",
                    "webhook_secret": "the replacement signing secret, returned by this call only",
                    "expires": "2026-09-30T09:00:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Authentication required.",
                  "error_code": "auth.unauthenticated"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Payment required.",
                  "error_code": "billing.payment_required"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Webhook configuration not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "example": {
                  "status": "error",
                  "code": 500,
                  "message": "Something went wrong.",
                  "error_code": "internal_error"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/webhooks/{id}/secret/activate": {
      "post": {
        "tags": [
          "Webhook Management"
        ],
        "summary": "Activate Webhook Secret",
        "description": "Promote the pending signing secret to active — phase two, the cutover.\n\nCarries no secret material: the pending value was disclosed by the rotate that\nminted it and by nothing else.",
        "operationId": "activate_webhook_secret_api_v2_webhooks__id__secret_activate_post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The pending secret is now the active signing secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiSuccessResponse_WebhookConfiguration_"
                },
                "example": {
                  "status": "success",
                  "data": {
                    "id": "018f3c1e-6b2a-7d48-9f3c-1e6b2a7d4f80",
                    "event_type": 1,
                    "business_id": "018e2b0d-5a19-7c37-8e2b-0d5a197c3760",
                    "webhook_url": "https://partner.example.com/ospree/webhooks",
                    "event_names": [
                      "INCOMING_TRANSFER",
                      "TRANSFER_COMPLETED"
                    ],
                    "ip_allowlist": [
                      "203.0.113.7"
                    ],
                    "created_by": "018d1a9c-4908-7b26-9d1a-9c49087b2650",
                    "created": "2026-08-31T09:14:22Z",
                    "modified_by": "018d1a9c-4908-7b26-9d1a-9c49087b2650",
                    "last_modified": "2026-08-31T12:40:11Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Authentication required.",
                  "error_code": "auth.unauthenticated"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Payment required.",
                  "error_code": "billing.payment_required"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "404": {
            "description": "No endpoint with this id, or it has no pending secret",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "No pending secret to activate"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "example": {
                  "status": "error",
                  "code": 500,
                  "message": "Something went wrong.",
                  "error_code": "internal_error"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/webhooks/event_types": {
      "get": {
        "tags": [
          "Webhook Management"
        ],
        "summary": "List Webhook Event Types",
        "operationId": "list_webhook_event_types_api_v2_webhooks_event_types_get",
        "responses": {
          "200": {
            "description": "The event families a webhook can be registered for.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiSuccessResponse_List_WebhookEventTypeResponse__"
                },
                "example": {
                  "status": "success",
                  "data": [
                    {
                      "event_type_name": "travel_rule",
                      "event_type_id": "1"
                    },
                    {
                      "event_type_name": "address_verification",
                      "event_type_id": "3"
                    },
                    {
                      "event_type_name": "compliance",
                      "event_type_id": "4"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                },
                "example": {
                  "status": "fail",
                  "message": "Authentication required.",
                  "error_code": "auth.unauthenticated"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                },
                "example": {
                  "status": "fail",
                  "message": "Payment required.",
                  "error_code": "billing.payment_required"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                },
                "example": {
                  "status": "error",
                  "code": 500,
                  "message": "Something went wrong.",
                  "error_code": "internal_error"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v2/webhooks/{id}": {
      "delete": {
        "tags": [
          "Webhook Management"
        ],
        "summary": "Delete Webhook",
        "operationId": "delete_webhook_api_v2_webhooks__id__delete",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The endpoint was deleted. `data` is the outcome flag.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiSuccessResponse_bool_"
                },
                "example": {
                  "status": "success",
                  "data": true
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Authentication required.",
                  "error_code": "auth.unauthenticated"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Payment required.",
                  "error_code": "billing.payment_required"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Webhook configuration not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "example": {
                  "status": "error",
                  "code": 500,
                  "message": "Something went wrong.",
                  "error_code": "internal_error"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/webhooks/requests": {
      "get": {
        "tags": [
          "Webhook Requests"
        ],
        "summary": "List Webhook Requests",
        "description": "List webhook requests for a business.",
        "operationId": "list_webhook_requests_api_v2_webhooks_requests_get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "event_type",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Event Type"
            }
          },
          {
            "name": "event_name",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Event Name"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1,
              "title": "Page"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10,
              "title": "Size"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of delivery attempts, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiSuccessResponse_Page_WebhookRequestResponse__"
                },
                "examples": {
                  "delivered_and_failed": {
                    "summary": "One delivered attempt, and one delivery retried once",
                    "description": "`status` is the HTTP status the destination returned, or null when the request never got one (a transport error). `error_details` carries the recorded failure.\n\nThe last two items are **one** delivery attempted twice, not two deliveries: attempts of one delivery repeat the `request_id` the destination was sent, and `execution_count` orders them (0 is the first attempt). Group on `request_id` to count how many times an event was tried.",
                    "value": {
                      "status": "success",
                      "data": {
                        "items": [
                          {
                            "id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5900",
                            "request_id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5a00",
                            "execution_count": 0,
                            "webhook_url": "https://partner.example.com/ospree/webhooks",
                            "event_type": "travel_rule",
                            "event_name": "INCOMING_TRANSFER",
                            "status": 200,
                            "timestamp": "2026-08-31T11:14:02Z",
                            "request_payload": {
                              "event_name": "INCOMING_TRANSFER",
                              "data": {
                                "transfer_id": "067fd57d-efe3-7eab-8000-04d8f072e418"
                              }
                            }
                          },
                          {
                            "id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5901",
                            "request_id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5a01",
                            "execution_count": 1,
                            "webhook_url": "https://partner.example.com/ospree/webhooks",
                            "event_type": "travel_rule",
                            "event_name": "TRANSFER_COMPLETED",
                            "status": 401,
                            "timestamp": "2026-08-31T11:12:44Z",
                            "request_payload": {
                              "event_name": "TRANSFER_COMPLETED",
                              "data": {
                                "transfer_id": "067fd57d-efe3-7eab-8000-04d8f072e418"
                              }
                            },
                            "error_details": {
                              "reason": "Destination rejected the request",
                              "body": "signature verification failed"
                            }
                          },
                          {
                            "id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5903",
                            "request_id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5a01",
                            "execution_count": 0,
                            "webhook_url": "https://partner.example.com/ospree/webhooks",
                            "event_type": "travel_rule",
                            "event_name": "TRANSFER_COMPLETED",
                            "status": 500,
                            "timestamp": "2026-08-31T11:12:39Z",
                            "request_payload": {
                              "event_name": "TRANSFER_COMPLETED",
                              "data": {
                                "transfer_id": "067fd57d-efe3-7eab-8000-04d8f072e418"
                              }
                            },
                            "error_details": {
                              "reason": "Destination returned HTTP 500",
                              "body": "internal error"
                            }
                          }
                        ],
                        "total": 3,
                        "page": 1,
                        "size": 50,
                        "pages": 1
                      }
                    }
                  },
                  "empty_page": {
                    "summary": "No delivery attempts match",
                    "value": {
                      "status": "success",
                      "data": {
                        "items": [],
                        "total": 0,
                        "page": 1,
                        "size": 50,
                        "pages": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Authentication required.",
                  "error_code": "auth.unauthenticated"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Payment required.",
                  "error_code": "billing.payment_required"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity - Validation Error",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Validation error",
                  "data": [
                    {
                      "loc": [
                        "query",
                        "event_type"
                      ],
                      "msg": "Field required",
                      "type": "missing"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "example": {
                  "status": "error",
                  "code": 500,
                  "message": "Something went wrong.",
                  "error_code": "internal_error"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/webhooks/requests/{request_id}": {
      "post": {
        "tags": [
          "Webhook Requests"
        ],
        "summary": "Retry Webhook Request",
        "description": "Retry a previously attempted webhook delivery.\n\nRe-sends the recorded payload to the endpoint's current URL and records a\nfresh delivery attempt, which is what the response carries. The original\nattempt is left in the log untouched.",
        "operationId": "retry_webhook_request_api_v2_webhooks_requests__request_id__post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Request Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The delivery was retried. The body is the new attempt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiSuccessResponse_WebhookRequestResponse_"
                },
                "examples": {
                  "retry_succeeded": {
                    "summary": "The retry was accepted by the destination",
                    "description": "The new attempt carries the same `request_id` the original was sent, so it is grouped with it. `execution_count` carries no value: it counts the delivery queue's own attempts, and this one you asked for.",
                    "value": {
                      "status": "success",
                      "data": {
                        "id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5904",
                        "request_id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5a01",
                        "webhook_url": "https://partner.example.com/ospree/webhooks",
                        "event_type": "travel_rule",
                        "event_name": "TRANSFER_COMPLETED",
                        "status": 200,
                        "timestamp": "2026-08-31T11:59:07Z",
                        "request_payload": {
                          "event_name": "TRANSFER_COMPLETED",
                          "data": {
                            "transfer_id": "067fd57d-efe3-7eab-8000-04d8f072e418"
                          }
                        }
                      }
                    }
                  },
                  "retry_failed_again": {
                    "summary": "The retry failed again",
                    "description": "A retry records a fresh attempt whether or not it succeeds.",
                    "value": {
                      "status": "success",
                      "data": {
                        "id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5902",
                        "request_id": "018f4d2f-7c3b-7e59-8f4d-2f7c3b7e5a01",
                        "webhook_url": "https://partner.example.com/ospree/webhooks",
                        "event_type": "travel_rule",
                        "event_name": "TRANSFER_COMPLETED",
                        "timestamp": "2026-08-31T12:01:19Z",
                        "request_payload": {
                          "event_name": "TRANSFER_COMPLETED",
                          "data": {
                            "transfer_id": "067fd57d-efe3-7eab-8000-04d8f072e418"
                          }
                        },
                        "error_details": {
                          "reason": "Connection timed out after 10s"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Authentication required.",
                  "error_code": "auth.unauthenticated"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Payment required.",
                  "error_code": "billing.payment_required"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "404": {
            "description": "No delivery attempt with this id",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Webhook request not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity - Validation Error",
            "content": {
              "application/json": {
                "example": {
                  "status": "fail",
                  "message": "Validation error",
                  "data": [
                    {
                      "loc": [
                        "path",
                        "request_id"
                      ],
                      "msg": "Field required",
                      "type": "missing"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiFailResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "example": {
                  "status": "error",
                  "code": 500,
                  "message": "Something went wrong.",
                  "error_code": "internal_error"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ApiErrorResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "default": "error"
          },
          "code": {
            "type": "integer",
            "title": "Code",
            "description": "Duplicates the HTTP status; use `error_code`. Removed in v3."
          },
          "message": {
            "type": "string",
            "title": "Message"
          },
          "error_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Code",
            "description": "Stable, machine-readable error code, e.g. `auth.unauthenticated`. Additive-only."
          },
          "data": {
            "anyOf": [
              {},
              {
                "type": "null"
              }
            ],
            "title": "Data"
          }
        },
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "title": "ApiErrorResponse",
        "description": "*summary*: Api error response model"
      },
      "ApiFailResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "default": "fail"
          },
          "message": {
            "type": "string",
            "title": "Message"
          },
          "error_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Code",
            "description": "Stable, machine-readable error code, e.g. `auth.unauthenticated`. Additive-only."
          },
          "data": {
            "anyOf": [
              {},
              {
                "type": "null"
              }
            ],
            "title": "Data"
          }
        },
        "type": "object",
        "required": [
          "message"
        ],
        "title": "ApiFailResponse",
        "description": "*summary*: Api fail response model"
      },
      "ApiSuccessResponse_List_WebhookConfigurationView__": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "default": "success"
          },
          "data": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/WebhookConfigurationView"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Data"
          }
        },
        "type": "object",
        "title": "ApiSuccessResponse[List[WebhookConfigurationView]]"
      },
      "ApiSuccessResponse_List_WebhookEventTypeResponse__": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "default": "success"
          },
          "data": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/WebhookEventTypeResponse"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Data"
          }
        },
        "type": "object",
        "title": "ApiSuccessResponse[List[WebhookEventTypeResponse]]"
      },
      "ApiSuccessResponse_Page_WebhookRequestResponse__": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "default": "success"
          },
          "data": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Page_WebhookRequestResponse_"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "title": "ApiSuccessResponse[Page[WebhookRequestResponse]]"
      },
      "ApiSuccessResponse_WebhookConfiguration_": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "default": "success"
          },
          "data": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/WebhookConfiguration"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "title": "ApiSuccessResponse[WebhookConfiguration]"
      },
      "ApiSuccessResponse_WebhookRequestResponse_": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "default": "success"
          },
          "data": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/WebhookRequestResponse"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "title": "ApiSuccessResponse[WebhookRequestResponse]"
      },
      "ApiSuccessResponse_WebhookSecretRotation_": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "default": "success"
          },
          "data": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/WebhookSecretRotation"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "title": "ApiSuccessResponse[WebhookSecretRotation]"
      },
      "ApiSuccessResponse_bool_": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "default": "success"
          },
          "data": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Data"
          }
        },
        "type": "object",
        "title": "ApiSuccessResponse[bool]"
      },
      "ConfigureWebhookRequest": {
        "properties": {
          "webhook_url": {
            "type": "string",
            "title": "Webhook Url"
          },
          "event_names": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Event Names"
          },
          "ip_allowlist": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ip Allowlist"
          },
          "event_type": {
            "type": "integer",
            "title": "Event Type"
          }
        },
        "type": "object",
        "required": [
          "webhook_url",
          "event_type"
        ],
        "title": "ConfigureWebhookRequest",
        "description": "Configure this business's webhook endpoint for one event family. The call is an upsert and carries no endpoint id: it updates the endpoint the family already has, and creates one when it has none. A business may hold up to 5 endpoints per event family, but this route never creates a second one — when the family already holds more than one endpoint it answers 409 rather than guessing which to overwrite. Use GET /webhooks to list them and DELETE /webhooks/{id} to address one by id.",
        "example": {
          "event_names": [
            "INCOMING_TRANSFER",
            "TRANSFER_COMPLETED"
          ],
          "event_type": 1,
          "ip_allowlist": [
            "00.00.00.xx",
            "00.00.00.xx"
          ],
          "webhook_url": "https://example.com/webhook"
        }
      },
      "Page_WebhookRequestResponse_": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/WebhookRequestResponse"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "title": "Total"
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "title": "Page"
          },
          "size": {
            "type": "integer",
            "minimum": 1,
            "title": "Size"
          },
          "pages": {
            "type": "integer",
            "minimum": 0,
            "title": "Pages"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total",
          "page",
          "size",
          "pages"
        ],
        "title": "Page[WebhookRequestResponse]"
      },
      "WebhookConfiguration": {
        "properties": {
          "webhook_url": {
            "type": "string",
            "title": "Webhook Url"
          },
          "event_names": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Event Names"
          },
          "ip_allowlist": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ip Allowlist"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "event_type": {
            "type": "integer",
            "title": "Event Type"
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "title": "Business Id"
          },
          "created_by": {
            "type": "string",
            "format": "uuid",
            "title": "Created By"
          },
          "created": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created"
          },
          "modified_by": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Modified By"
          },
          "last_modified": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Modified"
          },
          "webhook_secret": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Webhook Secret"
          }
        },
        "type": "object",
        "required": [
          "webhook_url",
          "id",
          "event_type",
          "business_id",
          "created_by"
        ],
        "title": "WebhookConfiguration",
        "description": "What the upsert hands back — with the signing secret only when it minted one.\n\nThe plaintext secret is returned by exactly the call that minted it and by no\nother (ADR-0034 §2, issue #735): a first create, or the re-mint of a config\nthat had none. An ordinary update carries no secret material at all, and\n\"no secret material\" is the *absence* of the field rather than a null: a\n``webhook_secret: null`` in the body reads as \"this config has no secret\",\nwhich is false and would send an integration looking for the bug in the wrong\nplace."
      },
      "WebhookConfigurationView": {
        "properties": {
          "webhook_url": {
            "type": "string",
            "title": "Webhook Url"
          },
          "event_names": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Event Names"
          },
          "ip_allowlist": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ip Allowlist"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "title": "Business Id"
          },
          "event_type": {
            "type": "string",
            "title": "Event Type"
          },
          "modified_by": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Modified By"
          },
          "last_modified": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Modified"
          },
          "webhook_desc": {
            "type": "string",
            "title": "Webhook Desc"
          }
        },
        "type": "object",
        "required": [
          "webhook_url",
          "id",
          "business_id",
          "event_type",
          "webhook_desc"
        ],
        "title": "WebhookConfigurationView"
      },
      "WebhookEventTypeResponse": {
        "properties": {
          "event_type_name": {
            "type": "string",
            "title": "Event Type Name"
          },
          "event_type_id": {
            "type": "string",
            "title": "Event Type Id"
          }
        },
        "type": "object",
        "required": [
          "event_type_name",
          "event_type_id"
        ],
        "title": "WebhookEventTypeResponse",
        "example": {
          "event_type_id": "1",
          "event_type_name": "travel_rule"
        }
      },
      "WebhookRequestResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "request_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Request Id"
          },
          "execution_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Execution Count"
          },
          "webhook_url": {
            "type": "string",
            "title": "Webhook Url"
          },
          "event_type": {
            "type": "string",
            "title": "Event Type"
          },
          "event_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Event Name"
          },
          "status": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Status"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "title": "Timestamp"
          },
          "request_payload": {
            "anyOf": [
              {},
              {
                "type": "null"
              }
            ],
            "title": "Request Payload"
          },
          "error_details": {
            "anyOf": [
              {},
              {
                "type": "null"
              }
            ],
            "title": "Error Details"
          }
        },
        "type": "object",
        "required": [
          "id",
          "webhook_url",
          "event_type",
          "timestamp"
        ],
        "title": "WebhookRequestResponse",
        "description": "The commercial delivery-attempt response — the status projection plus the payload.\n\nThe two additions are what makes this a PII-bearing read (see\n``docs/audit/adoption-roster.toml``): an integration token holder gets the\nbody Ospree delivered and the failure detail recorded against it."
      },
      "WebhookSecretRotation": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "webhook_secret": {
            "type": "string",
            "title": "Webhook Secret"
          },
          "expires": {
            "type": "string",
            "format": "date-time",
            "title": "Expires"
          }
        },
        "type": "object",
        "required": [
          "id",
          "webhook_secret",
          "expires"
        ],
        "title": "WebhookSecretRotation",
        "description": "The rotate response — the one and only disclosure of the new secret.\n\nIts own model rather than a reuse of :class:`WebhookConfiguration`, because\nthe two say different things: a configuration *may* carry a secret, a rotation\nalways does. Storing it is the caller's only chance; the value is never\nreturned again by any route, including a repeat of the rotate call (which\nmints a further replacement instead).\n\nThe secret is **pending**, not active: Ospree keeps signing with the current\nsecret until the tenant calls activate, so the two never race. ``expires`` is\nwhen the pending secret stops being promotable.",
        "example": {
          "expires": "2026-08-20T09:00:00Z",
          "id": "018f3c1e-6b2a-7d48-9f3c-1e6b2a7d4f80",
          "webhook_secret": "the plaintext signing secret, returned by this call only"
        }
      }
    },
    "securitySchemes": {
      "HTTPBearer": {
        "type": "http",
        "scheme": "bearer"
      }
    }
  },
  "tags": [
    {
      "name": "Webhook Management",
      "description": "Configure webhook endpoints and their event subscriptions."
    },
    {
      "name": "Webhook Requests",
      "description": "Inspect and retry webhook delivery attempts."
    }
  ],
  "servers": [
    {
      "url": "https://dev-api.ospree.io",
      "description": "Development"
    }
  ]
}
