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
233 changes: 200 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -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<TermiiMessageResponse> 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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
```
Expand All @@ -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

Expand Down
12 changes: 6 additions & 6 deletions docs/API_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
33 changes: 22 additions & 11 deletions examples/Termii.Examples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Loading