<!-- canonical: https://docs.costplus.io/docs/sdks/dotnet -->

> Official C#/.NET SDK for the Cost+ payment gateway
Official C#/.NET SDK for the Cost+ payment gateway. Simplifies the HPP (Hosted Payment Page) redirect flow, HMAC payload signing, and webhook verification.

## Features

- **Zero dependencies** — uses only built-in `System.Text.Json` and `System.Security.Cryptography`
- Targets .NET 8.0 with C# 12 features (records, file-scoped namespaces, pattern matching)
- Nullable reference types enabled throughout
- HMAC-SHA256 signature generation and constant-time verification
- Automatic snake_case/PascalCase mapping between the API and the SDK
- Webhook parsing + API-based order verification
- Fully async API surface

## Requirements

- .NET 8.0 SDK or later
- A Cost+ merchant account — [dashboard.costplus.io](https://dashboard.costplus.io/)

## Installation

```bash
dotnet add package NoPayn
```

Or as a local project reference:

```bash
dotnet add reference path/to/src/NoPayn/NoPayn.csproj
```

## Quick Start

### 1. Initialise the Client

```csharp
using NoPayn;
using NoPayn.Models;

var nopayn = new NoPaynClient(new NoPaynConfig(
    ApiKey: "your-api-key",
    MerchantId: "your-project"
));
```

### 2. Create a Payment and Redirect to the HPP

```csharp
var result = await nopayn.GeneratePaymentUrlAsync(new CreateOrderParams
{
    Amount = 1295,              // €12.95 in cents
    Currency = "EUR",
    MerchantOrderId = "ORDER-001",
    Description = "Premium Widget",
    ReturnUrl = "https://shop.example.com/success",
    FailureUrl = "https://shop.example.com/failure",
    WebhookUrl = "https://shop.example.com/webhook",
    Locale = "en-GB",
    ExpirationPeriod = "PT30M",
});

// Redirect the customer
// result.OrderUrl   → HPP (customer picks payment method)
// result.PaymentUrl → direct link to the first transaction's payment method
// result.Signature  → HMAC-SHA256 for verification
// result.OrderId    → NoPayn order UUID
```

### 3. Handle the Webhook

```csharp
app.MapPost("/webhook", async (HttpContext ctx) =>
{
    using var reader = new StreamReader(ctx.Request.Body);
    var rawBody = await reader.ReadToEndAsync();
    var verified = await nopayn.VerifyWebhookAsync(rawBody);

    Console.WriteLine(verified.Order.Status); // "completed", "cancelled", etc.
    Console.WriteLine(verified.IsFinal);      // true when the order won't change

    if (verified.Order.Status == "completed")
    {
        // Fulfil the order
    }

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

## API Reference

### `new NoPaynClient(config, httpClient?)`

| Parameter | Type | Required | Default |
|---|---|---|---|
| `ApiKey` | `string` | Yes | — |
| `MerchantId` | `string` | Yes | — |
| `BaseUrl` | `string` | No | `https://api.nopayn.co.uk` |

An optional `HttpClient` can be passed as the second constructor parameter for custom HTTP handling or testing.

### `client.CreateOrderAsync(params): Task<Order>`

Creates an order via `POST /v1/orders/`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `Amount` | `int` | Yes | Amount in smallest currency unit (cents) |
| `Currency` | `string` | Yes | ISO 4217 code (`EUR`, `GBP`, `USD`, `NOK`, `SEK`) |
| `MerchantOrderId` | `string?` | No | Your internal order reference |
| `Description` | `string?` | No | Order description |
| `ReturnUrl` | `string?` | No | Redirect after successful payment |
| `FailureUrl` | `string?` | No | Redirect on cancel/expiry/error |
| `WebhookUrl` | `string?` | No | Async status-change notifications |
| `Locale` | `string?` | No | HPP language (`en-GB`, `de-DE`, `nl-NL`, etc.) |
| `PaymentMethods` | `IReadOnlyList<string>?` | No | Filter HPP methods |
| `ExpirationPeriod` | `string?` | No | ISO 8601 duration (`PT30M`) |

**Available payment methods:** `credit-card`, `apple-pay`, `google-pay`, `vipps-mobilepay`

### `client.GetOrderAsync(orderId): Task<Order>`

Fetches order details via `GET /v1/orders/{id}/`.

### `client.CreateRefundAsync(orderId, amount, description?): Task<Refund>`

Issues a full or partial refund via `POST /v1/orders/{id}/refunds/`.

### `client.GeneratePaymentUrlAsync(params): Task<PaymentUrlResult>`

Convenience method that creates an order and returns:

```csharp
public record PaymentUrlResult(
    string OrderId,        // NoPayn order UUID
    string OrderUrl,       // HPP URL
    string? PaymentUrl,    // Direct payment URL (first transaction)
    string Signature,      // HMAC-SHA256 of amount:currency:orderId
    Order Order            // Full order object
);
```

### `client.GenerateSignature(amount, currency, orderId): string`

Generates an HMAC-SHA256 hex signature.

### `client.VerifySignature(amount, currency, orderId, signature): bool`

Constant-time verification of an HMAC-SHA256 signature.

### `client.VerifyWebhookAsync(rawBody): Task<VerifiedWebhook>`

Parses the webhook body, then calls `GET /v1/orders/{id}/` to verify the actual status.

### Standalone HMAC Utilities

```csharp
using NoPayn;

var sig = NoPaynSignature.Generate("your-api-key", 1295, "EUR", "order-uuid");
var ok  = NoPaynSignature.Verify("your-api-key", 1295, "EUR", "order-uuid", sig);
```

## Error Handling

```csharp
using NoPayn.Exceptions;

try
{
    await nopayn.CreateOrderAsync(new CreateOrderParams { Amount = 100, Currency = "EUR" });
}
catch (ApiException ex)
{
    Console.Error.WriteLine(ex.StatusCode);  // 401, 400, etc.
    Console.Error.WriteLine(ex.ErrorBody);   // Raw API error response
}
catch (NoPaynException ex)
{
    Console.Error.WriteLine(ex.Message);     // Network or parsing error
}
```

| Exception | Description |
|---|---|
| `NoPaynException` | Base exception (network, parsing) |
| `ApiException` | HTTP error from the API |
| `WebhookException` | Invalid webhook payload |

## Order Statuses

| Status | Final? | Description |
|---|---|---|
| `new` | No | Order created |
| `processing` | No | Payment in progress |
| `completed` | Yes | Payment successful — deliver the goods |
| `cancelled` | Yes | Payment cancelled by customer |
| `expired` | Yes | Payment link timed out |
| `error` | Yes | Technical failure |

## Webhook Best Practices

1. **Always verify via the API** — the webhook payload only contains the order ID, never the status. The SDK's `VerifyWebhookAsync()` does this automatically.
2. **Return HTTP 200** to acknowledge receipt. Any other code triggers up to 10 retries (2 minutes apart).
3. **Implement a backup poller** — for orders older than 10 minutes that haven't reached a final status, poll `GetOrderAsync()` as a safety net.
4. **Be idempotent** — you may receive the same webhook multiple times.

## Test Cards

Use these cards in Cost+ test mode (sandbox website):

| Card | Number | Notes |
|---|---|---|
| Visa (success) | `4111 1111 1111 1111` | Any CVV |
| Mastercard (success) | `5544 3300 0003 7` | Any CVV |
| Visa (declined) | `4111 1111 1111 1105` | Do Not Honor |
| Visa (insufficient funds) | `4111 1111 1111 1151` | Insufficient Funds |

Use any future expiry date and any 3-digit CVC.

## Demo App

A Docker-based ASP.NET Core demo app is included in the [GitHub repository](https://github.com/NoPayn/C_.NET-sdk) for testing the full payment flow.

## Support

Need help? Reach out to our support team at **support@costplus.io**.
