diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01c48bc1..e8d9c9bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org).
- Get Event Subscription by ID: Retrieve detailed information about specific event subscriptions
- Delete Event Subscription: Remove event subscriptions programmatically
- Get Callbacks History: Comprehensive webhook callback event history with advanced filtering and sorting capabilities
+ - Get Callbacks History by ID: webhook callback event history related to subscription ID with advanced filtering and sorting capabilities
- Document Template Operations:
- Template Routing Management: Create, retrieve, and update routing configurations for document templates
- Bulk Invite from Template: Send signing invitations to multiple recipients using templates
diff --git a/SignNow.Net.Examples/Webhooks/GetCallbacksBySubscriptionIdExample.cs b/SignNow.Net.Examples/Webhooks/GetCallbacksBySubscriptionIdExample.cs
new file mode 100644
index 00000000..90375efb
--- /dev/null
+++ b/SignNow.Net.Examples/Webhooks/GetCallbacksBySubscriptionIdExample.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SignNow.Net.Model.Requests;
+using SignNow.Net.Model.Requests.GetFolderQuery;
+
+namespace SignNow.Net.Examples
+{
+ [TestClass]
+ public class GetCallbacksBySubscriptionIdExample : ExamplesBase
+ {
+ ///
+ /// Demonstrates how to get webhook callback events for a specific event subscription.
+ /// This example shows how to retrieve callback history filtered by subscription ID and analyze webhook delivery results.
+ ///
+ ///
+ [TestMethod]
+ public async Task GetCallbacksBySubscriptionIdAsync()
+ {
+ var subscriptions = await testContext.Events
+ .GetEventSubscriptionsListAsync()
+ .ConfigureAwait(false);
+
+ if (subscriptions?.Data == null || subscriptions.Data.Count == 0)
+ {
+ Console.WriteLine("No event subscriptions found. Please create an event subscription first.");
+ return;
+ }
+
+ var subscriptionId = subscriptions.Data.First().Id;
+ Console.WriteLine($"Using subscription ID: {subscriptionId}");
+
+ // Example 1: Get all callbacks for a specific subscription with default options
+ Console.WriteLine("\n=== Example 1: Get all callbacks for specific subscription ===");
+ var allCallbacks = await testContext.Events
+ .GetCallbacksBySubscriptionIdAsync(subscriptionId)
+ .ConfigureAwait(false);
+
+ Console.WriteLine($"Total callbacks found for subscription {subscriptionId}: {allCallbacks.Data.Count}");
+ Console.WriteLine($"Current page: {allCallbacks.Meta.Pagination.CurrentPage}");
+ Console.WriteLine($"Per page: {allCallbacks.Meta.Pagination.PerPage}");
+ Console.WriteLine($"Total pages: {allCallbacks.Meta.Pagination.TotalPages}");
+ Console.WriteLine($"Total items: {allCallbacks.Meta.Pagination.Total}");
+
+ // Example 2: Filter successful callbacks for the subscription
+ Console.WriteLine($"\n=== Example 2: Get successful callbacks for subscription {subscriptionId} ===");
+ var successfulCallbacks = await testContext.Events
+ .GetCallbacksBySubscriptionIdAsync(subscriptionId, new GetCallbacksOptions
+ {
+ Filters = f => f.Or(
+ f => f.Date.Between(DateTime.UtcNow.AddDays(-30), DateTime.UtcNow),
+ f => f.Code.Between(200, 299),
+ f => f.CallbackUrl.Like("example")
+ ),
+ Sortings = s => s.Code(SortOrder.Descending),
+ PerPage = 10
+ })
+ .ConfigureAwait(false);
+
+ Console.WriteLine($"Successful callbacks: {successfulCallbacks.Data.Count}");
+ foreach (var callback in successfulCallbacks.Data.Take(3))
+ {
+ Console.WriteLine($" ID: {callback.Id}");
+ Console.WriteLine($" Subscription ID: {callback.EventSubscriptionId}");
+ Console.WriteLine($" Status Code: {callback.ResponseStatusCode}");
+ Console.WriteLine($" Event: {callback.EventName}");
+ Console.WriteLine($" Entity ID: {callback.EntityId}");
+ Console.WriteLine($" Duration: {callback.Duration:F3}s");
+ Console.WriteLine($" Start Time: {callback.RequestStartTime}");
+ Console.WriteLine($" Callback Url: {callback.CallbackUrl}");
+ Console.WriteLine();
+ }
+ }
+ }
+}
diff --git a/SignNow.Net.Test/AcceptanceTests/GetCallbacksBySubscriptionIdServiceTest.cs b/SignNow.Net.Test/AcceptanceTests/GetCallbacksBySubscriptionIdServiceTest.cs
new file mode 100644
index 00000000..365a3f4f
--- /dev/null
+++ b/SignNow.Net.Test/AcceptanceTests/GetCallbacksBySubscriptionIdServiceTest.cs
@@ -0,0 +1,52 @@
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SignNow.Net.Model.Requests;
+using SignNow.Net.Model.Requests.GetFolderQuery;
+using UnitTests;
+
+namespace AcceptanceTests
+{
+ [TestClass]
+ public class GetCallbacksBySubscriptionIdServiceTest : AuthorizedApiTestBase
+ {
+ [TestMethod]
+ public async Task GetCallbacksBySubscriptionIdAsync_WithFilters_ReturnsFilteredResults()
+ {
+ var eventSubscriptions = await SignNowTestContext.Events
+ .GetEventSubscriptionsListAsync()
+ .ConfigureAwait(false);
+
+ if (eventSubscriptions?.Data == null || eventSubscriptions.Data.Count == 0)
+ {
+ Assert.Inconclusive("No event subscriptions available for testing");
+ return;
+ }
+
+ var subscriptionId = eventSubscriptions.Data[0].Id;
+
+ var options = new GetCallbacksOptions
+ {
+ Filters = f => f.Code.Between(200, 599),
+ Sortings = s => s.StartTime(SortOrder.Descending),
+ PerPage = 15
+ };
+
+ var response = await SignNowTestContext.Events
+ .GetCallbacksBySubscriptionIdAsync(subscriptionId, options)
+ .ConfigureAwait(false);
+
+ Assert.IsNotNull(response);
+ Assert.IsNotNull(response.Data);
+ Assert.IsNotNull(response.Meta);
+ Assert.AreEqual(15, response.Meta.Pagination.PerPage);
+
+ // Verify all callbacks belong to the specified subscription and match filter criteria
+ foreach (var callback in response.Data)
+ {
+ Assert.AreEqual(subscriptionId, callback.EventSubscriptionId);
+ Assert.IsTrue(callback.ResponseStatusCode >= 200 && callback.ResponseStatusCode <= 299,
+ $"Callback status code {callback.ResponseStatusCode} should be in range 200-299");
+ }
+ }
+ }
+}
diff --git a/SignNow.Net.Test/UnitTests/Services/EventSubscriptionServiceTest.cs b/SignNow.Net.Test/UnitTests/Services/EventSubscriptionServiceTest.cs
index fb56fe51..3b9194b5 100644
--- a/SignNow.Net.Test/UnitTests/Services/EventSubscriptionServiceTest.cs
+++ b/SignNow.Net.Test/UnitTests/Services/EventSubscriptionServiceTest.cs
@@ -386,4 +386,102 @@ public async Task GetCallbacksAsync_WithNullRequestHeaders_ShouldHandleGracefull
Assert.IsNotNull(callback.RequestContent);
Assert.IsNotNull(callback.RequestContent.Content);
}
- }}
+
+ [TestMethod]
+ public async Task GetCallbacksBySubscriptionIdAsync_WithValidSubscriptionId_ShouldReturnCallbacks()
+ {
+ var subscriptionId = "8a49e32e267e42a18e4a3967669f347e06e3b71e";
+ var mockResponse = TestUtils.SerializeToJsonFormatted(new
+ {
+ data = new[]
+ {
+ new
+ {
+ id = "callback_123",
+ application_name = "TestApp",
+ entity_id = "doc_456",
+ event_subscription_id = subscriptionId,
+ event_subscription_active = true,
+ entity_type = "document",
+ event_name = "document.complete",
+ callback_url = "https://example.com/webhook",
+ request_method = "POST",
+ duration = 1.5,
+ request_start_time = 1609459200,
+ request_end_time = 1609459205,
+ request_headers = new
+ {
+ string_head = "test_value",
+ int_head = 42,
+ bool_head = true,
+ float_head = 3.14f
+ },
+ response_content = "OK",
+ response_status_code = 200,
+ event_subscription_owner_email = "owner@example.com",
+ request_content = new
+ {
+ meta = new
+ {
+ timestamp = 1609459200,
+ @event = "document.complete",
+ environment = "https://api.signnow.com/",
+ initiator_id = "user_789",
+ callback_url = "https://example.com/webhook",
+ access_token = "***masked***"
+ },
+ content = new
+ {
+ document_id = "doc_456",
+ document_name = "Test Document.pdf",
+ user_id = "user_789",
+ initiator_id = "user_789",
+ initiator_email = "initiator@example.com"
+ }
+ }
+ }
+ },
+ meta = new
+ {
+ pagination = new
+ {
+ total = 1,
+ count = 1,
+ per_page = 50,
+ current_page = 1,
+ total_pages = 1
+ }
+ }
+ });
+
+ var service = new EventSubscriptionService(ApiBaseUrl, new Token(), SignNowClientMock(mockResponse));
+ var options = new GetCallbacksOptions
+ {
+ Page = 1,
+ PerPage = 50
+ };
+
+ var response = await service.GetCallbacksBySubscriptionIdAsync(subscriptionId, options).ConfigureAwait(false);
+
+ Assert.IsNotNull(response);
+ Assert.IsNotNull(response.Data);
+ Assert.IsNotNull(response.Meta);
+ Assert.AreEqual(1, response.Data.Count);
+
+ var callback = response.Data[0];
+ Assert.AreEqual("callback_123", callback.Id);
+ Assert.AreEqual("TestApp", callback.ApplicationName);
+ Assert.AreEqual("doc_456", callback.EntityId);
+ Assert.AreEqual(subscriptionId, callback.EventSubscriptionId);
+ Assert.IsTrue(callback.EventSubscriptionActive);
+ Assert.AreEqual(EventSubscriptionEntityType.Document, callback.EntityType);
+ Assert.AreEqual(EventType.DocumentComplete, callback.EventName);
+ Assert.AreEqual("https://example.com/webhook", callback.CallbackUrl.ToString());
+ Assert.AreEqual(200, callback.ResponseStatusCode);
+
+ Assert.AreEqual(1, response.Meta.Pagination.Total);
+ Assert.AreEqual(1, response.Meta.Pagination.Count);
+ Assert.AreEqual(50, response.Meta.Pagination.PerPage);
+ }
+ }
+}
diff --git a/SignNow.Net/Interfaces/IEventSubscriptionService.cs b/SignNow.Net/Interfaces/IEventSubscriptionService.cs
index 37cd1fc1..4533f1c1 100644
--- a/SignNow.Net/Interfaces/IEventSubscriptionService.cs
+++ b/SignNow.Net/Interfaces/IEventSubscriptionService.cs
@@ -97,5 +97,16 @@ public interface IEventSubscriptionService
/// Propagates notification that operations should be canceled.
/// List of callback events with metadata
Task GetCallbacksAsync(GetCallbacksOptions options = default, CancellationToken cancellationToken = default);
+
+ ///
+ /// Allows users to get the list of webhook events (events history) by the subscription ID.
+ /// The results can be filtered and sorted. If the sort parameter is not indicated,
+ /// the results are sorted by the start_time in descending order.
+ ///
+ /// ID of the subscription
+ /// Options for filtering and sorting callbacks
+ /// Propagates notification that operations should be canceled.
+ /// List of callback events for the specified subscription with metadata
+ Task GetCallbacksBySubscriptionIdAsync(string subscriptionId, GetCallbacksOptions options = default, CancellationToken cancellationToken = default);
}
}
diff --git a/SignNow.Net/Service/EventSubscriptionService.cs b/SignNow.Net/Service/EventSubscriptionService.cs
index f793ddd6..7668ff2b 100644
--- a/SignNow.Net/Service/EventSubscriptionService.cs
+++ b/SignNow.Net/Service/EventSubscriptionService.cs
@@ -197,5 +197,26 @@ public async Task GetCallbacksAsync(GetCallbacksOptions optio
.RequestAsync(requestOptions, cancellationToken)
.ConfigureAwait(false);
}
+
+ ///
+ public async Task GetCallbacksBySubscriptionIdAsync(string subscriptionId, GetCallbacksOptions options = default, CancellationToken cancellationToken = default)
+ {
+ Token.TokenType = TokenType.Bearer;
+
+ var query = options?.ToQueryString();
+ var filters = string.IsNullOrEmpty(query)
+ ? string.Empty
+ : $"?{query}";
+
+ var requestOptions = new GetHttpRequestOptions
+ {
+ RequestUrl = new Uri(ApiBaseUrl, $"/v2/event-subscriptions/{subscriptionId.ValidateId()}/callbacks{filters}"),
+ Token = Token
+ };
+
+ return await SignNowClient
+ .RequestAsync(requestOptions, cancellationToken)
+ .ConfigureAwait(false);
+ }
}
}