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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ Then import the SDK namespace:
using Termii;
```

## Compatibility

The SDK targets `netstandard2.0` and `net8.0`.

- .NET Core 3.1 applications use the `netstandard2.0` asset.
- .NET 5, .NET 6, .NET 7, .NET 8, .NET 9, and .NET 10 applications can consume the package.
- Modern .NET applications use the most compatible asset NuGet selects for the application target.

## Configuration

Create a client manually:
Expand Down Expand Up @@ -225,6 +233,24 @@ var analytics = await client.Insights.GetMessageAnalyticsAsync(new GetMessageAna
});
```

## Webhooks

Termii can send delivery/report callbacks to an endpoint you own. The SDK includes receiver-side models that can be used with ASP.NET Core model binding:

```csharp
app.MapPost("/webhooks/termii", (TermiiWebhookEvent webhookEvent) =>
{
if (webhookEvent.Status == "delivered")
{
Console.WriteLine($"Delivered message {webhookEvent.MessageId}");
}

return Results.Ok();
});
```

Webhook payloads can vary by event type and Termii account configuration. Unknown fields are preserved in `TermiiWebhookEvent.AdditionalData`.

## Error Handling

The SDK throws `TermiiApiException` for non-success HTTP responses from Termii:
Expand Down Expand Up @@ -265,7 +291,6 @@ Deferred or not yet implemented:
- WhatsApp template/device message APIs.
- Campaign phonebook APIs.
- Product notification email APIs.
- Webhook event models.

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

Expand Down
2 changes: 1 addition & 1 deletion docs/API_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ The following documented APIs are useful but should come after the first SDK mil
| Campaigns | Update phonebook | PATCH | `/api/phonebooks/{phonebook_id}` | Deferred | Part of campaign/phonebook management. |
| Campaigns | Delete phonebook | DELETE | `/api/phonebooks/{phonebook_id}` | Deferred | Part of campaign/phonebook management. |
| Email | Send product notification email | POST | `/api/templates/send-email` | Deferred | Email notifications should be a separate milestone after SMS/token/insights. |
| Insights | Webhook events and reports | N/A | Consumer webhook endpoint | Deferred | This is SDK model/documentation support rather than an outbound Termii API call. |
| Insights | Webhook events and reports | N/A | Consumer webhook endpoint | Implemented | Receiver-side model support and README example covered by #32. |

## Postman Collection Reconciliation

Expand Down
4 changes: 2 additions & 2 deletions src/Termii/Termii.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<RootNamespace>Termii</RootNamespace>
<PackageId>Termii.SDK</PackageId>
<VersionPrefix>0.2.0</VersionPrefix>
<Description>A .NET SDK for the Termii messaging, token, and insights APIs.</Description>
<Description>A .NET SDK for the Termii messaging, token, and insights APIs, compatible with .NET Core 3.1 through .NET 10.</Description>
<PackageReleaseNotes>Add messaging, sender ID, Number API, Token API, and Insights API client support with unit and integration smoke coverage.</PackageReleaseNotes>
<PackageTags>termii;sms;otp;messaging;dotnet;sdk</PackageTags>
<PackageTags>termii;sms;otp;messaging;dotnet;sdk;netstandard2.0;netcoreapp3.1;net10</PackageTags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
Expand Down
70 changes: 70 additions & 0 deletions src/Termii/Webhooks/TermiiWebhookEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Termii;

public sealed class TermiiWebhookEvent
{
[JsonPropertyName("event")]
public string? Event { get; set; }

[JsonPropertyName("type")]
public string? Type { get; set; }

[JsonPropertyName("message_id")]
public string? MessageId { get; set; }

[JsonPropertyName("message_id_str")]
public string? MessageIdString { get; set; }

[JsonPropertyName("status")]
public string? Status { get; set; }

[JsonPropertyName("to")]
public string? To { get; set; }

[JsonPropertyName("from")]
public string? From { get; set; }

[JsonPropertyName("sender")]
public string? Sender { get; set; }

[JsonPropertyName("receiver")]
public string? Receiver { get; set; }

[JsonPropertyName("channel")]
public string? Channel { get; set; }

[JsonPropertyName("network")]
public string? Network { get; set; }

[JsonPropertyName("message")]
public string? Message { get; set; }

[JsonPropertyName("amount")]
public decimal? Amount { get; set; }

[JsonPropertyName("error_code")]
public string? ErrorCode { get; set; }

[JsonPropertyName("error_message")]
public string? ErrorMessage { get; set; }

[JsonPropertyName("created_at")]
public string? CreatedAt { get; set; }

[JsonPropertyName("updated_at")]
public string? UpdatedAt { get; set; }

[JsonPropertyName("sent_at")]
public string? SentAt { get; set; }

[JsonPropertyName("delivered_at")]
public string? DeliveredAt { get; set; }

[JsonPropertyName("done_date")]
public string? DoneDate { get; set; }

[JsonExtensionData]
public Dictionary<string, JsonElement>? AdditionalData { get; set; }
}
77 changes: 77 additions & 0 deletions tests/Termii.Tests/TermiiWebhookEventTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Text.Json;
using Termii;
using Xunit;

namespace Termii.Tests;

public sealed class TermiiWebhookEventTests
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web)
{
NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString,
};

[Fact]
public void CanDeserializeDeliveryReportPayload()
{
var webhookEvent = JsonSerializer.Deserialize<TermiiWebhookEvent>(
"""
{
"event": "message.status",
"message_id": "msg-123",
"message_id_str": "msg-123",
"status": "delivered",
"to": "2348012345678",
"from": "Termii",
"channel": "generic",
"network": "MTN",
"amount": "4.25",
"delivered_at": "2026-06-14 09:00:00",
"provider_reference": "provider-123"
}
""",
JsonSerializerOptions);

Assert.NotNull(webhookEvent);
Assert.Equal("message.status", webhookEvent.Event);
Assert.Equal("msg-123", webhookEvent.MessageId);
Assert.Equal("delivered", webhookEvent.Status);
Assert.Equal("2348012345678", webhookEvent.To);
Assert.Equal("Termii", webhookEvent.From);
Assert.Equal("generic", webhookEvent.Channel);
Assert.Equal("MTN", webhookEvent.Network);
Assert.Equal(4.25m, webhookEvent.Amount);
Assert.Equal("2026-06-14 09:00:00", webhookEvent.DeliveredAt);
Assert.NotNull(webhookEvent.AdditionalData);
Assert.Equal("provider-123", webhookEvent.AdditionalData["provider_reference"].GetString());
}

[Fact]
public void CanDeserializeFailedReportPayload()
{
var webhookEvent = JsonSerializer.Deserialize<TermiiWebhookEvent>(
"""
{
"type": "delivery_report",
"message_id": "msg-456",
"status": "failed",
"receiver": "2348012345678",
"sender": "Termii",
"error_code": "3001",
"error_message": "Insufficient balance",
"done_date": "2026-06-14 09:05:00"
}
""",
JsonSerializerOptions);

Assert.NotNull(webhookEvent);
Assert.Equal("delivery_report", webhookEvent.Type);
Assert.Equal("msg-456", webhookEvent.MessageId);
Assert.Equal("failed", webhookEvent.Status);
Assert.Equal("2348012345678", webhookEvent.Receiver);
Assert.Equal("Termii", webhookEvent.Sender);
Assert.Equal("3001", webhookEvent.ErrorCode);
Assert.Equal("Insufficient balance", webhookEvent.ErrorMessage);
Assert.Equal("2026-06-14 09:05:00", webhookEvent.DoneDate);
}
}
Loading