diff --git a/README.md b/README.md index fa5e987..fe908fd 100644 --- a/README.md +++ b/README.md @@ -2,39 +2,45 @@ [![CI](https://github.com/teesofttech/Termii.SDK/actions/workflows/ci.yml/badge.svg)](https://github.com/teesofttech/Termii.SDK/actions/workflows/ci.yml) -A .NET SDK for the Termii messaging, token, and insights APIs. +A .NET SDK for the Termii messaging, sender ID, Number API, Token API, and Insights APIs. -> This SDK is in early development. The first milestones establish the client, tests, examples, API coverage matrix, and package structure before endpoint support is added feature by feature. +## Installation -## Current Status - -The initial repository setup includes: - -- A `Termii` SDK project targeting `netstandard2.0` and `net8.0`. -- A lightweight unit test project. -- An examples project for developer-facing usage. -- An API coverage matrix in `docs/API_COVERAGE.md`. - -## Usage - -Install the package: +Install from NuGet: ```bash dotnet add package Termii.SDK ``` +Then import the SDK namespace: + ```csharp using Termii; +``` +## Configuration + +Create a client manually: + +```csharp var client = new TermiiClient(new TermiiOptions { ApiKey = "your-termii-api-key" }); ``` -The default base URL is `https://api.ng.termii.com`. +The default base URL is `https://api.ng.termii.com`. Override it only when your Termii account or environment requires a different base URL: + +```csharp +var client = new TermiiClient(new TermiiOptions +{ + ApiKey = "your-termii-api-key", + BaseUrl = new Uri("https://api.ng.termii.com"), + Timeout = TimeSpan.FromSeconds(30) +}); +``` -For ASP.NET Core applications, register the SDK with dependency injection: +For ASP.NET Core applications, register `TermiiClient` with dependency injection: ```csharp builder.Services.AddTermii(options => @@ -43,7 +49,35 @@ builder.Services.AddTermii(options => }); ``` -Send a message: +Use it from a service or endpoint: + +```csharp +public sealed class NotificationService +{ + private readonly TermiiClient _termii; + + public NotificationService(TermiiClient termii) + { + _termii = termii; + } + + public Task SendAsync(string phoneNumber, string message) + { + return _termii.Messaging.SendAsync(new SendMessageRequest + { + To = phoneNumber, + From = "Termii", + Sms = message, + Channel = TermiiMessageChannel.Generic, + Type = TermiiMessageType.Plain + }); + } +} +``` + +## Messaging + +Send a single SMS: ```csharp var response = await client.Messaging.SendAsync(new SendMessageRequest @@ -56,22 +90,50 @@ var response = await client.Messaging.SendAsync(new SendMessageRequest }); ``` -Fetch sender IDs: +Send a bulk SMS: ```csharp -var senderIds = await client.SenderIds.GetAsync(); +var response = await client.Messaging.SendBulkAsync(new SendBulkMessageRequest +{ + To = new[] { "2348012345678", "2348098765432" }, + From = "Termii", + Sms = "Hello everyone", + Channel = TermiiMessageChannel.Generic, + Type = TermiiMessageType.Plain +}); ``` Send through the Number API: ```csharp -var numberMessage = await client.Numbers.SendAsync(new SendNumberMessageRequest +var response = await client.Numbers.SendAsync(new SendNumberMessageRequest { To = "2348012345678", Sms = "Hello from a dedicated Termii number" }); ``` +## Sender IDs + +Fetch sender IDs: + +```csharp +var senderIds = await client.SenderIds.GetAsync(); +``` + +Request a sender ID: + +```csharp +var request = await client.SenderIds.RequestAsync(new RequestSenderIdRequest +{ + SenderId = "Termii", + UseCase = "Transactional alerts", + Company = "Example Ltd" +}); +``` + +## Tokens and OTP + Send an OTP token: ```csharp @@ -88,12 +150,115 @@ var token = await client.Tokens.SendAsync(new SendTokenRequest }); ``` +Verify an OTP token: + +```csharp +var verification = await client.Tokens.VerifyAsync(new VerifyTokenRequest +{ + PinId = token.PinId!, + Pin = "123456" +}); +``` + +Generate an in-app OTP: + +```csharp +var generated = await client.Tokens.GenerateAsync(new GenerateTokenRequest +{ + PhoneNumber = "2348012345678", + PinType = TermiiTokenPinType.Numeric, + PinAttempts = 3, + PinTimeToLive = 10, + PinLength = 6 +}); +``` + +The token client also supports voice OTP, voice call OTP, email OTP, and WhatsApp OTP through `SendVoiceAsync`, `CallAsync`, `SendEmailAsync`, and `SendWhatsAppAsync`. + +## Insights + Check account balance: ```csharp var balance = await client.Insights.GetBalanceAsync(); ``` +Check DND status: + +```csharp +var dnd = await client.Insights.CheckDndAsync(new CheckDndRequest +{ + PhoneNumber = "2348012345678" +}); +``` + +Query number intelligence: + +```csharp +var number = await client.Insights.QueryNumberAsync(new QueryNumberRequest +{ + PhoneNumber = "2348012345678", + CountryCode = "NG" +}); +``` + +Fetch message history: + +```csharp +var history = await client.Insights.GetMessageHistoryAsync(new GetMessageHistoryRequest +{ + Page = 0, + Size = 15, + Sender = "Termii", + Receiver = "2348012345678" +}); +``` + +## Error Handling + +The SDK throws `TermiiApiException` for non-success HTTP responses from Termii: + +```csharp +try +{ + await client.Messaging.SendAsync(new SendMessageRequest + { + To = "2348012345678", + From = "Termii", + Sms = "Hello from .NET", + Channel = TermiiMessageChannel.Generic + }); +} +catch (TermiiApiException ex) +{ + Console.WriteLine($"HTTP status: {(int)ex.StatusCode}"); + Console.WriteLine($"Termii code: {ex.TermiiCode}"); + Console.WriteLine($"Termii message: {ex.TermiiMessage}"); +} +``` + +Request models also validate required fields before sending. Missing required values throw `ArgumentException`, and invalid numeric ranges throw `ArgumentOutOfRangeException`. + +## Supported APIs + +Implemented in the current SDK: + +- Messaging: send single, WhatsApp conversational, and bulk messages. +- 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. + +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. + ## Examples Run the examples project after setting your Termii API key: @@ -103,10 +268,17 @@ export TERMII_API_KEY="your-termii-api-key" dotnet run --project examples/Termii.Examples ``` -To send a live Number API example message, opt in explicitly: +Run a live balance example: ```bash -export TERMII_SEND_NUMBER_MESSAGE="true" +export TERMII_EXAMPLE_ACTION="balance" +dotnet run --project examples/Termii.Examples +``` + +Send a live Number API example message: + +```bash +export TERMII_EXAMPLE_ACTION="number-message" export TERMII_EXAMPLE_PHONE_NUMBER="2348012345678" dotnet run --project examples/Termii.Examples ``` @@ -128,20 +300,15 @@ export TERMII_TEST_PHONE_NUMBER="2348012345678" dotnet test tests/Termii.IntegrationTests ``` -As endpoint support is added, integration tests should remain credential-gated so normal CI can run without network access or Termii credits. - -## Roadmap +## Release -Development is tracked through GitHub issues: +Releases are published from version tags through GitHub Actions. See [docs/RELEASING.md](docs/RELEASING.md) for the checklist. -- SDK foundation and HTTP pipeline. -- Messaging APIs. -- Sender ID and number APIs. -- Token/OTP APIs. -- Insights APIs. -- CI, documentation, NuGet packaging, and GitHub Releases. +## Links -Official Termii documentation: https://developer.termii.com/ +- Termii developer docs: https://developer.termii.com/ +- NuGet package: https://www.nuget.org/packages/Termii.SDK +- API coverage: [docs/API_COVERAGE.md](docs/API_COVERAGE.md) ## License diff --git a/docs/API_COVERAGE.md b/docs/API_COVERAGE.md index 2b17c02..00fb79a 100644 --- a/docs/API_COVERAGE.md +++ b/docs/API_COVERAGE.md @@ -31,10 +31,10 @@ The Termii docs describe a REST/JSON API and state that each account has its own | API coverage matrix | Implemented | #14, PR #18 | | Error handling and request validation | Implemented | #6, PR #19 | | Reusable fake HTTP test helpers and opt-in live test conventions | Implemented | #10, PR #20 | -| README endpoint examples | Planned | #12 | +| README endpoint examples | In progress | #12 | | CI build/test/package validation | Implemented | #11, PR #22 | | NuGet package metadata and publishing workflow | Implemented | #8, PR #23 | -| First GitHub Release and NuGet publish | Planned | #13 | +| First GitHub Release and NuGet publish | Implemented | #13, PR #28, v0.2.0 | ## Endpoint Coverage @@ -53,10 +53,10 @@ The Termii docs describe a REST/JSON API and state that each account has its own | Token | Send voice call token | POST | `/api/sms/otp/call` | JSON body | `TermiiClient.Tokens` | Implemented | #4, PR #26 | Unit tests added | | Token | Send email OTP token | POST | `/api/email/otp/send` | JSON body | `TermiiClient.Tokens` | Implemented | #4, PR #26 | Unit tests added | | Token | Send WhatsApp OTP token | POST | `/api/sms/send` | JSON body with `channel=whatsapp_otp` | `TermiiClient.Tokens` | Implemented | #4, PR #26 | Unit tests added | -| Insights | Get balance | GET | `/api/get-balance` | Query | `TermiiClient.Insights` | In progress | #9 | Unit tests added | -| Insights | Search DND/number status | GET | `/api/check/dnd` | Query | `TermiiClient.Insights` | In progress | #9 | Unit tests added | -| Insights | Query number intelligence/status | GET | `/api/insight/number/query` | Query | `TermiiClient.Insights` | In progress | #9 | Unit tests added | -| Insights | Fetch message inbox/history | GET | `/api/sms/inbox` | Query | `TermiiClient.Insights` | In progress | #9 | Unit tests added | +| Insights | Get balance | GET | `/api/get-balance` | Query | `TermiiClient.Insights` | Implemented | #9, PR #27 | Unit tests added | +| 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 | ## Deferred Coverage diff --git a/examples/Termii.Examples/Program.cs b/examples/Termii.Examples/Program.cs index a5e551c..d08e950 100644 --- a/examples/Termii.Examples/Program.cs +++ b/examples/Termii.Examples/Program.cs @@ -15,22 +15,33 @@ Console.WriteLine($"Termii client configured for {client.Options.BaseUrl}."); +var action = Environment.GetEnvironmentVariable("TERMII_EXAMPLE_ACTION"); var phoneNumber = Environment.GetEnvironmentVariable("TERMII_EXAMPLE_PHONE_NUMBER"); -var sendNumberMessage = string.Equals( - Environment.GetEnvironmentVariable("TERMII_SEND_NUMBER_MESSAGE"), - "true", - StringComparison.OrdinalIgnoreCase); -if (!sendNumberMessage || string.IsNullOrWhiteSpace(phoneNumber)) +if (string.Equals(action, "balance", StringComparison.OrdinalIgnoreCase)) { - Console.WriteLine("Set TERMII_SEND_NUMBER_MESSAGE=true and TERMII_EXAMPLE_PHONE_NUMBER to send a Number API message."); + var balance = await client.Insights.GetBalanceAsync(); + + Console.WriteLine($"Balance: {balance.Balance} {balance.Currency}".Trim()); return; } -var response = await client.Numbers.SendAsync(new SendNumberMessageRequest +if (string.Equals(action, "number-message", StringComparison.OrdinalIgnoreCase)) { - To = phoneNumber, - Sms = "Hello from Termii.SDK", -}); + if (string.IsNullOrWhiteSpace(phoneNumber)) + { + Console.WriteLine("Set TERMII_EXAMPLE_PHONE_NUMBER to send a Number API message."); + return; + } + + var response = await client.Numbers.SendAsync(new SendNumberMessageRequest + { + To = phoneNumber, + Sms = "Hello from Termii.SDK", + }); + + Console.WriteLine($"Number API response: {response.Message ?? response.Code ?? "sent"}"); + return; +} -Console.WriteLine($"Number API response: {response.Message ?? response.Code ?? "sent"}"); +Console.WriteLine("Set TERMII_EXAMPLE_ACTION=balance or TERMII_EXAMPLE_ACTION=number-message to run a live example.");