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

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

## Features

- HMAC-SHA256 signature generation and constant-time verification
- Automatic snake_case mapping from the API to Ruby-friendly OpenStruct objects
- Webhook parsing + API-based order verification
- Tested across Ruby 3.1, 3.2, and 3.3
- Sinatra-based demo merchant app included

## Requirements

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

## Installation

Add to your Gemfile:

```ruby
gem "nopayn"
```

Then run:

```bash
bundle install
```

Or install directly:

```bash
gem install nopayn
```

## Quick Start

### 1. Initialise the Client

```ruby
require "nopayn"

nopayn = NoPayn::Client.new(
  api_key:     "your-api-key",
  merchant_id: "your-project"
)
```

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

```ruby
result = nopayn.generate_payment_url(
  amount:            1295,           # €12.95 in cents
  currency:          "EUR",
  merchant_order_id: "ORDER-001",
  description:       "Premium Widget",
  return_url:        "https://shop.example.com/success",
  failure_url:       "https://shop.example.com/failure",
  webhook_url:       "https://shop.example.com/webhook",
  locale:            "en-GB",
  expiration_period: "PT30M"
)

# Redirect the customer
# result.order_url   → HPP (customer picks payment method)
# result.payment_url → direct link to the first transaction's payment method
# result.signature   → HMAC-SHA256 for verification
# result.order_id    → NoPayn order UUID
```

### 3. Handle the Webhook

```ruby
post "/webhook" do
  request.body.rewind
  raw_body = request.body.read

  verified = nopayn.verify_webhook(raw_body)

  puts verified.order.status  # "completed", "cancelled", etc.
  puts verified.is_final      # true when the order won't change

  if verified.order.status == "completed"
    # Fulfil the order
  end

  status 200
end
```

## API Reference

### `NoPayn::Client.new(api_key:, merchant_id:, base_url:)`

| Parameter | Type | Required | Default |
|---|---|---|---|
| `api_key` | `String` | Yes | — |
| `merchant_id` | `String` | Yes | — |
| `base_url` | `String` | No | `https://api.nopayn.co.uk` |

### `client.create_order(params) → OpenStruct`

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

| Parameter | Type | Required | Description |
|---|---|---|---|
| `:amount` | `Integer` | Yes | Amount in smallest currency unit (cents) |
| `:currency` | `String` | Yes | ISO 4217 code (`EUR`, `GBP`, `USD`, `NOK`, `SEK`) |
| `:merchant_order_id` | `String` | No | Your internal order reference |
| `:description` | `String` | No | Order description |
| `:return_url` | `String` | No | Redirect after successful payment |
| `:failure_url` | `String` | No | Redirect on cancel/expiry/error |
| `:webhook_url` | `String` | No | Async status-change notifications |
| `:locale` | `String` | No | HPP language (`en-GB`, `de-DE`, `nl-NL`, etc.) |
| `:payment_methods` | `Array<String>` | No | Filter HPP methods |
| `:expiration_period` | `String` | No | ISO 8601 duration (`PT30M`) |

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

### `client.get_order(order_id) → OpenStruct`

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

### `client.create_refund(order_id, amount, description: nil) → OpenStruct`

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

### `client.generate_payment_url(params) → OpenStruct`

Convenience method that creates an order and returns:

```ruby
result.order_id     # NoPayn order UUID
result.order_url    # HPP URL
result.payment_url  # Direct payment URL (first transaction)
result.signature    # HMAC-SHA256 of amount:currency:order_id
result.order        # Full order OpenStruct
```

### `client.generate_signature(amount, currency, order_id) → String`

Generates an HMAC-SHA256 hex signature.

### `client.verify_signature(amount, currency, order_id, signature) → Boolean`

Constant-time verification of an HMAC-SHA256 signature.

### `client.verify_webhook(raw_body) → OpenStruct`

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

### Standalone HMAC Utilities

```ruby
require "nopayn"

sig = NoPayn::Signature.generate("your-api-key", 1295, "EUR", "order-uuid")
ok  = NoPayn::Signature.verify("your-api-key", 1295, "EUR", "order-uuid", sig)
```

## Error Handling

```ruby
begin
  nopayn.create_order(amount: 100, currency: "EUR")
rescue NoPayn::ApiError => e
  puts e.status_code  # 401, 400, etc.
  puts e.error_body   # Raw API error response
rescue NoPayn::Error => e
  puts e.message      # Network or parsing error
end
```

| Exception | Parent | Description |
|---|---|---|
| `NoPayn::Error` | `StandardError` | Base error for all SDK errors |
| `NoPayn::ApiError` | `NoPayn::Error` | HTTP error from the API |
| `NoPayn::WebhookError` | `NoPayn::Error` | 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 `verify_webhook` 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 `get_order` 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 Sinatra-based demo app is included in the [GitHub repository](https://github.com/NoPayn/ruby-sdk) for testing the full payment flow.

## Support

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