Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,17 @@ var history = await client.Insights.GetMessageHistoryAsync(new GetMessageHistory
});
```

Fetch message analytics:

```csharp
var analytics = await client.Insights.GetMessageAnalyticsAsync(new GetMessageAnalyticsRequest
{
PhoneNumber = "2348012345678",
DateFrom = "2026-06-01",
DateTo = "2026-06-13"
});
```

## Error Handling

The SDK throws `TermiiApiException` for non-success HTTP responses from Termii:
Expand Down Expand Up @@ -247,15 +258,14 @@ Implemented in the current SDK:
- Sender IDs: list and request sender IDs.
- Number API: send message through a dedicated Termii number.
- Tokens: send, verify, generate, voice, email, and WhatsApp OTP flows.
- Insights: balance, DND status, number intelligence, and message history.
- Insights: balance, DND status, number intelligence, message history, and message analytics.

Deferred or not yet implemented:

- WhatsApp template/device message APIs.
- Campaign phonebook APIs.
- Product notification email APIs.
- Webhook event models.
- `/api/sms/history/analytics`, pending additional verification.

See [docs/API_COVERAGE.md](docs/API_COVERAGE.md) for the detailed coverage matrix.

Expand Down
4 changes: 2 additions & 2 deletions docs/API_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The Termii docs describe a REST/JSON API and state that each account has its own
| Insights | Search DND/number status | GET | `/api/check/dnd` | Query | `TermiiClient.Insights` | Implemented | #9, PR #27 | Unit tests added |
| Insights | Query number intelligence/status | GET | `/api/insight/number/query` | Query | `TermiiClient.Insights` | Implemented | #9, PR #27 | Unit tests added |
| Insights | Fetch message inbox/history | GET | `/api/sms/inbox` | Query | `TermiiClient.Insights` | Implemented | #9, PR #27 | Unit tests added |
| Insights | Fetch message analytics/history | GET | `/api/sms/history/analytics` | Query | `TermiiClient.Insights` | Needs verification | #9 | Planned unit + optional integration |
| Insights | Fetch message analytics/history | GET | `/api/sms/history/analytics` | Query | `TermiiClient.Insights` | In progress | #34 | Unit tests added |

## Deferred Coverage

Expand All @@ -82,7 +82,7 @@ The provided Postman collection currently lists these groups:
| --- | --- | --- |
| Switch | SenderId, Numbers, Send | Covered by #3, #5, and #7. |
| Token | Send Token, Verify Token, In-App Token | Covered by #4. |
| Insight | history analytics, Search, Balance, Inbox API | Covered by #9, with `/api/sms/history/analytics` marked for verification against live behavior/docs. |
| Insight | history analytics, Search, Balance, Inbox API | Covered by #9 and #34. |

## Implementation Rules

Expand Down
23 changes: 23 additions & 0 deletions src/Termii/Insights/GetMessageAnalyticsRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Termii;

public sealed class GetMessageAnalyticsRequest
{
public string? MessageId { get; set; }

public string? DateFrom { get; set; }

public string? DateTo { get; set; }

public string? PhoneNumber { get; set; }

internal string ToPath()
{
var query = new List<string>();
TermiiInsightsQueryString.AddIfPresent(query, "message_id", MessageId);
TermiiInsightsQueryString.AddIfPresent(query, "date_from", DateFrom);
TermiiInsightsQueryString.AddIfPresent(query, "date_to", DateTo);
TermiiInsightsQueryString.AddIfPresent(query, "phone_number", PhoneNumber);

return TermiiInsightsQueryString.Append("/api/sms/history/analytics", query);
}
}
4 changes: 4 additions & 0 deletions src/Termii/Insights/ITermiiInsightsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ Task<QueryNumberResponse> QueryNumberAsync(
Task<MessageHistoryResponse> GetMessageHistoryAsync(
GetMessageHistoryRequest? request = null,
CancellationToken cancellationToken = default);

Task<MessageAnalyticsResponse> GetMessageAnalyticsAsync(
GetMessageAnalyticsRequest? request = null,
CancellationToken cancellationToken = default);
}
25 changes: 25 additions & 0 deletions src/Termii/Insights/MessageAnalyticsResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Termii;

public sealed class MessageAnalyticsResponse
{
[JsonPropertyName("sent")]
public int? Sent { get; set; }

[JsonPropertyName("delivered")]
public int? Delivered { get; set; }

[JsonPropertyName("failed")]
public int? Failed { get; set; }

[JsonPropertyName("pending")]
public int? Pending { get; set; }

[JsonPropertyName("rejected")]
public int? Rejected { get; set; }

[JsonExtensionData]
public Dictionary<string, JsonElement>? AdditionalData { get; set; }
}
12 changes: 12 additions & 0 deletions src/Termii/Insights/TermiiInsightsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,16 @@ public Task<MessageHistoryResponse> GetMessageHistoryAsync(
TermiiAuthenticationLocation.Query,
cancellationToken);
}

public Task<MessageAnalyticsResponse> GetMessageAnalyticsAsync(
GetMessageAnalyticsRequest? request = null,
CancellationToken cancellationToken = default)
{
return _pipeline.SendJsonAsync<MessageAnalyticsResponse>(
HttpMethod.Get,
(request ?? new GetMessageAnalyticsRequest()).ToPath(),
body: null,
TermiiAuthenticationLocation.Query,
cancellationToken);
}
}
32 changes: 32 additions & 0 deletions tests/Termii.Tests/TermiiInsightsClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,38 @@ public async Task GetMessageHistoryAsyncAddsFiltersAndDeserializesContent()
Assert.Equal("sent", message.Status);
}

[Fact]
public async Task GetMessageAnalyticsAsyncAddsFiltersAndDeserializesFlexibleResponse()
{
using var handler = new TestHttpMessageHandler(
HttpStatusCode.OK,
"""{"sent":12,"delivered":10,"failed":1,"pending":1,"unknown_metric":5}""");
var client = TestTermiiClientFactory.Create(handler);

var response = await client.Insights.GetMessageAnalyticsAsync(
new GetMessageAnalyticsRequest
{
MessageId = "msg-123",
DateFrom = "2026-06-01",
DateTo = "2026-06-13",
PhoneNumber = "2348012345678",
},
CancellationToken.None);

var request = handler.LastRequest;
Assert.NotNull(request);

Assert.Equal(
"https://example.test/api/sms/history/analytics?message_id=msg-123&date_from=2026-06-01&date_to=2026-06-13&phone_number=2348012345678&api_key=test-api-key",
request.RequestUri!.AbsoluteUri);
Assert.Equal(12, response.Sent);
Assert.Equal(10, response.Delivered);
Assert.Equal(1, response.Failed);
Assert.Equal(1, response.Pending);
Assert.NotNull(response.AdditionalData);
Assert.Equal(5, response.AdditionalData["unknown_metric"].GetInt32());
}

[Fact]
public async Task CheckDndAsyncRejectsMissingPhoneNumber()
{
Expand Down
Loading