Skip to content

Commit 1d08d7d

Browse files
authored
Add Insights message analytics endpoint (#35)
1 parent d70d1b0 commit 1d08d7d

7 files changed

Lines changed: 110 additions & 4 deletions

File tree

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,17 @@ var history = await client.Insights.GetMessageHistoryAsync(new GetMessageHistory
214214
});
215215
```
216216

217+
Fetch message analytics:
218+
219+
```csharp
220+
var analytics = await client.Insights.GetMessageAnalyticsAsync(new GetMessageAnalyticsRequest
221+
{
222+
PhoneNumber = "2348012345678",
223+
DateFrom = "2026-06-01",
224+
DateTo = "2026-06-13"
225+
});
226+
```
227+
217228
## Error Handling
218229

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

252263
Deferred or not yet implemented:
253264

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

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

docs/API_COVERAGE.md

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

6262
## Deferred Coverage
6363

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

8787
## Implementation Rules
8888

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace Termii;
2+
3+
public sealed class GetMessageAnalyticsRequest
4+
{
5+
public string? MessageId { get; set; }
6+
7+
public string? DateFrom { get; set; }
8+
9+
public string? DateTo { get; set; }
10+
11+
public string? PhoneNumber { get; set; }
12+
13+
internal string ToPath()
14+
{
15+
var query = new List<string>();
16+
TermiiInsightsQueryString.AddIfPresent(query, "message_id", MessageId);
17+
TermiiInsightsQueryString.AddIfPresent(query, "date_from", DateFrom);
18+
TermiiInsightsQueryString.AddIfPresent(query, "date_to", DateTo);
19+
TermiiInsightsQueryString.AddIfPresent(query, "phone_number", PhoneNumber);
20+
21+
return TermiiInsightsQueryString.Append("/api/sms/history/analytics", query);
22+
}
23+
}

src/Termii/Insights/ITermiiInsightsClient.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ Task<QueryNumberResponse> QueryNumberAsync(
1515
Task<MessageHistoryResponse> GetMessageHistoryAsync(
1616
GetMessageHistoryRequest? request = null,
1717
CancellationToken cancellationToken = default);
18+
19+
Task<MessageAnalyticsResponse> GetMessageAnalyticsAsync(
20+
GetMessageAnalyticsRequest? request = null,
21+
CancellationToken cancellationToken = default);
1822
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Termii;
5+
6+
public sealed class MessageAnalyticsResponse
7+
{
8+
[JsonPropertyName("sent")]
9+
public int? Sent { get; set; }
10+
11+
[JsonPropertyName("delivered")]
12+
public int? Delivered { get; set; }
13+
14+
[JsonPropertyName("failed")]
15+
public int? Failed { get; set; }
16+
17+
[JsonPropertyName("pending")]
18+
public int? Pending { get; set; }
19+
20+
[JsonPropertyName("rejected")]
21+
public int? Rejected { get; set; }
22+
23+
[JsonExtensionData]
24+
public Dictionary<string, JsonElement>? AdditionalData { get; set; }
25+
}

src/Termii/Insights/TermiiInsightsClient.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,16 @@ public Task<MessageHistoryResponse> GetMessageHistoryAsync(
6464
TermiiAuthenticationLocation.Query,
6565
cancellationToken);
6666
}
67+
68+
public Task<MessageAnalyticsResponse> GetMessageAnalyticsAsync(
69+
GetMessageAnalyticsRequest? request = null,
70+
CancellationToken cancellationToken = default)
71+
{
72+
return _pipeline.SendJsonAsync<MessageAnalyticsResponse>(
73+
HttpMethod.Get,
74+
(request ?? new GetMessageAnalyticsRequest()).ToPath(),
75+
body: null,
76+
TermiiAuthenticationLocation.Query,
77+
cancellationToken);
78+
}
6779
}

tests/Termii.Tests/TermiiInsightsClientTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,38 @@ public async Task GetMessageHistoryAsyncAddsFiltersAndDeserializesContent()
142142
Assert.Equal("sent", message.Status);
143143
}
144144

145+
[Fact]
146+
public async Task GetMessageAnalyticsAsyncAddsFiltersAndDeserializesFlexibleResponse()
147+
{
148+
using var handler = new TestHttpMessageHandler(
149+
HttpStatusCode.OK,
150+
"""{"sent":12,"delivered":10,"failed":1,"pending":1,"unknown_metric":5}""");
151+
var client = TestTermiiClientFactory.Create(handler);
152+
153+
var response = await client.Insights.GetMessageAnalyticsAsync(
154+
new GetMessageAnalyticsRequest
155+
{
156+
MessageId = "msg-123",
157+
DateFrom = "2026-06-01",
158+
DateTo = "2026-06-13",
159+
PhoneNumber = "2348012345678",
160+
},
161+
CancellationToken.None);
162+
163+
var request = handler.LastRequest;
164+
Assert.NotNull(request);
165+
166+
Assert.Equal(
167+
"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",
168+
request.RequestUri!.AbsoluteUri);
169+
Assert.Equal(12, response.Sent);
170+
Assert.Equal(10, response.Delivered);
171+
Assert.Equal(1, response.Failed);
172+
Assert.Equal(1, response.Pending);
173+
Assert.NotNull(response.AdditionalData);
174+
Assert.Equal(5, response.AdditionalData["unknown_metric"].GetInt32());
175+
}
176+
145177
[Fact]
146178
public async Task CheckDndAsyncRejectsMissingPhoneNumber()
147179
{

0 commit comments

Comments
 (0)