# Cost+ Developer Docs — Full Bundle

This file concatenates every public Cost+ developer doc and API reference page as clean Markdown. Source: https://docs.costplus.io. For a short entry point, see https://docs.costplus.io/llms.txt.

---

## /llms.txt
<https://docs.costplus.io/llms.txt>

# Cost+ Payment API

> Cost+ is a REST payment-processing API for European merchants. Create an order, redirect the customer to `payment_url`, confirm via webhook or `GET /orders/{id}`. Cards, Apple Pay, Google Pay, Vipps/MobilePay, SEPA, and more through a single integration.

Docs site: https://docs.costplus.io
Full bundle: https://docs.costplus.io/llms-full.txt
OpenAPI: https://docs.costplus.io/openapi.json

## Base URL

```
https://api.costplus.online/v1
```

There is **no separate sandbox URL**. The API key determines the mode:
- Sandbox website key → simulated transactions (no money moves)
- Production website key → real charges

Manage keys at https://dashboard.costplus.io/ under Websites → *your site* → Integration.

## Authentication

HTTP Basic Auth. Use the **API key as the username** with an **empty password** (the trailing colon is required).

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/
```

Equivalent manual header: `Authorization: Basic <base64("YOUR_API_KEY:")>`. TLS 1.2+ required. Never expose the key in client-side code.

## Money Units

Amounts are **integers in the smallest currency unit** (cents for EUR, öre for SEK, etc.).

- 12.95 EUR → `1295`
- 100 SEK → `10000`
- 1.00 USD → `100`

## First Payment in 4 Steps

### 1. Create an order

```bash
curl -u YOUR_API_KEY: \
  -X POST https://api.costplus.online/v1/orders/ \
  -H 'Content-Type: application/json' \
  -d '{
    "currency": "EUR",
    "amount": 1295,
    "merchant_order_id": "my-first-order",
    "description": "Test order",
    "return_url": "https://example.com/return",
    "failure_url": "https://example.com/cancel",
    "webhook_url": "https://example.com/webhook",
    "transactions": [{ "payment_method": "credit-card" }]
  }'
```

The response contains the order `id` and, inside `transactions[0]`, a `payment_url`:

```json
{
  "id": "4851e31c-4137-4e91-95ef-1df945ee76a2",
  "status": "new",
  "currency": "EUR",
  "amount": 1295,
  "transactions": [
    {
      "id": "d291f03f-...",
      "payment_method": "credit-card",
      "payment_url": "https://pay.costplus.online/4851e31c.../credit-card/d291f03f..."
    }
  ]
}
```

Shortcut: omit `transactions` and use `order_url` from the response — Cost+ then shows the customer all enabled payment methods.

### 2. Redirect the customer

Send the browser to `transactions[0].payment_url` (or `order_url`). The customer completes payment on the Cost+ hosted page.

### 3. Handle the return

The customer returns to:
- `return_url` on success / most statuses
- `failure_url` on `cancelled`, `expired`, or `error`

**Never trust the redirect alone** — the customer may close the browser before the redirect fires, and query params can be tampered with.

### 4. Verify the final status

Use either (preferably both):

**Webhook** — Cost+ POSTs `{"event": "status_changed", "order_id": "..."}` to your `webhook_url`. Always re-fetch the order by ID before acting; return HTTP 200 within a few seconds to acknowledge.

**Polling** — `GET /v1/orders/{id}/?fields[]=amount_details` returns the authoritative status plus capturable / captured / refundable / voidable amounts.

```bash
curl -u YOUR_API_KEY: \
  'https://api.costplus.online/v1/orders/4851e31c.../?fields[]=amount_details'
```

Terminal statuses: `completed`, `cancelled`, `expired`, `error`. Fulfil the order when status is `completed`.

## Core Endpoints

| Method | Path | Purpose |
|---|---|---|
| POST | `/orders/` | Create an order (returns `payment_url`) |
| GET | `/orders/{id}/` | Fetch current status |
| GET | `/orders/` | List orders by date range |
| PUT | `/orders/{id}/` | Update an order (before payment) |
| DELETE | `/orders/{id}/` | Cancel an order |
| POST | `/orders/{id}/refunds/` | Refund by amount or line |
| POST | `/orders/{id}/captures/` | Capture an authorised amount |
| POST | `/orders/{id}/voids/` | Void an authorised amount |
| POST | `/paymentlinks/` | Create a reusable payment link |
| GET | `/paymentlinks/{id}/` | Fetch a payment link + its orders |
| GET | `/search/orders/` | Search orders by merchant_order_id or customer fields |
| GET | `/currencies/` | List supported currencies |

All paths above require Basic Auth. POSTs take `application/json`.

## Test Cards (Sandbox)

| Card | Number | Result |
|---|---|---|
| Visa | `4111 1111 1111 1111` | Success |
| Mastercard | `5544 3300 0000 0037` | Success |
| Visa | `4000 0000 0000 0002` | Do Not Honor |
| Visa | `4000 0000 0000 0069` | Insufficient Funds |

Any future expiry (e.g. `12/28`) and any 3-digit CVC work.

## Webhook Handler Checklist

1. Accept POST with JSON body (e.g. `{"event":"status_changed","order_id":"..."}`).
2. **Re-fetch** the order via `GET /orders/{id}/` — do not trust the payload.
3. Be idempotent — the same event may be delivered more than once.
4. Return HTTP 2xx within a few seconds. Cost+ retries on failure.
5. Fulfil only when `status === "completed"` (or the transaction reaches its terminal paid state).

See https://docs.costplus.io/docs/guides/webhooks for retry schedule and payload schemas.

## Key Links

- Quickstart: https://docs.costplus.io/docs/getting-started/quickstart
- Authentication: https://docs.costplus.io/docs/getting-started/authentication
- Testing & error codes: https://docs.costplus.io/docs/getting-started/testing
- Hosted Payment Page: https://docs.costplus.io/docs/guides/hosted-payment-page
- Payment Links: https://docs.costplus.io/docs/guides/payment-links
- Auth / Capture / Void: https://docs.costplus.io/docs/guides/auth-capture-void
- Refunds: https://docs.costplus.io/docs/guides/refunds
- Webhooks: https://docs.costplus.io/docs/guides/webhooks
- Recurring Payments: https://docs.costplus.io/docs/guides/recurring-payments
- One-Click Payments: https://docs.costplus.io/docs/guides/one-click-payments
- API reference (HTML): https://docs.costplus.io/api-reference/
- OpenAPI spec (JSON): https://docs.costplus.io/openapi.json
- Full Markdown bundle: https://docs.costplus.io/llms-full.txt
- SDKs: Node.js, Python, PHP, Java/Kotlin, C#/.NET, Ruby — https://docs.costplus.io/docs/sdks

## Clean Markdown Per Page

Every docs / API reference page is also available as clean Markdown by appending `.md`:

- https://docs.costplus.io/docs/getting-started/quickstart.md
- https://docs.costplus.io/api-reference/orders/createOrder.md

Use those URLs for LLM ingestion — they contain no HTML, no UI chrome, no nav.


---

## Welcome to Cost+ Developer Docs
<https://docs.costplus.io/docs>

> Welcome to Cost+ developer documentation

Cost+ is a modern payment processing platform that enables merchants to accept payments across multiple channels with transparent, cost-plus pricing. This documentation covers everything you need to integrate with the Cost+ API.

## API Base URL

| | URL |
|---|---|
| **API** | `https://api.costplus.online/v1` |

> [!NOTE]
> There is no separate sandbox URL. Cost+ uses a single API endpoint. Whether a transaction is processed in sandbox or production mode is determined by your **API key** — specifically whether the website (project) in the [Merchant Portal](https://dashboard.costplus.io/) is configured as sandbox or production.

## Security

All API requests are secured with **HTTP Basic Authentication**. You must connect over **TLS 1.2 or higher**.

```bash title="Example request"
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/
```

## Quick Links

- **[Quickstart](/docs/getting-started/quickstart)** — Accept your first payment in 5 minutes.
- **[Authentication](/docs/getting-started/authentication)** — Set up API credentials and learn how auth works.
- **[Testing Your Integration](/docs/getting-started/testing)** — Test cards, sandbox environment, and going live.
- **[FAQ & Troubleshooting](/docs/getting-started/faq)** — Common issues and how to resolve them.
- **[Guides](/docs/guides)** — Step-by-step walkthroughs for common integration patterns.
- **[Payment Methods](/docs/payment-methods)** — Supported payment methods and configuration.
- **[SDKs](/docs/sdks)** — Official libraries for Node.js, Python, PHP, Java/Kotlin, C#/.NET, and Ruby.
- **[Plugins & Integrations](/docs/plugins)** — Pre-built plugins for popular platforms.


---

## Authentication
<https://docs.costplus.io/docs/getting-started/authentication>

> Learn how to authenticate with the Cost+ API

All communication with the Cost+ API requires **TLS 1.2 or higher** and **HTTP Basic Authentication**.

## How Basic Auth Works

Authenticate by using your **API key as the username** with an **empty password**. Base64-encode the string `\{api_key\}:` (note the trailing colon — the password is empty).

The resulting `Authorization` header looks like this:

```
Authorization: Basic aHVudGVyMjo=
```

> [!WARNING]
> Never expose your API key in client-side code or public repositories. Keep it server-side only.

## Using cURL

### Out-of-the-box (recommended)

cURL natively supports Basic Auth with the `-u` flag. Pass your API key followed by a colon:

```bash title="cURL with -u flag"
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/
```

### Manual Base64 encoding

If you prefer to construct the header yourself, first encode the key:

```bash title="Encode the API key"
echo -n "YOUR_API_KEY:" | base64
```

Then pass the encoded value in the `Authorization` header:

```bash title="cURL with manual Authorization header"
curl -H "Authorization: Basic YOUR_BASE64_ENCODED_KEY" https://api.costplus.online/v1/orders/
```

> [!TIP]
> The trailing colon after the API key is required — it separates the username from the (empty) password in the Basic Auth scheme.

## HTTP Status Codes

The API uses standard HTTP status codes to indicate the result of a request.

| Status Code | Meaning | Description |
|---|---|---|
| **200** | OK | Request succeeded. |
| **201** | Created | Resource was successfully created. |
| **400** | Bad Request | The request was malformed or missing required fields. |
| **401** | Unauthorized | Authentication failed — check your API key. |
| **403** | Forbidden | You do not have permission to access this resource. |
| **404** | Not Found | The requested resource does not exist. |
| **500** | Internal Server Error | Something went wrong on our end. |
| **502** | Bad Gateway | Upstream service error. |
| **503** | Service Unavailable | The API is temporarily unavailable. |
| **504** | Gateway Timeout | The upstream service did not respond in time. |


---

## FAQ & Troubleshooting
<https://docs.costplus.io/docs/getting-started/faq>

> Common questions and troubleshooting tips

## 1. I'm getting an "Unauthorized" error

This usually means you are posting an incorrect or misspelled API key. Double-check that your key is correct and that it is formatted properly in the `Authorization` header.

**Example of a failed auth response:**

```json title="401 Unauthorized response"
{
  "error": "werkzeug.exceptions.Unauthorized",
  "message": "The server could not verify that you are authorized to access the URL requested."
}
```

> [!TIP]
> Make sure you include the trailing colon after your API key when using Basic Auth (e.g., `YOUR_API_KEY:`). The colon separates the username from the empty password.

## 2. I'm getting an "Unsupported payment method" error

Each payment method must be **activated on your account** before it can be used. If you attempt to use an inactive method, the API returns a `400 Bad Request`:

```json title="400 Bad Request response"
{
  "error": "werkzeug.exceptions.BadRequest",
  "message": "The requested payment method is not active for this merchant."
}
```

> [!NOTE]
> Contact your Cost+ account manager or enable the payment method in the admin portal under **Settings > Payment Methods**.

## 3. Which test card numbers can I use?

See the [Testing Your Integration](/docs/getting-started/testing) page for a full list of test card numbers, including cards that simulate successful and failed transactions.

## 4. How are webhook retries handled?

If your webhook endpoint does not respond with a `2xx` status code, Cost+ will retry delivery:

- **Up to 10 retries**, spaced **2 minutes apart**.
- The **first attempt** times out after **4 seconds**.
- **Subsequent retries** time out after **10 seconds**.

> [!WARNING]
> If all 10 retries fail, the webhook event is marked as failed and will not be retried again. Make sure your endpoint responds quickly and returns a `200` status code.

For full details on webhook configuration and payload formats, see the [Webhooks](/docs/guides/webhooks) guide.


---

## Quickstart
<https://docs.costplus.io/docs/getting-started/quickstart>

> Accept your first payment in 5 minutes

This guide walks you through creating and completing a test payment using the Cost+ API. By the end, you'll have a working integration you can build on.

## Prerequisites

- A Cost+ account with a **sandbox website** — [create one in the Merchant Portal](https://dashboard.costplus.io/)
- Your sandbox **API key** (found under Websites → your sandbox website → Integration)

> [!NOTE]
> Not sure how to get your API key? See [Testing Your Integration](/docs/getting-started/testing) for detailed setup instructions.

## Step 1: Create an Order

Send a POST request to create a payment order. Replace `YOUR_API_KEY` with your sandbox API key:

```bash title="Create an order"
curl -X POST https://api.costplus.online/v1/orders/ \
  -u YOUR_API_KEY: \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "EUR",
    "amount": 1295,
    "merchant_order_id": "my-first-order",
    "description": "Test order",
    "return_url": "https://example.com/return",
    "webhook_url": "https://example.com/webhook",
    "transactions": [
      {
        "payment_method": "credit-card"
      }
    ]
  }'
```

> [!TIP]
> The `amount` is in the smallest currency unit (cents). `1295` means **12.95 EUR**.

The API returns the full order object. The key fields are `id`, `status`, and the `payment_url` inside the transaction:

```json title="Response"
{
  "id": "4851e31c-4137-4e91-95ef-1df945ee76a2",
  "status": "new",
  "currency": "EUR",
  "amount": 1295,
  "merchant_order_id": "my-first-order",
  "description": "Test order",
  "return_url": "https://example.com/return",
  "webhook_url": "https://example.com/webhook",
  "created": "2026-01-15T12:00:05.433502+00:00",
  "modified": "2026-01-15T12:00:05.553125+00:00",
  "expiration_period": "PT1H",
  "transactions": [
    {
      "id": "d291f03f-a406-428a-967a-4895a46e03fd",
      "payment_method": "credit-card",
      "status": "new",
      "amount": 1295,
      "currency": "EUR",
      "payment_url": "https://api.costplus.online/pay/4851e31c.../select-payment-method/credit-card/d291f03f...",
      "is_capturable": false,
      "expiration_period": "PT30M"
    }
  ]
}
```

> [!NOTE]
> The response shape depends on what you send:
> - **With a `transactions` array** (this quickstart): each transaction object contains its own `payment_url`. Redirect the customer there.
> - **Without `transactions`**: the order has a top-level `order_url` instead, and the customer picks a payment method on Cost+'s hosted page. See the [Hosted Payment Page guide](/docs/guides/hosted-payment-page) for that flow.

Save the `id` — you'll need it in Step 3.

## Step 2: Complete the Test Payment

1. Open the `payment_url` from the response in your browser
2. On the payment page, enter the test card details:

| Field | Value |
|---|---|
| Card number | `4111 1111 1111 1111` |
| Expiry | Any future date (e.g. `12/28`) |
| CVC | Any 3 digits (e.g. `123`) |

3. Submit the payment
4. You'll be redirected back to your `return_url`

> [!WARNING]
> Don't rely on the redirect alone to confirm payment. The customer may close their browser before being redirected. Always verify via the API (Step 3) or webhooks (Step 4).

## Step 3: Verify the Payment

Fetch the order to confirm it completed:

```bash title="Check order status"
curl -u YOUR_API_KEY: \
  https://api.costplus.online/v1/orders/4851e31c-4137-4e91-95ef-1df945ee76a2/
```

A successful payment looks like this:

```json title="Response (completed)"
{
  "id": "4851e31c-4137-4e91-95ef-1df945ee76a2",
  "status": "completed",
  "currency": "EUR",
  "amount": 1295,
  "merchant_order_id": "my-first-order",
  "completed": "2026-01-15T12:02:30.123456+00:00",
  "transactions": [
    {
      "id": "d291f03f-a406-428a-967a-4895a46e03fd",
      "payment_method": "credit-card",
      "status": "completed",
      "amount": 1295,
      "currency": "EUR",
      "payment_method_details": {
        "truncated_pan": "1111",
        "card_expiry": "122028"
      }
    }
  ]
}
```

The order `status` is `"completed"` — the payment was successful.

## Step 4: Handle the Webhook (Recommended)

When the payment status changes, Cost+ sends a POST request to your `webhook_url`:

```json title="Webhook payload"
{
  "event": "status_changed",
  "order_id": "4851e31c-4137-4e91-95ef-1df945ee76a2"
}
```

When you receive this:
1. Call `GET /v1/orders/{order_id}/` to verify the current status (never trust the webhook payload alone)
2. Return HTTP `200` to acknowledge receipt
3. Fulfill the order if the status is `"completed"`

> [!TIP]
> For local development, use a tunnel like [ngrok](https://ngrok.com) to expose your local server and receive webhooks.

See the [Webhooks guide](/docs/guides/webhooks) for retry logic, best practices, and payload details.

## Alternative: Payment Links

If you don't need server-side redirect logic, **payment links** offer a simpler path. Create a link, share the URL with your customer, and check the status later.

```bash title="Create a payment link"
curl -X POST https://api.costplus.online/v1/paymentlinks/ \
  -u YOUR_API_KEY: \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_order_id": "invoice-1234",
    "amount": 2500,
    "currency": "EUR",
    "description": "Invoice #1234"
  }'
```

The response includes a `payment_url` you can share via email, SMS, or chat. The customer can attempt payment multiple times (up to 25) until the link expires or payment succeeds.

See the [Payment Links guide](/docs/guides/payment-links) for the full workflow.

## What's Next?

You've completed your first payment. Here's where to go from here:

- **[Hosted Payment Page](/docs/guides/hosted-payment-page)** — full HPP reference with all request fields and options
- **[Recurring Payments](/docs/guides/recurring-payments)** — set up subscriptions and scheduled billing
- **[One-Click Payments](/docs/guides/one-click-payments)** — fast checkout for returning customers
- **[Auth / Capture / Void](/docs/guides/auth-capture-void)** — authorize first, capture later (e.g. when shipping)
- **[Refunds](/docs/guides/refunds)** — process full and partial refunds
- **[SDKs](/docs/sdks)** — official libraries for Node.js, Python, PHP, Java/Kotlin, C#/.NET, and Ruby
- **[Plugins](/docs/plugins)** — pre-built integrations for Shopify, WooCommerce, Magento, and more

## Related Endpoints

- [Create Order](/api-reference/orders/createOrder) — full API reference for order creation
- [Get Order](/api-reference/orders/getOrder) — retrieve order details and status
- [Create Payment Link](/api-reference/payment-links/createPaymentLink) — create reusable payment links


---

## Testing Your Integration
<https://docs.costplus.io/docs/getting-started/testing>

> Set up your test environment and use test credentials

Before going live, use a Cost+ sandbox website to verify your integration works correctly. There is no separate sandbox URL — you use the same production API endpoint (`https://api.costplus.online/v1`) with an API key from a sandbox website.

## Getting Your Test API Key

1. Log in to the [Merchant Portal](https://dashboard.costplus.io/)
2. Navigate to **Websites**
3. Create a new website and select **Sandbox** as the type, or select an existing sandbox website
4. Click on **Integration** to find your API key
5. Use this API key in your integration — all transactions will be simulated

> [!NOTE]
> Sandbox vs production mode is determined entirely by the website type in the Merchant Portal, not by the API URL. A sandbox website's API key processes simulated transactions; a production website's API key processes real payments. Both use the same API endpoint.

## Test Card Numbers

Use the card numbers below to simulate different payment scenarios. **Do not use real card numbers in test mode.**

| Card Number | CVV | Brand | Result |
|---|---|---|---|
| `4111111111111111` | any | Visa | Success |
| `5544330000000037` | any | Mastercard | Success |
| `4462030000000000` | any | Visa | Success |
| `4111111111111105` | any | Visa | Do Not Honor |
| `4111111111111143` | any | Visa | Stolen Card |
| `4111111111111151` | any | Visa | Insufficient Funds |

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

> [!WARNING]
> Test card numbers only work in sandbox mode. Submitting them to a production website's API key will result in a declined transaction.

## Error Codes

When a transaction fails, the API returns one of the following error codes in the transaction's `reason` field:

| Error Code | Description |
|---|---|
| `ERROR_CARD_AUTHENTICATION_FAILURE` | 3DS authentication not completed in time; payment cancelled |
| `ERROR_CARD_CVV_NOT_VALID` | CVV correctly formatted but not valid |
| `ERROR_CARD_INFORMATION_NOT_VALID` | Card info correctly formatted but not valid |
| `ERROR_CARD_NOT_SUPPORTED_FOR_ECOMMERCE` | Transaction not supported for eCommerce |
| `ERROR_CARD_NOT_VALID` | Card ID is not valid |
| `ERROR_CARD_TYPE_DISABLED` | Card type is disabled |
| `ERROR_TRANSACTION_FAILED` | Card transaction failed |
| `ERROR_TRANSACTION_REJECTED_BY_CARD_PROCESSOR` | Rejected by processor (includes Visa/MC industry numeric code) |
| `ERROR_TRANSACTION_TYPE_NOT_ALLOWED_BY_SELLER` | Transaction type not allowed by seller |
| `ERROR_TRANSACTION_TYPE_NOT_SUPPORTED` | Not supported by card network |
| `INVALID_CARD_CVV` | CVV not recognized |
| `INVALID_CARD_NUMBER` | Card number not recognized |

## Common API Validation Errors

These errors are returned as `400 Bad Request` when the API request itself is invalid:

| Error | Description |
|---|---|
| Unknown payment method | You supplied an incorrect payment method name |
| No permissions for payment method | Your API key (project) doesn't have permissions for the specified payment method |
| Payment method not enabled | The payment method has not been enabled for your API key (project) |
| Payment method not supported | Your API key (project) does not have the correct status to use the specified payment method |

> [!TIP]
> If you receive "Payment method not enabled" or "No permissions", check the payment method configuration in the [Merchant Portal](https://dashboard.costplus.io/) under your website settings.


---

## Authorizations, Captures & Voids
<https://docs.costplus.io/docs/guides/auth-capture-void>

> Manage payment authorization, capture, and void flows

Some payment methods support a two-step flow: first **authorize** (reserve funds), then **capture** (collect the funds) or **void** (release the reservation).

## Capture Modes

Set `capture_mode` on the order to control when funds are captured:

| Mode | Behavior |
|------|----------|
| `manual` | You explicitly capture when ready (e.g., after shipping). If you don't capture before the order expires, the authorization is **lost** and cannot be captured. |
| `delayed` | Funds are automatically captured at the moment the `expiration_period` elapses. |

```json title="POST /v1/orders/ (manual capture)"
{
  "currency": "EUR",
  "amount": 5000,
  "capture_mode": "manual",
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    {
      "payment_method": "credit-card"
    }
  ]
}
```

> [!WARNING]
> With `manual` capture, you **must** capture before the order's expiration period ends. Once expired, the authorization is released and the funds cannot be captured. Set an appropriate `expiration_period` for your fulfillment timeline.

> [!TIP]
> You can set `capture_mode` at the order level **without** specifying `transactions`. The hosted payment page will then only show payment methods that support the specified capture mode.

### Checking Capturable Amounts

Before capturing, you can check how much is available for capture by requesting `amount_details`:

```bash
curl -u YOUR_API_KEY: \
  "https://api.costplus.online/v1/orders/{order_id}/?fields[]=amount_details"
```

The response includes an `amount_details` object:

```json
{
  "amount": 5000,
  "amount_details": {
    "capturable": 5000,
    "captured": 0,
    "refundable": 0,
    "refunded": 0,
    "voidable": 5000,
    "voided": 0
  }
}
```

## Order Lines

When using captures and voids by order line, use these types:

| Type | Description |
|------|-------------|
| `physical` | Physical product |
| `discount` | Discount amount |
| `shipping_fee` | Shipping cost |
| `sales_tax` | Sales tax |
| `digital` | Digital product |
| `gift_card` | Gift card |
| `store_credit` | Store credit |
| `surcharge` | Surcharge |

## Capturing Payments

### Capture by Order Line

```bash
POST /v1/orders/{id}/transactions/{transaction_id}/captures/orderlines
```

```json title="Request body"
{
  "description": "Shipping item #1",
  "order_line": {
    "merchant_order_line_id": "item-001",
    "quantity": 1
  }
}
```

### Capture by Amount

```bash
POST /v1/orders/{id}/transactions/{transaction_id}/captures/amount
```

```json title="Request body"
{
  "description": "Partial capture",
  "amount": 2500
}
```

## Voiding Payments

Void releases the authorized funds back to the customer.

### Void by Order Line

```bash
POST /v1/orders/{id}/transactions/{transaction_id}/voids/orderlines
```

```json title="Request body"
{
  "description": "Voiding item #2",
  "order_line": {
    "merchant_order_line_id": "item-002",
    "quantity": 1
  }
}
```

### Void by Amount

```bash
POST /v1/orders/{id}/transactions/{transaction_id}/voids/amount
```

```json title="Request body"
{
  "description": "Partial void",
  "amount": 1500
}
```

## Query Parameters

Add query parameters to include additional details in the response:

| Parameter | Description |
|-----------|-------------|
| `?fields[]=order_line_details` | Include order line breakdown |
| `?fields[]=amount_details` | Include amount breakdown |

## Optimistic Locking

Capture and void endpoints support **ETag-based optimistic locking** via the `if-match` header. This prevents race conditions when multiple systems attempt to capture the same transaction.

```bash title="Capture with optimistic locking"
curl -X POST https://api.costplus.online/v1/orders/{id}/transactions/{tid}/captures/amount \
  -u {api_key}: \
  -H "Content-Type: application/json" \
  -H "if-match: \"etag-value\"" \
  -d '{"description": "Capture", "amount": 2500}'
```

If the ETag doesn't match (the order was modified since you last fetched it), you'll receive a `412 Precondition Failed` response.

> [!WARNING]
> Cost+ supports **one capture per authorization**, and voids can only be processed **before** any capture. Plan your capture strategy accordingly.

## Related Endpoints

- [Capture by Amount](/api-reference/orders/captureByAmount) — capture a specific amount from an authorized order
- [Capture by Order Line](/api-reference/orders/captureByOrderLine) — capture specific order lines
- [Void by Amount](/api-reference/orders/voidByAmount) — void a specific amount from an authorized order
- [Void by Order Line](/api-reference/orders/voidByOrderLine) — void specific order lines


---

## Hosted Payment Page (HPP)
<https://docs.costplus.io/docs/guides/hosted-payment-page>

> Accept payments using Cost+'s hosted payment page

The Hosted Payment Page (HPP) is Cost+'s PCI DSS compliant payment form. It allows you to accept payments without handling sensitive card data on your own servers. You create an order via the API, redirect the customer to the hosted page, and they return to your site after payment.

## How It Works

1. Your server creates an order by calling **POST /v1/orders/**.
2. The API returns a URL pointing to the hosted payment page.
3. You redirect the customer to the payment page.
4. The customer completes payment on the Cost+ hosted page.
5. The customer is redirected back to your `return_url` (or `failure_url` for failed payments).
6. Cost+ sends a webhook notification to your `webhook_url` with the order status.

> [!NOTE]
> The hosted payment page is fully PCI DSS compliant. You never need to handle raw card numbers or sensitive payment data on your servers.

## Creating an Order

There are two approaches to using the HPP:

### Approach 1: Show All Payment Methods (Simplest)

Create an order **without** specifying `transactions`. The response includes an `order_url` — the customer is redirected there and sees all payment methods enabled for your account:

```json title="Request"
{
  "currency": "EUR",
  "amount": 1295,
  "merchant_order_id": "my-order-id-1",
  "description": "My amazing order",
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook"
}
```

```json title="Response"
{
  "id": "43114fde-da30-4115-8004-b7f808f9b25c",
  "status": "new",
  "currency": "EUR",
  "amount": 1295,
  "order_url": "https://api.costplus.online/pay/43114fde.../select-payment-method/",
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook"
}
```

Redirect the customer to the `order_url`. On the hosted page, all enabled payment methods are shown.

### Approach 2: Pre-select Payment Methods

Create an order **with** a `transactions` array to control which payment methods appear and in what order. Each transaction includes a `payment_method`, and the response returns a `payment_url` inside the transaction object:

```json title="Request"
{
  "currency": "EUR",
  "amount": 1295,
  "merchant_order_id": "my-order-id-1",
  "description": "My amazing order",
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    { "payment_method": "credit-card" }
  ]
}
```

```json title="Response"
{
  "id": "4851e31c-4137-4e91-95ef-1df945ee76a2",
  "status": "new",
  "currency": "EUR",
  "amount": 1295,
  "transactions": [
    {
      "id": "d291f03f-a406-428a-967a-4895a46e03fd",
      "payment_method": "credit-card",
      "status": "new",
      "payment_url": "https://api.costplus.online/pay/4851e31c.../select-payment-method/credit-card/d291f03f.../"
    }
  ]
}
```

Redirect the customer to the `payment_url` from the transaction.

> [!TIP]
> If you provide only a single entry in the `transactions` array, the customer is taken directly to that payment method without seeing a selection screen. The `flags` array contains `"is-test"` when using a sandbox API key.

## Request Fields

| Field | Required | Description |
| --- | --- | --- |
| `currency` | Yes | ISO 4217 currency code (e.g., `EUR`, `GBP`, `SEK`) |
| `amount` | Yes | Amount in cents. For example, 12.95 EUR is represented as `1295` |
| `merchant_order_id` | No | Your own reference ID for the order |
| `return_url` | No | URL to redirect the customer after payment (default for all statuses) |
| `failure_url` | No | URL to redirect the customer on `cancelled`, `expired`, or `error` status (see Return URLs below) |
| `locale` | No | Language for the payment page. Supported: `en-GB`, `de-DE`, `nl-NL`, `nl-BE`, `fr-BE`, `sv-SE`, `no-NO`, `da-DK` |
| `description` | No | Description of the order, shown to the customer |
| `payment_methods` | No | Filter to a single payment method (e.g. `["credit-card"]`). Omit to show all enabled methods. For multiple specific methods, use the `transactions` array instead |
| `webhook_url` | No | URL to receive status change notifications |
| `expiration_period` | No | ISO 8601 duration for order expiry. Default is `PT30M` (30 minutes) |

> [!WARNING]
> The `amount` field is always in the smallest currency unit (cents). Passing `1295` means 12.95 in the given currency. Passing `1295.00` or `12.95` will result in an error or an incorrect charge.

## Multiple Payment Methods

There are two ways to control which payment methods appear on the hosted page:

**Option A — `payment_methods` (single filter).** Pass a single-element array to restrict the order to one payment method. Omit the field entirely to show all methods enabled for your account.

**Option B — `transactions` array (recommended for multiple methods).** Add one entry per payment method. Each transaction gets its own `payment_url`, and the methods appear in array order on the hosted page:

```json
"transactions": [
  { "payment_method": "credit-card" },
  { "payment_method": "apple-pay" }
]
```

> [!NOTE]
> The `payment_methods` field on orders accepts at most one value. To offer multiple specific methods, always use the `transactions` array. If you need a reusable link with multiple payment methods, consider [Payment Links](/docs/guides/payment-links) instead, which support a true `payment_methods` array.

## Return URLs

After payment, the customer is redirected based on the order status and which URLs you provided:

- **When both `return_url` and `failure_url` are set:**
  - `cancelled`, `expired`, or `error` → customer is redirected to `failure_url`
  - All other statuses → customer is redirected to `return_url`

- **When only `return_url` is set:**
  - All statuses → customer is redirected to `return_url`

> [!TIP]
> Use `failure_url` to show a retry or support page for failed payments, while `return_url` shows an order confirmation. If you only need one destination, `return_url` alone is sufficient.

## Cancel Button Behavior

The hosted payment page includes a cancel button. When the customer clicks it, they are redirected to `failure_url` (if provided) or `return_url`. The order status will transition to `cancelled`. Always verify the order status via the API or webhooks rather than relying on the redirect alone.

## Related Endpoints

- [Create Order](/api-reference/orders/createOrder) — create a payment order and receive the `payment_url`
- [Get Order](/api-reference/orders/getOrder) — check the order status after payment


---

## One-Click Payments (CIT)
<https://docs.costplus.io/docs/guides/one-click-payments>

> Enable customer-initiated one-click payments with card tokenization

One-click payments allow returning customers to pay with a single click using a previously stored card. This is a **Customer Initiated Transaction (CIT)** flow that uses tokenization.

## How It Works

The one-click flow has two phases:

1. **First payment** — The customer pays normally, and you request a token for future use
2. **Subsequent payments** — Use the stored token to charge the customer with one click

## Phase 1: First Payment (Tokenization)

Create an order with `one_click_type: "first"` in the transaction to request tokenization:

```json title="POST /v1/orders/"
{
  "merchant_order_id": "first-order",
  "currency": "EUR",
  "amount": 1295,
  "return_url": "https://www.example.com",
  "transactions": [
    {
      "payment_method": "credit-card",
      "one_click_type": "first"
    }
  ]
}
```

After a successful payment, the response will include a `vault_token` and `first_transaction_id` in the transaction object. **Store these values** — you'll need them for future one-click payments.

```json title="Response (transaction object)"
{
  "payment_method": "credit-card",
  "payment_method_details": {
    "vault_token": "abc123-stored-token",
    "first_authorised_transaction_id": "txn_def456"
  }
}
```

## Phase 2: One-Click Payment

Use the stored `vault_token` to create a one-click payment:

```json title="POST /v1/orders/"
{
  "merchant_order_id": "oneclick-order",
  "currency": "EUR",
  "amount": 995,
  "return_url": "https://www.example.com",
  "transactions": [
    {
      "payment_method": "credit-card",
      "one_click_type": "one-click",
      "vault_token": "{vault_token from phase 1}"
    }
  ]
}
```

> [!TIP]
> You can use either `vault_token` or `first_transaction_id` to reference the stored card. Both work interchangeably.

## CVC Handling

CVC is **optional** for one-click payments. If you want to collect CVC for additional security, include it in the transaction:

```json
{
  "payment_method": "credit-card",
  "one_click_type": "one-click",
  "vault_token": "{vault_token}",
  "cvc": "123"
}
```

> [!NOTE]
> One-click payments are customer-initiated (CIT). For merchant-initiated recurring charges (MIT), see [Recurring Payments](/docs/guides/recurring-payments).

## Related Endpoints

- [Create Order](/api-reference/orders/createOrder) — use `one_click_type` in the transaction to initiate tokenization or one-click payments


---

## Payment Links
<https://docs.costplus.io/docs/guides/payment-links>

> Create reusable payment links

Payment links are reusable URLs that allow customers to pay for an order. Unlike standard orders that expire after a single failed attempt, payment links support multiple retry attempts, making them ideal for invoices, email-based payments, and scenarios where the customer may not pay immediately.

## Key Features

- **Reusable:** Customers can retry payment up to 25 times if previous attempts fail.
- **Long-lived:** Default expiration is 30 days (configurable via `expiration_period`).
- **Shareable:** Send the link via email, SMS, chat, or embed it on your website.

## Creating a Payment Link

Send a **POST** request to `/v1/paymentlinks/`:

```bash title="POST /v1/paymentlinks/"
curl -X POST https://api.costplus.online/v1/paymentlinks/ \
  -u YOUR_API_KEY: \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_order_id": "invoice-1234",
    "amount": 995,
    "currency": "EUR",
    "description": "Invoice #1234"
  }'
```

The response includes the `payment_url` to share with the customer and a unique `id` for tracking:

```json title="Response (201 Created)"
{
  "id": "e6eecc6a-47c5-4948-bcc0-d8b73f5c55a1",
  "merchant_order_id": "invoice-1234",
  "amount": 995,
  "currency": "EUR",
  "description": "Invoice #1234",
  "expiration_period": "P30D",
  "payment_url": "https://api.costplus.online/paymentlinks/e6eecc6a.../",
  "status": "new",
  "reason": "Payment Link was created, not yet visited",
  "orders": {},
  "created": "2026-01-15T12:00:00.000000Z"
}
```

Save the `id` — you'll use it to check the payment link status later.

### Required Fields

| Field | Description |
| --- | --- |
| `merchant_order_id` | Your own reference ID for the payment link |
| `amount` | Amount in cents (e.g., 9.95 EUR = `995`) |
| `currency` | ISO 4217 currency code (e.g., `EUR`, `GBP`) |

### Optional Fields

| Field | Description |
| --- | --- |
| `description` | Description shown to the customer |
| `expiration_period` | ISO 8601 duration. Default is `P30D` (30 days) |
| `return_url` | URL to redirect the customer after successful payment |
| `failure_url` | URL to redirect the customer on cancellation, expiry, or error |
| `webhook_url` | URL to receive status change notifications |
| `customer` | Customer details object (name, email, etc.) |

> [!TIP]
> If you provide both `return_url` and `failure_url`, customers are redirected to `failure_url` when the order status is `cancelled`, `expired`, or `error`. Otherwise, all redirects go to `return_url`.

## Retrieving a Payment Link

Send a **GET** request to `/v1/paymentlinks/{id}/` using the payment link `id` from the create response:

```bash title="GET /v1/paymentlinks/{id}/"
curl -u YOUR_API_KEY: \
  https://api.costplus.online/v1/paymentlinks/e6eecc6a-47c5-4948-bcc0-d8b73f5c55a1/
```

The response includes the current status and references to all orders created from the link, grouped by their status:

```json title="Response (completed example)"
{
  "id": "e6eecc6a-47c5-4948-bcc0-d8b73f5c55a1",
  "merchant_order_id": "invoice-1234",
  "amount": 995,
  "currency": "EUR",
  "description": "Invoice #1234",
  "expiration_period": "P30D",
  "payment_url": "https://api.costplus.online/paymentlinks/e6eecc6a.../",
  "status": "completed",
  "reason": "Completed",
  "completed": "2026-01-15T12:05:30.123456+00:00",
  "completed_order_id": "3bb663cc-2a20-400d-8bf6-18d9695d0c66",
  "orders": {
    "error": ["0d79014c-0aaa-4fd6-87c5-c8cfa5f5ac69"],
    "completed": ["3bb663cc-2a20-400d-8bf6-18d9695d0c66"]
  }
}
```

In this example, the customer made one failed attempt (order `0d79014c...` in `error` status) before succeeding (order `3bb663cc...` in `completed` status). You can retrieve full details of any order via `GET /v1/orders/{order_id}/`.

## Payment Link Statuses

| Status | Description |
| --- | --- |
| `new` | The link has been created but no payment attempt has been made. |
| `processing` | A payment attempt is currently in progress. |
| `all_unsuccessful` | All payment attempts so far have failed. The customer can still retry (up to 25 attempts). |
| `completed` | Payment was successful. The link is no longer active. |
| `expired` | The link has expired before a successful payment was made. |

> [!NOTE]
> The `all_unsuccessful` status is not a final status. The customer can still attempt to pay again until either the payment succeeds, the maximum number of attempts (25) is reached, or the link expires.

> [!WARNING]
> Once a payment link reaches the `completed` or `expired` status, it cannot be used again. Create a new payment link if the customer needs to pay again.

## Example Workflow

1. Create a payment link via `POST /v1/paymentlinks/`.
2. Share the returned `payment_url` with your customer (e.g., via email, SMS, or invoice).
3. The customer opens the link and completes payment.
4. Cost+ sends a webhook to your `webhook_url` when the status changes.
5. Verify the payment link status via `GET /v1/paymentlinks/{id}/`.
6. Fulfill the order once the status is `completed`.

## Related Endpoints

- [Create Payment Link](/api-reference/payment-links/createPaymentLink) — create a reusable payment link
- [Get Payment Link](/api-reference/payment-links/getPaymentLink) — retrieve the current status of a payment link


---

## Recurring Payments (MIT)
<https://docs.costplus.io/docs/guides/recurring-payments>

> Set up merchant-initiated recurring payments and subscriptions

Recurring payments allow you to charge customers on a schedule without their active participation. This is a **Merchant Initiated Transaction (MIT)** flow.

## How It Works

The recurring flow has two phases:

1. **First payment** — The customer authenticates and pays, granting permission for future charges
2. **Subsequent payments** — You charge the customer's stored card without their interaction

## Phase 1: First Payment

Create an order with `recurring_type: "first"` and a `schedule_type`:

```json title="POST /v1/orders/"
{
  "merchant_order_id": "first-recurring",
  "currency": "EUR",
  "amount": 1295,
  "return_url": "https://www.example.com",
  "transactions": [
    {
      "payment_method": "credit-card",
      "recurring_type": "first",
      "schedule_type": "scheduled"
    }
  ]
}
```

### Schedule Types

| Type | Description |
|------|-------------|
| `scheduled` | Fixed schedule (e.g., monthly subscription) |
| `unscheduled` | Variable timing (e.g., top-up when balance is low) |

After successful payment, store the `vault_token` and/or `first_transaction_id` from the response.

## Phase 2: Subsequent Recurring Payment

Charge the customer using the stored token:

```json title="POST /v1/orders/"
{
  "merchant_order_id": "recurring-order",
  "currency": "EUR",
  "amount": 995,
  "transactions": [
    {
      "payment_method": "credit-card",
      "recurring_type": "recurring",
      "vault_token": "{vault_token}"
    }
  ]
}
```

> [!NOTE]
> Recurring payments do **not** return a `payment_url` in the response since no customer interaction is needed. The payment is processed immediately.

> [!TIP]
> You can use either `vault_token` or `first_transaction_id` to reference the stored card.

## Token Validity

Recurring tokens have a **maximum validity of 1 year**. After expiry, you must initiate a new first payment to obtain a fresh token.

> [!WARNING]
> Make sure your system handles token expiry gracefully. Set up a process to re-authenticate customers before their tokens expire.

## Recurring vs One-Click

| Feature | Recurring (MIT) | One-Click (CIT) |
|---------|----------------|-----------------|
| Initiated by | Merchant | Customer |
| Customer present | No | Yes |
| Use case | Subscriptions, scheduled billing | Fast checkout for returning customers |
| `schedule_type` required | Yes | No |
| `payment_url` returned | No | Yes |

## Related Endpoints

- [Create Order](/api-reference/orders/createOrder) — use `recurring_type` and `schedule_type` in the transaction to set up or charge recurring payments


---

## Refunds
<https://docs.costplus.io/docs/guides/refunds>

> Process full and partial refunds

Cost+ supports both full and partial refunds on completed orders. Refunds are processed through the original payment method used for the transaction.

## Creating a Refund

Send a **POST** request to `/v1/orders/\{id\}/refunds/` to initiate a refund on a completed order.

### Request

```json
{
  "amount": 100,
  "description": "Refund for item",
  "merchant_order_id": "refund-001"
}
```

> [!NOTE]
> The `amount` field is in cents. To refund 1.00 EUR, set `amount` to `100`. To issue a full refund, set the amount to the full order amount.

### Request Fields

| Field | Required | Description |
| --- | --- | --- |
| `amount` | Yes | Refund amount in cents |
| `description` | No | Reason for the refund |
| `merchant_order_id` | No | Your own reference ID for the refund |
| `extra` | No | Additional metadata as a key-value object |

### Response

The API returns the refund object with its current status:

```json
{
  "id": "ref_abc123...",
  "created": "2024-01-02T10:00:00.000000+00:00",
  "modified": "2024-01-02T10:00:00.000000+00:00",
  "amount": 100,
  "currency": "EUR",
  "status": "pending",
  "description": "Refund for item",
  "merchant_order_id": "refund-001",
  "extra": null
}
```

### Response Fields

| Field | Description |
| --- | --- |
| `id` | Unique identifier for the refund |
| `created` | Timestamp when the refund was created |
| `modified` | Timestamp when the refund was last updated |
| `amount` | Refund amount in cents |
| `currency` | Currency of the refund (matches the order currency) |
| `status` | Current refund status (`pending`, `completed`, `failed`) |
| `description` | Reason for the refund |
| `merchant_order_id` | Your reference ID |
| `extra` | Additional metadata |

## Full vs. Partial Refunds

- **Full refund:** Set the `amount` to the total order amount. The entire payment is returned to the customer.
- **Partial refund:** Set the `amount` to less than the total order amount. Only the specified amount is returned.

You can issue multiple partial refunds on the same order, as long as the total refunded amount does not exceed the original order amount.

```json
{
  "amount": 500,
  "description": "Partial refund - damaged item"
}
```

> [!WARNING]
> Refunds can only be issued on orders with a `completed` status. Attempting to refund an order that is not yet completed will result in an error.

> [!TIP]
> Use the `merchant_order_id` field to link refunds back to your internal systems. This is especially useful when issuing multiple partial refunds on the same order.

## Refund Statuses

| Status | Description |
| --- | --- |
| `pending` | The refund has been initiated and is being processed. |
| `completed` | The refund has been successfully processed. Funds will be returned to the customer. |
| `failed` | The refund could not be processed. Contact Cost+ support for assistance. |

> [!NOTE]
> The time it takes for the refunded amount to appear in the customer's account depends on the payment method and the customer's bank. Credit card refunds typically take 5-10 business days.

### Checking Refundable Amount

Before issuing a partial refund, you can check how much is available:

```bash
curl -u YOUR_API_KEY: \
  "https://api.costplus.online/v1/orders/{order_id}/?fields[]=amount_details"
```

The `refundable` field in the response shows the maximum amount you can refund.

## Related Endpoints

- [Create Refund](/api-reference/orders/createRefund) — issue a full or partial refund on a completed order
- [List Refunds](/api-reference/orders/listRefunds) — retrieve all refunds for an order


---

## Status Requests
<https://docs.costplus.io/docs/guides/status-requests>

> Check order and transaction statuses

Every payment in Cost+ is represented by an **order** that contains one or more **transactions**. You can check the current status of any order by querying the API, either by polling or by relying on [webhooks](/docs/guides/webhooks).

## Retrieving Order Status

Send a **GET** request to `/v1/orders/\{id\}/` to retrieve the full order object including its current status and all associated transactions.

```bash
GET /v1/orders/b9ae6.../
```

> [!TIP]
> While polling works for checking order status, webhooks are the recommended approach for production integrations. They provide real-time notifications without the overhead of repeated API calls.

## Order Statuses

An order progresses through the following statuses:

| Status | Final | Description |
| --- | --- | --- |
| `new` | No | Order has just been created. No payment attempt has been made yet. |
| `processing` | No | A payment attempt is in progress. The customer may be completing 3D Secure or another verification step. |
| `error` | No | The payment attempt failed. The customer can retry with the same or a different payment method. |
| `completed` | Yes | Payment was successful. You can fulfill the order. |
| `cancelled` | Yes | The order was cancelled, either by the customer or via the API. |
| `expired` | Yes | The order expired before a successful payment was made. The default expiration period is 30 minutes. |

> [!NOTE]
> Only statuses marked as **Final = Yes** are terminal. Orders in `new`, `processing`, or `error` status can still transition to `completed`.

## Example: Order in Processing

When a customer has initiated payment but it is not yet complete:

```json
{
  "id": "b9ae6...",
  "project_id": "proj_abc123",
  "merchant_order_id": "my-order-id-1",
  "created": "2024-01-01T12:00:00.000000+00:00",
  "modified": "2024-01-01T12:01:30.000000+00:00",
  "completed": null,
  "expiration_period": "PT30M",
  "status": "processing",
  "currency": "EUR",
  "amount": 1295,
  "description": "My amazing order",
  "return_url": "https://www.example.com",
  "payment_url": "https://api.costplus.online/pay/...",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    {
      "id": "txn_001...",
      "payment_method": "credit-card",
      "payment_method_brand": "visa",
      "status": "processing",
      "amount": 1295,
      "currency": "EUR"
    }
  ],
  "flags": ["is-test"]
}
```

## Example: Completed Order

Once the payment is successful, the order reaches the `completed` status:

```json
{
  "id": "b9ae6...",
  "project_id": "proj_abc123",
  "merchant_order_id": "my-order-id-1",
  "created": "2024-01-01T12:00:00.000000+00:00",
  "modified": "2024-01-01T12:02:15.000000+00:00",
  "completed": "2024-01-01T12:02:15.000000+00:00",
  "expiration_period": "PT30M",
  "status": "completed",
  "currency": "EUR",
  "amount": 1295,
  "description": "My amazing order",
  "return_url": "https://www.example.com",
  "payment_url": "https://api.costplus.online/pay/...",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    {
      "id": "txn_001...",
      "payment_method": "credit-card",
      "payment_method_brand": "visa",
      "payment_method_details": {
        "card_last_four": "4242",
        "card_expiry_month": 12,
        "card_expiry_year": 2026,
        "card_holder_name": "J. Smith"
      },
      "status": "completed",
      "amount": 1295,
      "currency": "EUR"
    }
  ],
  "flags": ["is-test"]
}
```

## Transaction Details

Each transaction within an order contains the following key fields:

| Field | Description |
| --- | --- |
| `payment_method` | The payment method used (e.g., `credit-card`, `ideal`, `apple-pay`) |
| `payment_method_brand` | The brand or issuer (e.g., `visa`, `mastercard`, `amex`) |
| `payment_method_details` | An object with method-specific details such as card last four digits, expiry, and holder name |
| `status` | The status of this specific transaction |
| `amount` | The transaction amount in cents |
| `currency` | The transaction currency |

> [!WARNING]
> Do not rely solely on the customer being redirected to your `return_url` as confirmation of payment. Always verify the order status via the API or through a webhook before fulfilling an order.

## Related Endpoints

- [Get Order](/api-reference/orders/getOrder) — retrieve the full order object and its current status
- [List Orders](/api-reference/orders/listOrders) — list orders with date range filtering


---

## Webhooks
<https://docs.costplus.io/docs/guides/webhooks>

> Receive real-time payment status notifications

Webhooks allow Cost+ to notify your server in real time whenever an order's status changes. Instead of polling the API, you provide a URL and Cost+ sends an HTTP POST request to it whenever there is an update.

## How Webhooks Work

1. You specify a `webhook_url` when creating an order, or configure a default webhook URL at the project level.
2. When the order status changes (e.g., from `new` to `completed`), Cost+ sends a POST request to your webhook URL.
3. Your server receives the webhook, then calls the Cost+ API to retrieve the full order details.
4. You update your system based on the verified order status.

> [!WARNING]
> Never trust the webhook payload alone to fulfill an order. Always verify the order status by making a **GET /v1/orders/\{id\}/** request after receiving a webhook. The webhook is a notification mechanism, not a source of truth.

## Webhook Payload

The webhook body is a JSON object containing the event type and the order ID:

```json
{
  "event": "status_changed",
  "order_id": "b9ae6...",
  "project_id": "proj_abc123"
}
```

| Field | Description |
| --- | --- |
| `event` | The type of event. Currently `status_changed` for order updates. |
| `order_id` | The unique identifier of the order whose status changed. |
| `project_id` | The project that the order belongs to. |

## Setting the Webhook URL

You can set the webhook URL in two ways:

### Per-Order

Include the `webhook_url` field when creating an order:

```json
{
  "currency": "EUR",
  "amount": 1295,
  "webhook_url": "https://www.example.com/webhook"
}
```

### Project Default

Configure a default webhook URL in your Cost+ dashboard under project settings. This URL will be used for all orders that do not specify their own `webhook_url`.

> [!TIP]
> Even if you have a project-level default, you can override it on a per-order basis by including `webhook_url` in the order creation request.

## Retry Logic

If your server does not respond with a `2xx` status code, Cost+ retries the webhook delivery:

- **Maximum retries:** 10 attempts
- **Retry interval:** 2 minutes between each attempt
- **First attempt timeout:** 4 seconds
- **Subsequent attempt timeout:** 10 seconds

If all 10 retry attempts fail, the webhook is marked as failed. You can still retrieve the order status at any time by polling the API.

> [!NOTE]
> Make sure your webhook endpoint responds quickly (within 4 seconds for the first attempt). Perform any long-running processing asynchronously after returning a `200 OK` response.

## Best Practices

- **Always verify via API.** Upon receiving a webhook, call `GET /v1/orders/\{order_id\}/` to confirm the current status before taking action such as shipping goods or granting access.
- **Return 200 quickly.** Acknowledge the webhook with a `200` response immediately and process the order asynchronously to avoid timeouts.
- **Handle duplicates.** Your webhook endpoint may receive the same event more than once. Ensure your processing logic is idempotent.
- **Use HTTPS.** Always use an HTTPS URL for your webhook endpoint to ensure the payload is transmitted securely.
- **Log webhook payloads.** Store incoming webhook data for debugging and audit purposes.

## Related Endpoints

- [Create Order](/api-reference/orders/createOrder) — set the `webhook_url` per order
- [Update Order](/api-reference/orders/updateOrder) — update the webhook URL on an existing order
- [Webhook Event Schemas](/api-reference/webhooks) — payload format for `OrderStatusChangedEvent` and `TransactionStatusChangedEvent`


---

## Apple Pay
<https://docs.costplus.io/docs/payment-methods/apple-pay>

> Accept Apple Pay payments

## Overview

Accept Apple Pay payments through the Cost+ API using the `apple-pay` payment method.

## Supported Card Networks

| Brand | Type |
|-------|------|
| Amex | Credit |
| Mastercard | Credit / Debit |
| Maestro | Debit |
| Visa | Credit / Debit |
| V Pay | Debit |

## Request Example

To accept an Apple Pay payment, specify `apple-pay` as the `payment_method` in your order's transaction:

```json
{
  "currency": "EUR",
  "amount": 2500,
  "merchant_order_id": "order-ap-001",
  "transactions": [
    {
      "payment_method": "apple-pay"
    }
  ],
  "return_url": "https://example.com/return",
  "webhook_url": "https://example.com/webhook"
}
```

## Testing

Apple Pay **cannot be tested in test mode**. This is an Apple restriction. You must use a live environment to verify Apple Pay integration.

To test Apple Pay, you will need:

- A real Apple device (iPhone, iPad, or Mac with Touch ID / Face ID)
- A valid card added to Apple Wallet
- A live (non-test) Cost+ project

## Integration Notes

- Apple Pay is available on Safari and in native iOS/macOS apps.
- Customers authenticate using Face ID, Touch ID, or their device passcode.
- The payment flow is handled through the Cost+ hosted payment page or via the Apple Pay JS API if you are building a custom integration.


---

## Cards (Visa & Mastercard)
<https://docs.costplus.io/docs/payment-methods/cards>

> Accept credit and debit card payments

## Overview

Accept credit and debit card payments through the Cost+ API using the `credit-card` payment method.

```json
{
  "transactions": [
    {
      "payment_method": "credit-card"
    }
  ]
}
```

## Supported Card Brands

| Brand | Type |
|-------|------|
| Amex | Credit |
| Mastercard | Credit / Debit |
| Maestro | Debit |
| Visa | Credit / Debit |
| V Pay | Debit |

## Configuration Options

### Dynamic Descriptor

Use the `dynamic_descriptor` field to set a custom statement text that appears on your customer's bank or card statement.

```json
{
  "transactions": [
    {
      "payment_method": "credit-card",
      "payment_method_details": {
        "dynamic_descriptor": "My Store Order 123"
      }
    }
  ]
}
```

### Use Customer Name as Cardholder Name

Set `use_customer_name_as_cardholder_name` to `true` to automatically use the customer's name from the order as the cardholder name.

```json
{
  "transactions": [
    {
      "payment_method": "credit-card",
      "payment_method_details": {
        "use_customer_name_as_cardholder_name": true
      }
    }
  ]
}
```

## Custom Card Entry Form

If you want to build your own card entry form instead of using the hosted payment page, follow these four steps.

### Step 1: Create an Order with a Setup Token

Create an order and include `setup_token: true` in the transaction's `payment_method_details`. This tells Cost+ to generate a setup token you can use to securely tokenize card details.

```bash
curl -X POST https://api.costplus.online/v1/orders \
  -u your-api-key: \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "EUR",
    "amount": 5000,
    "merchant_order_id": "order-001",
    "transactions": [
      {
        "payment_method": "credit-card",
        "payment_method_details": {
          "setup_token": true
        }
      }
    ],
    "return_url": "https://example.com/return",
    "webhook_url": "https://example.com/webhook"
  }'
```

The response will include a `setup_token` value in the transaction's `payment_method_details`:

```json
{
  "id": "order-uuid",
  "transactions": [
    {
      "id": "txn-uuid",
      "payment_method": "credit-card",
      "payment_method_details": {
        "setup_token": "st_abc123..."
      }
    }
  ]
}
```

### Step 2: Tokenize Card Details

Send the card PAN, expiry date, and the setup token to the token endpoint. This securely vaults the card and returns a `vault_token`.

```bash
curl -X POST https://api.costplus.online/v1/tokens/ \
  -H "Content-Type: application/json" \
  -d '{
    "pan": "4111111111111111",
    "expiry_date": "1228",
    "setup_token": "st_abc123..."
  }'
```

Response:

```json
{
  "vault_token": "vt_xyz789..."
}
```

### Step 3: Authenticate the Transaction

Submit the `vault_token` and `cvc` to the authenticate endpoint. If 3D Secure is required, you will receive a `redirect_url` to redirect the customer to their bank's authentication page.

```bash
curl -X POST https://api.costplus.online/v1/orders/{order_id}/transactions/{transaction_id}/authenticate/ \
  -u your-api-key: \
  -H "Content-Type: application/json" \
  -d '{
    "vault_token": "vt_xyz789...",
    "cvc": "123"
  }'
```

Response:

```json
{
  "redirect_url": "https://3ds.bank.example.com/auth?id=..."
}
```

Redirect the customer to `redirect_url` to complete 3D Secure authentication. After the customer completes (or cancels) authentication, they will be redirected back to your `return_url`.

### Step 4: Poll Order Status

After the customer returns from 3D Secure, poll the order to check the final status.

```bash
curl -X GET https://api.costplus.online/v1/orders/{order_id} \
  -u your-api-key:
```

The order `status` will transition to one of:

| Status | Meaning |
|--------|---------|
| `completed` | Payment was successful |
| `cancelled` | Customer cancelled or authentication failed |
| `error` | An error occurred during processing |
| `expired` | The order expired before completion |

> [!TIP]
> For manual authorization and capture flows with card payments, see the [Auth / Capture / Void](/docs/guides/auth-capture-void) guide.


---

## Google Pay
<https://docs.costplus.io/docs/payment-methods/google-pay>

> Accept Google Pay payments

Google Pay enables fast, secure checkout using cards stored in a customer's Google account.

## Configuration

| Setting | Value |
|---------|-------|
| `payment_method` | `"google-pay"` |
| Tokenization gateway | `"ginger"` |
| Supported networks | Amex, Mastercard, Maestro, Visa, V Pay |

## Authentication Methods

| Method | Description |
|--------|-------------|
| `PAN_ONLY` | Uses the card number stored in Google. **Requires 3D Secure authentication.** |
| `CRYPTOGRAM_3DS` | Uses a device token with a 3DS cryptogram. No additional authentication needed. |

## Creating an Order

```json title="POST /v1/orders/"
{
  "merchant_order_id": "order-123",
  "currency": "EUR",
  "amount": 2500,
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    {
      "payment_method": "google-pay"
    }
  ]
}
```

Redirect the customer to the `payment_url` in the response to complete the Google Pay flow.

> [!WARNING]
> You must follow [Google Pay's brand guidelines](https://developers.google.com/pay/api/web/guides/brand-guidelines) and [acceptable use policies](https://payments.developers.google.com/terms/aup) when integrating Google Pay.


---

## Vipps / MobilePay
<https://docs.costplus.io/docs/payment-methods/vipps-mobilepay>

> Accept Vipps and MobilePay mobile payments

Vipps (Norway) and MobilePay (Denmark/Finland) are popular Nordic mobile payment methods, now unified under one integration.

## Configuration

| Setting | Value |
|---------|-------|
| `payment_method` | `"vipps-mobilepay"` |

## Creating an Order

```json title="POST /v1/orders/"
{
  "merchant_order_id": "order-456",
  "currency": "NOK",
  "amount": 29900,
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    {
      "payment_method": "vipps-mobilepay"
    }
  ],
  "customer": {
    "phone_numbers": ["+4712345678"]
  }
}
```

> [!TIP]
> Include `phone_numbers` in the `customer` object to pre-fill the customer's phone number in the Vipps/MobilePay app, reducing friction.

## Authorization & Capture

Vipps/MobilePay supports **manual authorization** with separate capture:

```json title="POST /v1/orders/ (with authorization)"
{
  "currency": "NOK",
  "amount": 29900,
  "capture_mode": "manual",
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    {
      "payment_method": "vipps-mobilepay"
    }
  ]
}
```

### Capture Options

After authorization, capture using any of these methods:

| Endpoint | Description |
|----------|-------------|
| `POST /v1/orders/\{id\}/captures/` | Full capture |
| `POST /v1/orders/\{id\}/captures/amount/` | Capture specific amount |
| `POST /v1/orders/\{id\}/captures/orderlines/` | Capture by order line |

## Refunds

Refund a Vipps/MobilePay payment:

```json title="POST /v1/orders/\{id\}/refunds/"
{
  "amount": 10000,
  "description": "Partial refund"
}
```

## Testing

Vipps/MobilePay testing is done via the Vipps test environment. Contact support for test credentials and environment setup.

> [!TIP]
> For more details on capture and void operations, see the [Auth / Capture / Void](/docs/guides/auth-capture-void) guide. For refund processing details, see [Refunds](/docs/guides/refunds).


---

## Plugins & Integrations
<https://docs.costplus.io/docs/plugins>

> Integrate Cost+ with your e-commerce platform

Cost+ provides native plugins for popular e-commerce platforms. Choose your platform below to get started.

## Supported Platforms

<div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '1rem', marginTop: '1rem'}}>
  <a href="/docs/plugins/shopify" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/shopify.png" alt="Shopify" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/woocommerce" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/woocommerce.png" alt="WooCommerce" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/prestashop" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/prestashop.png" alt="PrestaShop" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/magento" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/magento.png" alt="Magento" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/modified-ecommerce" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/modified-ecommerce.png" alt="modified eCommerce" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/opencart" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/opencart.png" alt="OpenCart" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/shopware" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/shopware.png" alt="Shopware" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/nopcommerce" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/nopcommerce.png" alt="nopCommerce" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/drupal-commerce" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/drupal-commerce.png" alt="Drupal Commerce" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/oscommerce" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/oscommerce.png" alt="osCommerce" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#22c55e', fontSize: '0.875rem', fontWeight: 500}}>Available</span>
  </a>
  <a href="/docs/plugins/wix" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/wix.png" alt="Wix" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#6b7280', fontSize: '0.875rem', fontWeight: 500}}>Coming Soon</span>
  </a>
  <a href="/docs/plugins/ecwid" style={{background: 'white', borderRadius: '12px', padding: '1.5rem', border: '1px solid #e5e7eb', textDecoration: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)'}}>
    <img src="/images/logos/ecwid.png" alt="Ecwid" style={{height: '40px', width: 'auto'}} />
    <span style={{color: '#6b7280', fontSize: '0.875rem', fontWeight: 500}}>Coming Soon</span>
  </a>
</div>

## Supported Payment Methods

All plugins support the following payment methods (subject to your merchant account approval):

- **Credit/Debit Cards** -- Visa, Mastercard, and more
- **Google Pay** -- Fast checkout with Google Wallet
- **Apple Pay** -- Seamless payments on Apple devices
- **MobilePay** -- Trusted by Nordic users
- **Swish** -- Popular in Sweden

## Custom Integration

If your platform isn't listed above, you can integrate directly using our [API Reference](/api-reference). The API supports any platform that can make HTTP requests.

See the [Hosted Payment Page](/docs/guides/hosted-payment-page) guide for the simplest integration path.


---

## Drupal Commerce
<https://docs.costplus.io/docs/plugins/drupal-commerce>

> Integrate Cost+ with your Drupal Commerce store using the official payment gateway module

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/drupal-commerce.png" alt="Drupal Commerce" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your Drupal Commerce store. The official **NoPayn Payment Gateway** module uses the Hosted Payment Page flow, so no card data touches your server — fully PCI DSS compliant.

## Prerequisites

- Active Cost+ merchant account
- Drupal 10 or 11
- Drupal Commerce 3.x
- PHP 8.1 or later
- Admin access to your Drupal site

## Supported Payment Methods

- **Credit / Debit Card** — Visa, Mastercard, and more
- **Apple Pay**
- **Google Pay**
- **Vipps / MobilePay**

## 1. Install the Module

Copy the `commerce_nopayn` module into your Drupal installation and enable it:

```bash
cp -r commerce_nopayn /path/to/drupal/web/modules/custom/
drush en commerce_nopayn -y
drush cr
```

> [!NOTE]
> You can also download the module directly from the [GitHub repository](https://github.com/NoPayn/nopayn-drupal-commerce).

## 2. Add a Payment Gateway

1. Navigate to **Commerce → Configuration → Payment gateways** (`/admin/commerce/config/payment-gateways`)
2. Click **Add payment gateway**
3. Select **NoPayn Payment Gateway** as the plugin

## 3. Configure the Gateway

Enter the following settings:

- **NoPayn API Key** — Your merchant API key from the [Merchant Portal](https://dashboard.costplus.io/)
- **Payment Methods** — Enable or disable Credit Card, Apple Pay, Google Pay, Vipps MobilePay
- **Mode** — Set to **Test** when using an API key from a sandbox website, or **Live** when using a production website key

> [!WARNING]
> Only activate the payment methods you have been approved for and received confirmation for.

### Optional Settings

- **Manual Capture** — Authorize credit card payments without immediate capture (funds are reserved, then captured when the order is fulfilled)
- **Debug Logging** — Enable to log all API requests and responses to the Drupal log for troubleshooting

Click **Save** when done.

## 4. Payment Flow

Once configured, the payment flow works as follows:

1. Customer adds products to cart and proceeds to checkout
2. At the payment step, enabled Cost+ methods are shown as radio buttons
3. Customer selects a method and confirms the order
4. The module creates an order via the Cost+ API and redirects to the hosted payment page
5. Customer completes payment on the Cost+ page
6. Customer returns to the store — payment is verified via API
7. A webhook from Cost+ asynchronously confirms the payment result

> [!TIP]
> Payment sessions expire after 5 minutes per Cost+ standards. The module also sends itemized order lines (products and shipping) to Cost+ for detailed transaction records.

## 5. Webhook Configuration

The webhook URL is automatically generated by Drupal Commerce:

```
https://your-store.com/payment/notify/{gateway_machine_name}
```

For example: `https://your-store.com/payment/notify/nopayn`

This URL must be accessible from the internet without authentication. The module always verifies payment status via the API — it never trusts the webhook payload alone.

## Manual Capture

When manual capture is enabled for credit cards:

1. Customer's card is **authorized** (funds reserved) during checkout
2. Capture occurs automatically when the order transitions to **Fulfilled**
3. If the order is cancelled, the authorization is **voided** via webhook

> [!TIP]
> Use manual capture if you want to only charge customers when their order ships. This is useful for merchants with longer fulfillment times.

## Refunds

Refunds can be processed directly from the Commerce admin panel. Both full and partial refunds are supported.

## Database Tables

The module creates two tables for tracking:

| Table | Purpose |
|---|---|
| `nopayn_transactions` | Tracks payment orders created with the Cost+ API |
| `nopayn_refunds` | Records refund operations for audit purposes |

## Uninstallation

```bash
drush pmu commerce_nopayn -y
drush cr
```

> [!CAUTION]
> Uninstalling the module will drop the `nopayn_transactions` and `nopayn_refunds` tables. Make sure to export any data you need before uninstalling.

## Test and Launch

Place a few test transactions to ensure everything works smoothly. Set the gateway **Mode** to **Test** and verify both successful and failed payments before switching to **Live**.

## Support

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


---

## Ecwid
<https://docs.costplus.io/docs/plugins/ecwid>

> Integrate Cost+ with your Ecwid store

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/ecwid.png" alt="Ecwid" style={{height: '48px', width: 'auto'}} />
</div>

## Coming Soon

The Cost+ plugin for Ecwid is currently in development. Full setup documentation will be available here once the integration is released.

In the meantime, you can integrate Cost+ with your Ecwid store using our [API Reference](/api-reference) and [Hosted Payment Page](/docs/guides/hosted-payment-page) guide.

For questions or early access, contact our support team at **support@costplus.io**.


---

## Magento
<https://docs.costplus.io/docs/plugins/magento>

> Integrate Cost+ with your Magento store (2.x and 1.9 / OpenMage LTS)

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/magento.png" alt="Magento" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your Magento store. The Cost+ extension is available for **Magento 2** (2.4.6 – 2.4.8) and **Magento 1.9** (including OpenMage LTS 20+).

## Prerequisites

#### Magento 2

- Active Cost+ merchant account
- Magento 2.4.6 – 2.4.8 installation
- PHP 8.2 – 8.4
- Admin access to your Magento admin panel
- SSH access to your server (for Composer installation)

#### Magento 1.9

- Active Cost+ merchant account
- Magento 1.9.x or OpenMage LTS 20+
- PHP 7.4 or later
- Admin access to your Magento admin panel
- SSH or FTP access to your server

## 1. Install the Extension

#### Magento 2

### Method A: Installation via Composer (Recommended)

Connect to your Magento server via SSH and navigate to your Magento root directory.

Install the plugin:

```bash
composer require nopayn/nopayn-magento-2
```

Run Magento setup commands:

```bash
php bin/magento setup:upgrade
php bin/magento module:enable GingerPay_Payment
php bin/magento cache:clean
```

If you are in **production mode**, also run:

```bash
php bin/magento setup:static-content:deploy
```

### Method B: Manual Installation

1. Navigate to your `app/code` directory
2. Download and unzip the [Cost+ release from GitHub](https://github.com/NoPayn/nopayn-magento-2)
3. Run the setup commands:

```bash
php bin/magento setup:upgrade
php bin/magento cache:clean
```

If in production mode:

```bash
php bin/magento setup:static-content:deploy
```

#### Magento 1.9

Download or clone the module from [GitHub](https://github.com/NoPayn/magento1_9).

Copy the contents of `app/` into your Magento root `app/` directory:

```bash
cp -r app/* /path/to/magento/app/
```

Clear the Magento cache:

```bash
rm -rf var/cache/*
```

Log in to the Magento admin panel and navigate to **System → Configuration → Sales → Payment Methods** to verify the **NoPayn Payment Gateway** section appears.

> [!NOTE]
> The module creates a `nopayn_transactions` table in your database to track payment transactions. This is created automatically on first use.

## 2. Configure the Plugin

#### Magento 2

1. Go to **Stores → Configuration → Sales → Payment Methods → Cost+ Payments**
2. Enter your API key (see step 3 below)
3. Enable the payment methods you have been approved for
4. Save changes

#### Magento 1.9

1. Go to **System → Configuration → Sales → Payment Methods**
2. Locate the **NoPayn Payment Gateway** section
3. Enter your API key (see step 3 below)
4. Enable individual payment methods (Credit / Debit Card, Apple Pay, Google Pay, Vipps MobilePay)
5. Optionally restrict by country
6. Save configuration

## 3. Enter API Credentials

Log into the [Merchant Portal](https://dashboard.costplus.io/) and navigate to **Websites**, then click on the website you want to connect. Click on **Integration** where you will find your API key.

![Cost+ Merchant Portal showing API key](/images/plugins/shared/merchant-portal-api-key.png)

Paste the API key into the configuration field and save.

> [!WARNING]
> Only activate the payment methods you have been approved for and received confirmation for.

## Supported Payment Methods

| Checkout Name | NoPayn Identifier |
|---|---|
| Credit / Debit Card | `credit-card` |
| Apple Pay | `apple-pay` |
| Google Pay | `google-pay` |
| Vipps MobilePay | `vipps-mobilepay` |

## Payment Flow

#### Magento 2

1. Customer selects a payment method at checkout and places the order
2. Customer is redirected to the NoPayn Hosted Payment Page
3. After payment, the customer returns and the order status is updated automatically
4. NoPayn sends a webhook for asynchronous status confirmation

#### Magento 1.9

1. Customer selects a payment method at checkout and places the order
2. Order is created with status **Pending Payment**
3. Customer is redirected to the NoPayn secure payment page
4. After payment:
   - **Success** — customer returns, status verified via API, order set to **Processing**
   - **Cancelled** — customer returns, order set to **Canceled**
   - **Expired** (5-minute timeout) — webhook fires, order set to **Canceled**
5. NoPayn sends a webhook for asynchronous status confirmation

### Order Status Mapping

| NoPayn Status | Magento Order State | Magento Order Status |
|---|---|---|
| `new` | pending_payment | Pending Payment |
| `processing` | pending_payment | Pending Payment |
| `completed` | processing | Processing |
| `cancelled` | canceled | Canceled |
| `expired` | canceled | Canceled |
| `error` | canceled | Canceled |

### Webhooks

The module registers a webhook endpoint at `/nopayn/payment/webhook`. This URL is automatically sent to NoPayn when creating orders. The webhook always verifies the order status via the NoPayn API before updating the Magento order.

> [!NOTE]
> No manual webhook configuration is required — the module handles registration automatically.

## 4. Test Your Setup

1. Place a few test transactions — both successful and failed
2. Verify all enabled payment methods appear at checkout
3. Configure capture behavior (auto-capture vs. manual) based on your fulfillment process

## Support

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


---

## modified eCommerce
<https://docs.costplus.io/docs/plugins/modified-ecommerce>

> Integrate Cost+ with your modified eCommerce shop using the MMLC module

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/modified-ecommerce.png" alt="modified eCommerce" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your modified eCommerce shop. The **NoPayn Payment Gateway** module is installed via the Modified Module Loader Client (MMLC) and supports Credit/Debit Cards, Apple Pay, Google Pay, and Vipps/MobilePay.

## Prerequisites

- Active Cost+ merchant account
- modified eCommerce shop with admin access
- [MMLC](https://module-loader.de) installed on your shop

## 1. Install MMLC (if not already installed)

Download MMLC from [module-loader.de](https://module-loader.de). Upload the `ModifiedModuleLoaderClient` folder to your shop root directory.

Once uploaded, access MMLC at:

```
https://your-shop.com/ModifiedModuleLoaderClient/
```

## 2. Add the NoPayn Module Repository

In MMLC, click the **Settings** icon (gear icon). Under **Module repositories**, add the following repository URL:

```
https://github.com/NoPayn/modified-nopayn.git
```

Click **Save** to confirm.

## 3. Install the Module

In MMLC, search for **"NoPayn"** or browse the **Payment** category. Click **Install** on **"NoPayn Payment Gateway"**, then click **Link** to deploy the module files into your shop.

> [!NOTE]
> The Link step copies the module files into the correct shop directories. Make sure your file permissions allow MMLC to write to the shop folder.

## 4. Activate Payment Methods

Navigate to **Admin → Modules → Payment** in your shop admin panel. You'll see the available Cost+ payment methods:

- **Credit / Debit Card**
- **Apple Pay**
- **Google Pay**
- **Vipps/MobilePay**

For each method you want to offer:

1. Click **Install**
2. Enter your **NoPayn API key** (from your [NoPayn dashboard](https://dashboard.costplus.io/) → Settings → API Key)
3. Set the **Completed Order Status** (e.g. "Processing" or a custom status)
4. Click **Save**

> [!TIP]
> You can find your API key in the [Merchant Portal](https://dashboard.costplus.io/) under **Websites** → your website → **Integration**.

## 5. Test and Launch

Place a test order in your storefront to verify the integration:

1. Add a product to the cart and proceed to checkout
2. Select one of the Cost+ payment methods
3. You'll be redirected to the NoPayn payment page
4. After payment, the order status should update automatically in your shop admin

> [!WARNING]
> Make sure to test with your **test API key** before going live. Switch to your production key only after verifying the full payment flow.

## Support

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


---

## nopCommerce
<https://docs.costplus.io/docs/plugins/nopcommerce>

> Integrate Cost+ with your nopCommerce store using the official payment plugin

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/nopcommerce.png" alt="nopCommerce" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your nopCommerce store. The official **NoPayn Payment Gateway** plugin supports multiple payment methods with direct redirect to the selected payment method — fully PCI DSS compliant.

## Prerequisites

- Active Cost+ merchant account
- nopCommerce 4.90 or later
- .NET 9
- Admin access to your nopCommerce admin panel
- Access to the nopCommerce source code (for plugin installation)

## Supported Payment Methods

| Checkout Display Name | NoPayn Identifier |
|---|---|
| Credit / Debit Card | `credit-card` |
| Apple Pay | `apple-pay` |
| Google Pay | `google-pay` |
| Vipps MobilePay | `vipps-mobilepay` |

## 1. Install the Plugin

Copy the plugin folder into your nopCommerce source directory:

```
src/Plugins/Nop.Plugin.Payments.NoPayn/
```

Add the project to your solution and build:

```bash
dotnet sln add src/Plugins/Nop.Plugin.Payments.NoPayn/Nop.Plugin.Payments.NoPayn.csproj
dotnet build
```

In the nopCommerce admin panel:

1. Go to **Configuration → Local plugins**
2. Find **NoPayn Payment Gateway** and click **Install**
3. Go to **Configuration → Payment methods**
4. Activate **NoPayn Payment Gateway**
5. Click **Configure** to open the settings

## 2. Configure the Plugin

Log into the [Merchant Portal](https://dashboard.costplus.io/) and navigate to **Websites**, then click on the website you want to connect. Click on **Integration** where you will find your API key.

![Cost+ Merchant Portal showing API key](/images/plugins/shared/merchant-portal-api-key.png)

In the plugin configuration, enter your **API Key** and toggle the payment methods you want to offer:

| Setting | Description |
|---|---|
| API Key | Your NoPayn API key |
| Enable Credit / Debit Card | Toggle credit/debit card payments |
| Enable Apple Pay | Toggle Apple Pay payments |
| Enable Google Pay | Toggle Google Pay payments |
| Enable Vipps MobilePay | Toggle Vipps MobilePay payments |

> [!WARNING]
> Only activate the payment methods you have been approved for and received confirmation for.

## 3. Payment Flow

1. Customer selects **NoPayn Payment Gateway** at checkout
2. A sub-method selection appears (Credit Card, Apple Pay, etc.)
3. Customer picks a method and confirms the order
4. Order is created with **Pending** payment status
5. Customer is redirected directly to the selected payment method on the NoPayn payment page
6. After payment:
   - **Success** — order marked as **Paid / Processing**
   - **Cancelled / Failed / Expired** — order set to **Cancelled**
7. NoPayn sends a webhook for asynchronous status confirmation

> [!TIP]
> Transactions expire after 5 minutes. If a customer does not complete payment within this window, the order is automatically cancelled via webhook.

## 4. Order Status Mapping

| NoPayn Status | nopCommerce Payment Status | nopCommerce Order Status |
|---|---|---|
| `new` | Pending | Pending |
| `processing` | Pending | Pending |
| `completed` | Paid | Processing |
| `cancelled` | Voided | Cancelled |
| `expired` | Voided | Cancelled |
| `error` | Voided | Cancelled |

## 5. Webhooks

The plugin registers a webhook endpoint at `/NoPayn/Webhook`. NoPayn sends POST data containing the order identifier on status changes. The plugin verifies the current status via API call before updating the order.

> [!NOTE]
> No manual webhook configuration is required — the plugin handles registration automatically.

## 6. Test and Launch

Place a few test transactions to ensure everything works smoothly. We recommend testing both successful and failed payments to confirm all scenarios are handled correctly.

## Support

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


---

## OpenCart
<https://docs.costplus.io/docs/plugins/opencart>

> Integrate Cost+ with your OpenCart store (4.x and 3.x / ocStore 3)

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/opencart.png" alt="OpenCart" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your OpenCart store. The official **NoPayn Payment Gateway** extension is available for **OpenCart 4** and **OpenCart 3 / ocStore 3**.

## Prerequisites

#### OpenCart 4

- Active Cost+ merchant account
- OpenCart 4.0.0.0 or later
- PHP 8.0 or later
- Admin access to your OpenCart dashboard

#### OpenCart 3

- Active Cost+ merchant account
- ocStore 3.0.x or OpenCart 3.0.3.x
- PHP 7.4 or later
- Admin access to your OpenCart dashboard

## Supported Payment Methods

#### OpenCart 4

- **Credit / Debit Card** — Visa, Mastercard, Amex, Maestro, V Pay, Bancontact, Diners, Discover
- **Apple Pay**
- **Google Pay**
- **Vipps / MobilePay**

#### OpenCart 3

- **Credit / Debit Card** — Visa, Mastercard, Amex, Maestro, V Pay, Bancontact, Diners, Discover
- **Apple Pay**
- **Google Pay**
- **Vipps / MobilePay**
- **Swish**

## 1. Install the Extension

#### OpenCart 4

### Method A: Upload via Admin Panel (Recommended)

1. Download the latest `.ocmod.zip` release from [GitHub](https://github.com/NoPayn/nopayn-opencart)
2. In your OpenCart admin, go to **Extensions → Installer**
3. Upload the `.ocmod.zip` file
4. Go to **Extensions → Extensions → Payment**
5. Find **NoPayn Payment Gateway** and click **Install**, then **Edit**

### Method B: Manual Upload

1. Download or clone the [repository](https://github.com/NoPayn/nopayn-opencart)
2. Copy the contents of the `upload/` folder into your OpenCart root directory
3. Go to **Extensions → Extensions → Payment**
4. Find **NoPayn Payment Gateway** and click **Install**, then **Edit**

#### OpenCart 3

### Architecture

OpenCart 3 uses a **multi-extension setup**:

- **NoPayn - Global Settings** — shared configuration (API key, order statuses, method availability, manual capture, debug logging)
- **NoPayn - Card Payments** — credit/debit card checkout
- **NoPayn - Apple Pay / Google Pay** — wallet checkout (if only one wallet is enabled, the label adjusts automatically)
- **NoPayn - Vipps MobilePay** — Vipps/MobilePay checkout
- **NoPayn - Swish** — Swish checkout

### Method A: Upload via Admin Panel (Recommended)

1. Download the `.ocmod.zip` asset from the latest [GitHub release](https://github.com/NoPayn/nopayn-opencart3)

> [!WARNING]
> Use the `.ocmod.zip` asset from the release — not GitHub's auto-generated "Source code" downloads, which contain the repository layout, not the installer layout.

2. In admin, go to **Extensions → Installer**
3. Upload the `.ocmod.zip` file
4. Go to **Extensions → Extensions → Payments**
5. Install **NoPayn - Global Settings** first
6. Open **NoPayn - Global Settings** and configure:
   - API key
   - Completed, pending, and cancelled order statuses
   - Available payment methods your merchant account is approved for
   - Optional: card manual capture
   - Optional: debug logging
7. Install the checkout modules you want to expose:
   - **NoPayn - Card Payments**
   - **NoPayn - Apple Pay / Google Pay**
   - **NoPayn - Vipps MobilePay**
   - **NoPayn - Swish**
8. For each checkout module, set: **Status**, **Geo Zone**, and **Sort Order**

### Method B: Manual Upload

1. Download or clone the [repository](https://github.com/NoPayn/nopayn-opencart3)
2. Copy the contents of the `upload/` folder into your store root
3. Go to **Extensions → Extensions → Payments**
4. Follow the same install order as Method A (Global Settings first, then checkout modules)

## 2. Enter API Credentials

Log into the [Merchant Portal](https://dashboard.costplus.io/) and navigate to **Websites**, then click on the website you want to connect. Click on **Integration** where you will find your API key.

![Cost+ Merchant Portal showing API key](/images/plugins/shared/merchant-portal-api-key.png)

#### OpenCart 4

In the NoPayn Payment Gateway settings, enter your **API Key** in the corresponding field.

#### OpenCart 3

In **NoPayn - Global Settings**, enter your **API Key** in the corresponding field. The API key is shared across all checkout modules — you only need to enter it once.

## 3. Configure Payment Settings

#### OpenCart 4

1. Enable the **payment methods** you have been approved for
2. Set your preferred **order statuses** for completed, pending, and cancelled payments
3. Optionally restrict payments by **Geo Zone**
4. Set **Status** to **Enabled**
5. Click **Save**

#### OpenCart 3

### Global Settings

**NoPayn - Global Settings** stores all shared configuration:

| Setting | Description |
|---|---|
| API Key | Your NoPayn API key |
| Order Statuses | Completed, pending, and cancelled status mapping |
| Method Availability | Which payment methods your merchant account is approved for |
| Manual Capture | Authorize credit card payments only — capture later |
| Debug Logging | Enable detailed logging for troubleshooting |

### Checkout Module Settings

Each checkout module has its own storefront settings:

| Setting | Description |
|---|---|
| Status | Enable or disable this payment method at checkout |
| Geo Zone | Restrict to specific geographic zones |
| Sort Order | Display order on the checkout page |

> [!WARNING]
> Only activate the payment methods you have been approved for and received confirmation for.

## Payment Flow

#### OpenCart 4

1. Customer selects a Cost+ payment method at checkout
2. Customer is redirected to the NoPayn Hosted Payment Page
3. After payment, the customer returns and the order status is updated automatically
4. NoPayn sends a webhook for asynchronous status confirmation

> [!TIP]
> The extension uses the Cost+ Hosted Payment Page, meaning customers are redirected to a secure payment form. No sensitive card data is handled by your server.

#### OpenCart 3

Each checkout module is a separate OpenCart payment extension, giving customers distinct radio options at checkout (e.g. "Card Payments", "Apple Pay & Google Pay").

1. Customer selects a payment method label at checkout (e.g. **Card Payments**)
2. The extension creates a NoPayn hosted payment order
3. Customer is redirected to the secure NoPayn payment page
4. After payment, the customer returns and the order status is updated automatically
5. NoPayn sends a webhook for asynchronous status confirmation

> [!TIP]
> The Apple Pay / Google Pay module sends both methods in one transaction so the NoPayn hosted page can offer both wallet options in a single flow.

#### OpenCart 4

#### OpenCart 3

### Upgrading from v1.0.0 to v2.0.0

> [!NOTE]
> Version 1.0.0 used a single checkout method called "NoPayn Checkout". Version 2.0.0 changes to separate checkout modules per payment method. When upgrading:
> 1. Upload the new package
> 2. Your existing "NoPayn Checkout" entry becomes **NoPayn - Global Settings**
> 3. Review and save the global settings
> 4. Install and enable the new checkout modules you want customers to see

## 4. Test and Launch

Place a few test transactions to ensure everything works smoothly. We recommend testing both successful and failed payments to confirm all scenarios are handled correctly.

## Support

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


---

## osCommerce
<https://docs.costplus.io/docs/plugins/oscommerce>

> Integrate Cost+ with your osCommerce 4 store using the official payment module

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/oscommerce.png" alt="osCommerce" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your osCommerce 4 store. The official **NoPayn Payments** module uses the Hosted Payment Page flow, so no card data touches your server — fully PCI DSS compliant.

## Prerequisites

- Active Cost+ merchant account
- osCommerce 4.x
- PHP 8.1 or later
- cURL extension enabled
- SSL certificate (HTTPS required)
- Admin access to your osCommerce admin panel

## Supported Payment Methods

| Checkout Label | NoPayn Identifier |
|---|---|
| Credit / Debit Card | `credit-card` |
| Apple Pay | `apple-pay` |
| Google Pay | `google-pay` |
| Vipps MobilePay | `vipps-mobilepay` |

Each method can be individually enabled or disabled from the admin panel.

## 1. Install the Module

Download or clone the module from [GitHub](https://github.com/NoPayn/nopayn-oscommerce4).

Copy the `lib/` directory into your osCommerce 4 root:

```bash
cp -r lib/ /path/to/oscommerce/
```

This places the module files at:

```
lib/common/modules/orderPayment/nopayn.php
lib/common/modules/orderPayment/nopayn/NoPaynApiClient.php
lib/common/modules/orderPayment/nopayn/NoPaynLogger.php
lib/common/modules/orderPayment/nopayn/NoPaynWebhookHandler.php
```

In your osCommerce admin panel:

1. Navigate to **Modules → Payment → Online**
2. Enable **"Show inactive"** and **"Show not installed"** filters if needed
3. Find **NoPayn Payments** and click **Install**

## 2. Configure the Module

Log into the [Merchant Portal](https://dashboard.costplus.io/) and navigate to **Websites**, then click on the website you want to connect. Click on **Integration** where you will find your API key.

![Cost+ Merchant Portal showing API key](/images/plugins/shared/merchant-portal-api-key.png)

Enter your **API Key** and configure the following settings:

| Setting | Description | Default |
|---|---|---|
| Enable NoPayn Payments | Master enable/disable switch | True |
| API Key | Your NoPayn API key | — |
| Enable Credit / Debit Card | Show credit/debit card at checkout | True |
| Enable Apple Pay | Show Apple Pay at checkout | True |
| Enable Google Pay | Show Google Pay at checkout | True |
| Enable Vipps MobilePay | Show Vipps MobilePay at checkout | True |
| Manual Capture (Credit Card) | Authorize only — capture when order is completed | False |
| Debug Logging | Write API requests/responses to log | False |
| Completed Order Status | Status set when payment succeeds | Processing |
| Pending Order Status | Status set while awaiting payment | Pending |
| Cancelled Order Status | Status set on cancellation/failure/expiry | Cancelled |
| Payment Zone | Restrict to a geographic zone (optional) | All zones |
| Sort Order | Display order on the checkout page | 0 |

> [!WARNING]
> Only activate the payment methods you have been approved for and received confirmation for.

## 3. Update Checkout Labels (Recommended)

The default osCommerce checkout button labels assume a single-step flow. For a better experience with the offsite payment redirect, update these translations in **Admin → Localisation → Languages → English → Define**:

| Key | Default | Recommended |
|---|---|---|
| `TEXT_PAY_WITH_CARD` | Pay with card | Go to payment |
| `TEXT_CONFIRM_AND_PAY` | Confirm and pay | Review order |
| `CONFIRM_ORDER` | Confirm order | Confirm and pay |

> [!TIP]
> This improves the customer experience by making the checkout flow clearer — customers understand they'll be redirected to complete payment on a secure page.

## 4. Payment Flow

Once configured, the payment flow works as follows:

1. Customer selects a Cost+ payment method at checkout
2. Customer confirms the order
3. Module creates a pending order and calls the Cost+ API
4. Customer is redirected to the Cost+ Hosted Payment Page (HPP)
5. Customer completes payment on the secure HPP
6. Customer is redirected back to the store
7. Module verifies payment status via the Cost+ API
8. Order status is updated accordingly

> [!NOTE]
> Payment links expire after 5 minutes. If the customer does not complete payment within this window, the order is automatically marked as cancelled.

## 5. Webhooks

The module registers a webhook URL with Cost+ for server-to-server status updates. When a payment status changes, Cost+ sends a notification and the module:

1. Receives the webhook POST
2. Verifies the payment status via API (never trusts the webhook payload)
3. Updates the order status in osCommerce

> [!NOTE]
> No manual webhook configuration is required — the module handles registration automatically.

## Manual Capture

When enabled for credit card payments:

- Payment is **authorized** but not captured at checkout
- Funds are captured when the order transitions to **completed** status
- If the order is cancelled, the authorization is automatically **voided**

> [!TIP]
> Use manual capture if you want to only charge customers when their order ships. This is useful for merchants with longer fulfillment times.

## Debug Logging

When **Debug Logging** is enabled, the module writes to `nopayn_debug.log` in the osCommerce logs directory. Log entries include:

- All API requests and responses
- Webhook events and processing results
- Capture and void operations
- Errors (always logged, regardless of debug toggle)

All entries are prefixed with `NoPayn_` for easy filtering.

## Uninstallation

1. Go to **Modules → Payment → Online** in the admin panel
2. Select **NoPayn Payments**
3. Click **Remove**

> [!CAUTION]
> Uninstalling the module removes the configuration and drops the `nopayn_transactions` and `nopayn_refunds` database tables. Make sure to export any data you need before uninstalling.

## Test and Launch

Place a few test transactions to ensure everything works smoothly. We recommend testing both successful and failed payments to confirm all scenarios are handled correctly.

## Support

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


---

## PrestaShop
<https://docs.costplus.io/docs/plugins/prestashop>

> Integrate Cost+ with your PrestaShop store (9.x, 8.x, and 1.7)

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/prestashop.png" alt="PrestaShop" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your PrestaShop store. The Cost+ module is available for **PrestaShop 9.x**, **PrestaShop 8.x**, and **PrestaShop 1.7**.

## Prerequisites

#### PrestaShop 9.x

- Active Cost+ merchant account
- PrestaShop 9.1 or later
- PHP 8.1 or later
- SSL/HTTPS enabled
- Admin access to your PrestaShop back office

#### PrestaShop 8.x

- Active Cost+ merchant account
- PrestaShop 8.x installation
- PHP 8.0 or later
- Admin access to your PrestaShop back office
- MySQL 5.4 or higher

#### PrestaShop 1.7

- Active Cost+ merchant account
- PrestaShop 1.7.x installation
- PHP 5.4 or later
- Admin access to your PrestaShop back office
- MySQL 5.4 or higher

## 1. Install the Module

#### PrestaShop 9.x

Download the module from [GitHub](https://github.com/NoPayn/nopayn-prestashop91).

### Method A: Upload Through Admin Panel (Recommended)

1. Go to **Modules → Module Manager** in your admin panel
2. Click **Upload a module**
3. Drag and drop the ZIP file or select it manually
4. Wait for the installation to finish
5. Search for **"NoPayn"** and click **Configure**

### Method B: Manual Upload

1. Copy the `nopaynpayment/` folder into your PrestaShop `modules/` directory
2. Go to **Modules → Module Manager**
3. Search for **"NoPayn"** and click **Install**
4. Click **Configure** to open the settings

#### PrestaShop 8.x

Download the module from [GitHub](https://github.com/NoPayn/nopayn-prestashop-8).

### Method A: Upload Through Admin Panel (Recommended)

1. Go to **Improve** > **Module Manager** in your admin panel
2. Click **Upload a module**
3. Drag and drop the ZIP file or select it manually
4. Wait for the installation to finish
5. Scroll down to the **"Other"** section and click **Configure** under Cost+

![PrestaShop 8 Module Manager](/images/plugins/prestashop-8/step1a.png)

![Upload a module dialog](/images/plugins/prestashop-8/step1b.png)

### Method B: Manual Upload via (S)FTP

1. Extract the downloaded ZIP file
2. Using an SFTP client (e.g. FileZilla, WinSCP), upload all folders to the `/modules/` directory in your PrestaShop installation
3. Go to **Improve** > **Modules** > **Module Catalog**
4. Search for **"Cost+"** -- check the **Uninstalled Modules** tab if needed
5. Click **Install**

#### PrestaShop 1.7

Download the module from [GitHub](https://github.com/NoPayn/nopayn-prestashop-1.7).

### Method A: Upload Through Admin Panel (Recommended)

1. Go to **Improve** > **Module Manager** in your admin panel
2. Click **Upload a module**
3. Drag and drop the ZIP file or select it manually
4. Wait for the installation to finish
5. Scroll down to the **"Other"** section and click **Configure** under Cost+

![PrestaShop 1.7 Module Manager](/images/plugins/prestashop-17/step1a.png)

![Upload a module dialog](/images/plugins/prestashop-17/step1b.png)

### Method B: Manual Upload via (S)FTP

1. Extract the downloaded ZIP file
2. Using an SFTP client (e.g. FileZilla, WinSCP), upload all folders to the `/modules/` directory in your PrestaShop installation
3. Go to **Modules** > **Module Manager**
4. Search for **"Cost+"** -- check the **Uninstalled Modules** tab if needed
5. Click **Install**

## 2. Enter API Credentials

Log into the [Merchant Portal](https://dashboard.costplus.io/) and navigate to **Websites**, then click on the website you want to connect. Click on **Integration** where you will find your API key.

![Cost+ Merchant Portal showing API key](/images/plugins/shared/merchant-portal-api-key.png)

#### PrestaShop 9.x

In the NoPayn module configuration, enter your **API Key** and configure the following settings:

| Setting | Description |
|---|---|
| API Key | Your NoPayn API key |
| Credit / Debit Card | Enable/disable credit card payments |
| Apple Pay | Enable/disable Apple Pay |
| Google Pay | Enable/disable Google Pay |
| Vipps MobilePay | Enable/disable Vipps MobilePay |
| Manual Capture | Authorize credit card payments only — capture when you ship |
| Debug Logging | Enable detailed logging to `var/logs/nopayn_debug.log` |

#### PrestaShop 8.x

Paste the key into the **API Key** field in the Cost+ Library configuration page and click **Save**.

![Cost+ configuration page in PrestaShop 8](/images/plugins/prestashop-8/step2.png)

#### PrestaShop 1.7

Paste the key into the **API Key** field in the Cost+ Library configuration page and click **Save**.

![Cost+ configuration page in PrestaShop 1.7](/images/plugins/prestashop-17/step2.png)

> [!TIP]
> For PrestaShop 8.x and 1.7 hosting environments, enable the **cURL CA bundle** option during configuration to avoid SSL certificate issues.

## 3. Enable Payment Methods

#### PrestaShop 9.x

Payment methods are configured directly in the module settings (see step 2 above). Toggle each method on or off as needed.

> [!WARNING]
> Only activate the payment methods you have been approved for and received confirmation for.

#### PrestaShop 8.x

After saving your API key, upload each payment method module separately via **Upload a module**.

Return to **Improve** > **Module Manager**, click **Upload a module** again, and upload each ZIP file for the payment methods you want to enable (e.g. Credit Card, MobilePay, Swish).

> [!WARNING]
> Only activate the payment methods you have been approved for and received confirmation for.

Payment modules will either have a **Configure** option (if additional setup is needed) or just an Enable/Disable toggle.

#### PrestaShop 1.7

After saving your API key, upload each payment method module separately via **Upload a module**.

Return to **Improve** > **Module Manager**, click **Upload a module** again, and upload each ZIP file for the payment methods you want to enable (e.g. Credit Card, MobilePay, Swish).

> [!WARNING]
> Only activate the payment methods you have been approved for and received confirmation for.

Payment modules will either have a **Configure** option (if additional setup is needed) or just an Enable/Disable toggle.

## Payment Flow

1. Customer selects a Cost+ payment method at checkout
2. Customer is redirected to the Cost+ Hosted Payment Page (HPP)
3. After completing payment, customer is redirected back to the store
4. A webhook confirms the payment status server-to-server

#### PrestaShop 9.x

### Webhook

The webhook URL is automatically configured per order:

```
https://your-shop.com/module/nopaynpayment/webhook
```

> [!NOTE]
> No manual webhook configuration is required — the module handles registration automatically.

### Manual Capture

When enabled for credit card payments:

- Payment is **authorized** but not captured at checkout
- Capture occurs when the order is shipped / marked as completed
- If the order is cancelled, the authorization is automatically **voided**

### Auto-Refund

If a captured payment order is cancelled, the module automatically issues a full refund. Partial refunds can be processed via PrestaShop credit slips.

### Checkout Button Labels

For the best user experience with offsite redirect flows, consider updating the checkout button label to **"Review order"** instead of "Confirm and pay", since the actual payment happens on the Cost+ hosted page. This is a global PrestaShop setting, not module-specific.

#### PrestaShop 8.x

### Webhook

The webhook is automatically configured by the module. No manual setup is required.

#### PrestaShop 1.7

### Webhook

The webhook is automatically configured by the module. No manual setup is required.

## 4. Final Steps and Testing

1. Double-check all enabled payment methods
2. Place a few test transactions -- both successful and failed -- to ensure everything flows smoothly
3. Configure advanced behavior such as manual vs. auto-capture depending on your fulfillment process

## Support

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


---

## Shopify
<https://docs.costplus.io/docs/plugins/shopify>

> Integrate Cost+ with your Shopify store using the official Shopify App

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/shopify.png" alt="Shopify" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your Shopify store. The official **Cost+ Payments** app is available for quick installation directly from the Shopify App Store.

## Prerequisites

- Active Cost+ merchant account
- Shopify store on any paid plan
- Admin access to your Shopify store

## 1. Install the Cost+ Payments App

Go to the [Cost+ Payments App Page](https://apps.shopify.com/nopayn-payments) on the Shopify App Store and click **Install**.

> [!NOTE]
> Shopify will guide you through the necessary steps to add the app to your store securely.

## 2. Enter Your Merchant Credentials

Log into the [Merchant Portal](https://dashboard.costplus.io/) and navigate to **Websites**, then click on the website you want to connect. Click on **Integration** where you will find your **Merchant ID** and **API key**.

![Merchant Portal showing Merchant ID and API key](/images/plugins/shopify/merchant-portal-ids.png)

Enter your **Merchant ID** and **API Key** when prompted inside the Cost+ app settings.

![Cost+ app settings with Merchant ID and API Key fields](/images/plugins/shopify/app-credentials.png)

> [!WARNING]
> Unlike other plugins, Shopify requires **both** a Merchant ID and an API Key. Make sure to enter both values.

## 3. Customize Payment Display

Select which payment icons (e.g. Visa, Mastercard, Apple Pay, Google Pay) you'd like to show on your checkout. This can help increase customer trust and improve conversions.

![Payment icon selection for checkout display](/images/plugins/shopify/payment-logos.png)

## 4. Activate Cost+ Payments

Click the **Activate** button to enable Cost+ Payments for your store.

![Activate button to enable Cost+ Payments](/images/plugins/shopify/activate.png)

## 5. Test and Launch

Make a few test purchases to ensure everything works smoothly. We suggest testing both successful and failed payments to confirm all scenarios are handled.

## Optimize Your Checkout (Optional)

We recommend installing the [Checkout Rules App](https://apps.shopify.com/nopayn-checkout-rules) to reorder and rename your payment methods for a more intuitive shopper experience.

**Suggested settings:**
- Rename Cost+ to: **"Visa, MasterCard, Apple Pay & Google Pay"**
- Sort it to **Position 1** to maximize visibility

A cleaner, more relevant checkout experience can help reduce cart abandonment and increase trust.

## Support

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


---

## Shopware
<https://docs.costplus.io/docs/plugins/shopware>

> Integrate Cost+ with your Shopware 6 store using the official payment plugin

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/shopware.png" alt="Shopware" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your Shopware 6 store. The official **NoPayn Payment** plugin supports multiple payment methods per sales channel and uses the Hosted Payment Page flow — fully PCI DSS compliant.

## Prerequisites

- Active Cost+ merchant account
- Shopware 6.7 or later
- PHP 8.2 or later
- SSH or terminal access to your Shopware server

## Supported Payment Methods

| Checkout Name | Technical Name | NoPayn Identifier |
|---|---|---|
| Credit / Debit Card | `nopayn_credit_card` | `credit-card` |
| Apple Pay | `nopayn_apple_pay` | `apple-pay` |
| Google Pay | `nopayn_google_pay` | `google-pay` |
| Vipps MobilePay | `nopayn_vipps_mobilepay` | `vipps-mobilepay` |

Each method can be enabled or disabled per sales channel from the plugin configuration.

## 1. Install the Plugin

Clone or copy the plugin into your Shopware `custom/plugins/` directory:

```bash
cd /path/to/shopware/custom/plugins
git clone git@github.com:NoPayn/shopware.git NoPaynPayment
```

Then install and activate via the Shopware CLI:

```bash
bin/console plugin:refresh
bin/console plugin:install NoPaynPayment --activate
bin/console cache:clear
```

## 2. Configure the Plugin

1. In your Shopware admin, go to **Settings → Extensions → NoPayn Payment**
2. Enter your **API Key** from the [Merchant Portal](https://dashboard.costplus.io/) — navigate to **Websites**, click on the website you want to connect, then click **Integration** to find your API key
3. Toggle individual **payment methods** on or off
4. Save

![Cost+ Merchant Portal showing API key](/images/plugins/shared/merchant-portal-api-key.png)

> [!TIP]
> You can configure a different API key per sales channel if you operate multiple storefronts.

> [!WARNING]
> Only activate the payment methods you have been approved for and received confirmation for.

## 3. Payment Flow

1. Customer selects a payment method at checkout and places the order
2. The order is created with transaction status **in_progress**
3. Customer is redirected to the NoPayn Hosted Payment Page
4. After payment:
   - **Success** — customer returns, status is verified via API, transaction set to **paid**, order set to **processing**
   - **Cancelled** — customer returns, transaction and order set to **cancelled**
   - **Expired** (5-minute timeout) — webhook fires, transaction and order set to **cancelled**

## 4. Order Status Mapping

| NoPayn Status | Transaction State | Order State |
|---|---|---|
| `new` | in_progress | open |
| `processing` | in_progress | open |
| `completed` | paid | in_progress |
| `cancelled` | cancelled | cancelled |
| `expired` | cancelled | cancelled |
| `error` | cancelled | cancelled |

## 5. Webhooks

The plugin automatically registers a webhook endpoint at `/api/nopayn/webhook`. This URL is sent to NoPayn when creating orders, providing asynchronous status confirmation for all transactions.

> [!NOTE]
> No manual webhook configuration is required — the plugin handles registration automatically.

## 6. Test and Launch

Place a few test transactions to ensure everything works smoothly. We recommend testing both successful and failed payments to confirm all scenarios are handled correctly.

## Support

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


---

## Wix
<https://docs.costplus.io/docs/plugins/wix>

> Integrate Cost+ with your Wix e-commerce site

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/wix.png" alt="Wix" style={{height: '48px', width: 'auto'}} />
</div>

## Coming Soon

The Cost+ plugin for Wix is currently in development. Full setup documentation will be available here once the integration is released.

In the meantime, you can integrate Cost+ with your Wix store using our [API Reference](/api-reference) and [Hosted Payment Page](/docs/guides/hosted-payment-page) guide.

For questions or early access, contact our support team at **support@costplus.io**.


---

## WooCommerce
<https://docs.costplus.io/docs/plugins/woocommerce>

> Integrate Cost+ with your WooCommerce store using the official WordPress plugin

<div style={{background: 'white', borderRadius: '8px', padding: '1rem', display: 'inline-block', marginBottom: '1rem'}}>
  <img src="/images/logos/woocommerce.png" alt="WooCommerce" style={{height: '48px', width: 'auto'}} />
</div>

Integrate Cost+ as a payment method in your WooCommerce store. The official **Cost+ Payments** WordPress plugin provides seamless integration with your existing WooCommerce checkout.

## Prerequisites

- Active Cost+ merchant account
- WordPress site with WooCommerce installed
- Admin access to your WordPress dashboard

## 1. Install the Cost+ Plugin

Log in to your WordPress admin panel, then navigate to **Plugins** > **Add New**.

In the search bar, type **"Cost+ Payments"** (or "NoPayn Payments"). Find the plugin in the search results, click **Install Now**, then click **Activate**.

![Search for Cost+ Payments in WordPress plugin directory](/images/plugins/woocommerce/step1.png)

> [!NOTE]
> Alternatively, you can download the plugin directly from the [WordPress Plugin Repository](https://wordpress.org/plugins/nopayn/) and upload it manually.

## 2. Configure Cost+ Settings

In your WordPress dashboard, go to **WooCommerce** > **Settings**.

![Navigate to WooCommerce Settings](/images/plugins/woocommerce/step2a.png)

Click on the **Payments** tab to view available payment methods.

![WooCommerce Payments tab](/images/plugins/woocommerce/step2b.png)

Locate **Cost+ - Settings** in the list, toggle the switch to enable it, and click the **Manage/Finish setup** button.

![Enable Cost+ Payments and click Manage](/images/plugins/woocommerce/step2c.png)

## 3. Enter API Credentials

Log into the [Merchant Portal](https://dashboard.costplus.io/) and navigate to **Websites**, then click on the website you want to connect. Click on **Integration** where you will find your API key.

![Cost+ Merchant Portal showing API key](/images/plugins/shared/merchant-portal-api-key.png)

In the Cost+ plugin settings, enter the API key in the **API key** field.

![Cost+ plugin settings with API key field](/images/plugins/woocommerce/step3.png)

## 4. Customize Payment Settings

By default, **all payment methods operate in Autocapture mode**, meaning payments are captured immediately after authorization.

For **Cost+ Credit/Debit Cards**, an additional setting -- **Capture on Complete** -- is available directly in its settings panel. Checking this box will delay capture until an order is marked as complete in WooCommerce.

![Capture on Complete checkbox in Credit/Debit Cards settings](/images/plugins/woocommerce/step4.png)

> [!TIP]
> This flexible option allows you to align payment flows with your fulfillment process. Use "Capture on Complete" if you want to only charge customers when their order ships.

## 5. Save Changes and Test

After configuring your preferences, click **Save changes**.

![Save changes button](/images/plugins/woocommerce/step5.png)

Run a few test payments to ensure everything works smoothly. We recommend testing both successful and failed transactions to confirm behavior across scenarios.

## Support

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


---

## SDKs & Libraries
<https://docs.costplus.io/docs/sdks>

> Official Cost+ SDKs for integrating payments into your application

Official SDKs that simplify integrating Cost+ payments into your application. Each SDK handles order creation, HMAC signing, webhook verification, and more.

## Available SDKs

| Language | Package | Install |
|---|---|---|
| [Node.js / TypeScript](/docs/sdks/node) | `nopayn-node-sdk` | `npm install nopayn-node-sdk` |
| [Python](/docs/sdks/python) | `nopayn-sdk` | `pip install nopayn-sdk` |
| [PHP](/docs/sdks/php) | `nopayn/sdk` | `composer require nopayn/sdk` |
| [Java / Kotlin](/docs/sdks/java-kotlin) | `io.nopayn:nopayn-sdk` | Gradle / Maven |
| [C# / .NET](/docs/sdks/dotnet) | `NoPayn` | `dotnet add package NoPayn` |
| [Ruby](/docs/sdks/ruby) | `nopayn` | `gem install nopayn` |

## What the SDKs Handle

- **Order creation** — create payment orders via the Cost+ API
- **Payment URL generation** — get HPP or direct payment URLs
- **HMAC-SHA256 signing** — automatic signature generation and constant-time verification
- **Webhook verification** — parse webhook payloads and verify order status via API
- **Refunds** — issue full or partial refunds
- **Full type support** — TypeScript types, Python type hints, C# records, Kotlin data classes

## Custom Integration

If an SDK isn't available for your language, you can integrate directly using the [API Reference](/api-reference). The API uses HTTP Basic authentication and standard REST conventions.


---

## C# / .NET
<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**.


---

## Java / Kotlin
<https://docs.costplus.io/docs/sdks/java-kotlin>

> Official Kotlin/Java SDK for the Cost+ payment gateway

Official Kotlin SDK for the Cost+ payment gateway, fully interoperable with Java. Simplifies the HPP (Hosted Payment Page) redirect flow, HMAC payload signing, and webhook verification.

## Features

- **Kotlin-first, Java-friendly** — data classes, coroutines, null safety; fully usable from Java
- **kotlinx.serialization** — no Gson/Jackson dependency; automatic snake_case/camelCase mapping
- HMAC-SHA256 signature generation and constant-time verification
- Webhook parsing + API-based order verification
- Suspend functions for non-blocking IO via Kotlin coroutines
- Injectable `HttpClient` for easy testing with mock transports
- Targets **Java 17+**

## Requirements

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

## Installation

### Gradle (Kotlin DSL)

```kotlin
dependencies {
    implementation("io.nopayn:nopayn-sdk:1.0.0")
}
```

### Gradle (Groovy)

```groovy
dependencies {
    implementation 'io.nopayn:nopayn-sdk:1.0.0'
}
```

### Maven

```xml
<dependency>
    <groupId>io.nopayn</groupId>
    <artifactId>nopayn-sdk</artifactId>
    <version>1.0.0</version>
</dependency>
```

## Quick Start (Kotlin)

### 1. Initialise the Client

```kotlin
import io.nopayn.*
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val nopayn = NoPaynClient(
        NoPaynConfig(
            apiKey = "your-api-key",
            merchantId = "your-project",
        )
    )
}
```

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

```kotlin
val result = nopayn.generatePaymentUrl(
    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",
    )
)

println(result.orderUrl)    // HPP URL
println(result.paymentUrl)  // Direct payment method URL
println(result.signature)   // HMAC-SHA256 signature
```

### 3. Handle the Webhook

```kotlin
// In your HTTP handler (Ktor, Spring, etc.)
val rawBody: String = request.body()
val verified = nopayn.verifyWebhook(rawBody)

println(verified.order.status)  // "completed", "cancelled", etc.
println(verified.isFinal)       // true when the order won't change

if (verified.order.status == "completed") {
    // Fulfil the order
}
```

## Quick Start (Java)

```java
import io.nopayn.*;
import kotlinx.coroutines.BuildersKt;
import kotlinx.coroutines.Dispatchers;

public class Example {
    public static void main(String[] args) throws Exception {
        NoPaynClient client = new NoPaynClient(
            new NoPaynConfig("your-api-key", "your-project", "https://api.nopayn.co.uk")
        );

        Order order = BuildersKt.runBlocking(
            Dispatchers.getIO(),
            (scope, continuation) -> client.createOrder(
                new CreateOrderParams(
                    1295, "EUR", "ORDER-001", "Premium Widget",
                    "https://shop.example.com/success",
                    "https://shop.example.com/failure",
                    "https://shop.example.com/webhook",
                    "en-GB", null, "PT30M"
                ),
                continuation
            )
        );

        System.out.println("Order ID: " + order.getId());
        System.out.println("Order URL: " + order.getOrderUrl());

        // Signature utilities work synchronously
        String sig = client.generateSignature(1295, "EUR", order.getId());
        boolean valid = client.verifySignature(1295, "EUR", order.getId(), sig);
    }
}
```

## API Reference

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

| Parameter | Type | Required | Default |
|---|---|---|---|
| `config.apiKey` | `String` | Yes | — |
| `config.merchantId` | `String` | Yes | — |
| `config.baseUrl` | `String` | No | `https://api.nopayn.co.uk` |
| `httpClient` | `java.net.http.HttpClient` | No | Default client |

### `client.createOrder(params): Order` (suspend)

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` | `List<String>?` | No | Filter HPP methods |
| `expirationPeriod` | `String?` | No | ISO 8601 duration (`PT30M`) |

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

### `client.getOrder(orderId): Order` (suspend)

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

### `client.createRefund(orderId, amount, description?): Refund` (suspend)

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

### `client.generatePaymentUrl(params): PaymentUrlResult` (suspend)

Convenience method that creates an order and returns:

```kotlin
PaymentUrlResult(
    orderId: String,     // NoPayn order UUID
    orderUrl: String,    // HPP URL
    paymentUrl: String?, // Direct payment URL (first transaction)
    signature: String,   // HMAC-SHA256 of amount:currency:orderId
    order: Order,        // Full order object
)
```

### `client.generateSignature(amount, currency, orderId): String`

Generates an HMAC-SHA256 hex signature. The canonical message is `$amount:$currency:$orderId`, signed with the API key.

### `client.verifySignature(amount, currency, orderId, signature): Boolean`

Constant-time verification of an HMAC-SHA256 signature.

### `client.verifyWebhook(rawBody): VerifiedWebhook` (suspend)

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

### Standalone HMAC Utilities

```kotlin
import io.nopayn.NoPaynSignature

val sig = NoPaynSignature.generate("your-api-key", 1295, "EUR", "order-uuid")
val ok  = NoPaynSignature.verify("your-api-key", 1295, "EUR", "order-uuid", sig)
```

From Java:

```java
String sig = NoPaynSignature.generate("your-api-key", 1295, "EUR", "order-uuid");
boolean ok = NoPaynSignature.verify("your-api-key", 1295, "EUR", "order-uuid", sig);
```

## Error Handling

```kotlin
import io.nopayn.*

try {
    nopayn.createOrder(CreateOrderParams(amount = 100, currency = "EUR"))
} catch (e: ApiException) {
    println(e.statusCode)  // 401, 400, etc.
    println(e.errorBody)   // Raw API error response
} catch (e: NoPaynException) {
    println(e.message)     // Network or parsing error
}
```

## 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 `verifyWebhook()` 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 `getOrder()` 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 demo app (Ktor) is included in the [GitHub repository](https://github.com/NoPayn/Java-Kotlin) for testing the full payment flow.

## Support

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


---

## Node.js / TypeScript
<https://docs.costplus.io/docs/sdks/node>

> Official Node.js SDK for the Cost+ payment gateway

Official Node.js/TypeScript 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 Node.js `crypto` and `fetch`
- Full TypeScript types with declaration maps
- HMAC-SHA256 signature generation and constant-time verification
- Automatic snake_case/camelCase mapping between the API and SDK
- Webhook parsing + API-based order verification
- Tested across Node.js 18, 20, and 22

## Requirements

- Node.js 18 or later (uses built-in `fetch`)
- A Cost+ merchant account — [dashboard.costplus.io](https://dashboard.costplus.io/)

## Installation

```bash
npm install nopayn-node-sdk
```

## Quick Start

### 1. Initialise the Client

```typescript

const nopayn = new NoPaynClient({
  apiKey: 'your-api-key',      // From the NoPayn merchant portal
  merchantId: 'your-project',  // Your project/merchant ID
});
```

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

```typescript
const result = await nopayn.generatePaymentUrl({
  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

```typescript
app.post('/webhook', async (req, res) => {
  const verified = await nopayn.verifyWebhook(JSON.stringify(req.body));

  console.log(verified.order.status); // 'completed', 'cancelled', etc.
  console.log(verified.isFinal);      // true when the order won't change

  if (verified.order.status === 'completed') {
    // Fulfil the order
  }

  res.sendStatus(200);
});
```

## API Reference

### `new NoPaynClient(config)`

| Parameter | Type | Required | Default |
|---|---|---|---|
| `apiKey` | `string` | Yes | — |
| `merchantId` | `string` | Yes | — |
| `baseUrl` | `string` | No | `https://api.nopayn.co.uk` |

### `client.createOrder(params)`

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

| Parameter | Type | Required | Description |
|---|---|---|---|
| `amount` | `number` | 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` | `PaymentMethod[]` | No | Filter HPP methods |
| `expirationPeriod` | `string` | No | ISO 8601 duration (`PT30M`) |

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

### `client.getOrder(orderId)`

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

### `client.createRefund(orderId, amount, description?)`

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

### `client.generatePaymentUrl(params)`

Convenience method that creates an order and returns:

```typescript
{
  orderId: string;     // NoPayn order UUID
  orderUrl: string;    // HPP URL
  paymentUrl?: string; // Direct payment URL (first transaction)
  signature: string;   // HMAC-SHA256 of amount:currency:orderId
  order: Order;        // Full order object
}
```

### `client.generateSignature(amount, currency, orderId)`

Generates an HMAC-SHA256 hex signature. The canonical message is `${amount}:${currency}:${orderId}`, signed with the API key.

### `client.verifySignature(amount, currency, orderId, signature)`

Constant-time verification of an HMAC-SHA256 signature. Returns `true` if valid.

### `client.verifyWebhook(rawBody)`

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

```typescript
{
  orderId: string;
  order: Order;     // Verified via API
  isFinal: boolean; // true for completed/cancelled/expired/error
}
```

### `client.parseWebhookBody(rawBody)`

Parses and validates a webhook body without calling the API.

### Standalone HMAC Utilities

```typescript

const sig = generateSignature('your-api-key', 1295, 'EUR', 'order-uuid');
const ok  = verifySignature('your-api-key', 1295, 'EUR', 'order-uuid', sig);
```

## Error Handling

```typescript

try {
  await nopayn.createOrder({ amount: 100, currency: 'EUR' });
} catch (err) {
  if (err instanceof NoPaynApiError) {
    console.error(err.statusCode); // 401, 400, etc.
    console.error(err.errorBody);  // Raw API error response
  } else if (err instanceof NoPaynError) {
    console.error(err.message);    // Network or parsing error
  }
}
```

## 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 `verifyWebhook()` 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 `getOrder()` 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 demo app is included in the [GitHub repository](https://github.com/NoPayn/node-sdk) for testing the full payment flow:

```bash
cd demo

cat > .env << EOF
NOPAYN_API_KEY=your-api-key
NOPAYN_MERCHANT_ID=your-merchant-id
PUBLIC_URL=http://localhost:3000
EOF

docker compose up --build
```

Open `http://localhost:3000` to see the demo checkout page.

## Support

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


---

## PHP
<https://docs.costplus.io/docs/sdks/php>

> Official PHP SDK for the Cost+ payment gateway

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

## Features

- **Zero external dependencies** — uses only built-in PHP extensions (`curl`, `json`, `hash`)
- PHP 8.1+ with readonly properties, named arguments, and strict types
- HMAC-SHA256 signature generation and constant-time verification via `hash_equals`
- Automatic snake_case/camelCase mapping between the API and the SDK
- Webhook parsing + API-based order verification
- Tested across PHP 8.1, 8.2, and 8.3

## Requirements

- PHP 8.1 or later
- Extensions: `curl`, `json` (both included in standard PHP)
- A Cost+ merchant account — [dashboard.costplus.io](https://dashboard.costplus.io/)

## Installation

```bash
composer require nopayn/sdk
```

## Quick Start

### 1. Initialise the Client

```php
use NoPayn\NoPaynClient;

$nopayn = new NoPaynClient([
    'apiKey'     => 'your-api-key',
    'merchantId' => 'your-project',
]);
```

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

```php
$result = $nopayn->generatePaymentUrl([
    '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
header('Location: ' . ($result['paymentUrl'] ?? $result['orderUrl']));
```

### 3. Handle the Webhook

```php
$rawBody  = file_get_contents('php://input');
$verified = $nopayn->verifyWebhook($rawBody);

echo $verified['order']['status']; // 'completed', 'cancelled', etc.
echo $verified['isFinal'];        // true when the order won't change

if ($verified['order']['status'] === 'completed') {
    // Fulfil the order
}

http_response_code(200);
```

## API Reference

### `new NoPaynClient(array $config)`

| Parameter | Type | Required | Default |
|---|---|---|---|
| `apiKey` | `string` | Yes | — |
| `merchantId` | `string` | Yes | — |
| `baseUrl` | `string` | No | `https://api.nopayn.co.uk` |

### `$client->createOrder(array $params): array`

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` | `string[]` | No | Filter HPP methods |
| `expirationPeriod` | `string` | No | ISO 8601 duration (`PT30M`) |

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

### `$client->getOrder(string $orderId): array`

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

### `$client->createRefund(string $orderId, int $amount, ?string $description = null): array`

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

### `$client->generatePaymentUrl(array $params): array`

Convenience method that creates an order and returns:

```php
[
    'orderId'    => string,   // NoPayn order UUID
    'orderUrl'   => string,   // HPP URL
    'paymentUrl' => ?string,  // Direct payment URL (first transaction)
    'signature'  => string,   // HMAC-SHA256 of amount:currency:orderId
    'order'      => array,    // Full order object
]
```

### `$client->generateSignature(int $amount, string $currency, string $orderId): string`

Generates an HMAC-SHA256 hex signature.

### `$client->verifySignature(int $amount, string $currency, string $orderId, string $signature): bool`

Constant-time verification of an HMAC-SHA256 signature.

### `$client->verifyWebhook(string $rawBody): array`

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

### Standalone HMAC Utilities

```php
use NoPayn\Signature;

$sig = Signature::generate('your-api-key', 1295, 'EUR', 'order-uuid');
$ok  = Signature::verify('your-api-key', 1295, 'EUR', 'order-uuid', $sig);
```

## Error Handling

```php
use NoPayn\Exceptions\ApiException;
use NoPayn\Exceptions\NoPaynException;
use NoPayn\Exceptions\WebhookException;

try {
    $nopayn->createOrder(['amount' => 100, 'currency' => 'EUR']);
} catch (ApiException $e) {
    echo $e->getStatusCode(); // 401, 400, etc.
    print_r($e->getErrorBody()); // Raw API error response
} catch (NoPaynException $e) {
    echo $e->getMessage(); // Network or parsing error
}
```

## 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 `verifyWebhook()` 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 `getOrder()` 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 demo app is included in the [GitHub repository](https://github.com/NoPayn/php-sdk) for testing the full payment flow.

## Support

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


---

## Python
<https://docs.costplus.io/docs/sdks/python>

> Official Python SDK for the Cost+ payment gateway

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

## Features

- **Minimal dependencies** — only `requests`
- Full type hints and frozen dataclass return types
- HMAC-SHA256 signature generation and constant-time verification
- Webhook parsing + API-based order verification
- Tested across Python 3.10, 3.11, and 3.12

## Requirements

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

## Installation

```bash
pip install nopayn-sdk
```

## Quick Start

### 1. Initialise the Client

```python
from nopayn import NoPaynClient

client = NoPaynClient(
    api_key="your-api-key",
    merchant_id="your-project",
)
```

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

```python
result = client.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 (Flask Example)

```python
@app.route("/webhook", methods=["POST"])
def webhook():
    verified = client.verify_webhook(request.get_data(as_text=True))

    print(verified.order.status)  # 'completed', 'cancelled', etc.
    print(verified.is_final)      # True when the order won't change

    if verified.order.status == "completed":
        # Fulfil the order
        pass

    return "", 200
```

## API Reference

### `NoPaynClient(api_key, merchant_id, base_url?)`

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

### `client.create_order(**kwargs) -> Order`

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

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

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

### `client.get_order(order_id) -> Order`

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

### `client.create_refund(order_id, amount, description?) -> Refund`

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

### `client.generate_payment_url(**kwargs) -> PaymentUrlResult`

Convenience method that creates an order and returns:

```python
PaymentUrlResult(
    order_id="...",       # NoPayn order UUID
    order_url="...",      # HPP URL
    payment_url="...",    # Direct payment URL (first transaction)
    signature="...",      # HMAC-SHA256 of amount:currency:order_id
    order=Order(...),     # Full order object
)
```

### `client.generate_signature(amount, currency, order_id) -> str`

Generates an HMAC-SHA256 hex signature.

### `client.verify_signature(amount, currency, order_id, signature) -> bool`

Constant-time verification of an HMAC-SHA256 signature.

### `client.verify_webhook(raw_body) -> VerifiedWebhook`

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

### Standalone HMAC Utilities

```python
from nopayn import generate_signature, verify_signature

sig = generate_signature("your-api-key", 1295, "EUR", "order-uuid")
ok = verify_signature("your-api-key", 1295, "EUR", "order-uuid", sig)
```

## Data Types

All API responses are returned as frozen dataclasses:

| Class | Fields |
|---|---|
| `Order` | `id`, `amount`, `currency`, `status`, `created`, `modified`, `transactions`, `description`, `merchant_order_id`, `return_url`, `failure_url`, `order_url`, `completed` |
| `Transaction` | `id`, `amount`, `currency`, `status`, `created`, `modified`, `payment_method`, `payment_url`, `expiration_period` |
| `Refund` | `id`, `amount`, `status` |
| `PaymentUrlResult` | `order_id`, `order_url`, `payment_url`, `signature`, `order` |
| `VerifiedWebhook` | `order_id`, `order`, `is_final` |

## Error Handling

```python
from nopayn import ApiError, NoPaynError, WebhookError

try:
    client.create_order(amount=100, currency="EUR")
except ApiError as exc:
    print(exc.status_code)  # 401, 400, etc.
    print(exc.error_body)   # Raw API error response
except NoPaynError as exc:
    print(exc)              # Network or parsing error
```

## 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 Flask-based demo app is included in the [GitHub repository](https://github.com/NoPayn/python-sdk) for testing the full payment flow.

## Support

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


---

## Ruby
<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**.


---

## API Reference
<https://docs.costplus.io/api-reference>

> Complete API reference for the Cost+ Payment API

The Cost+ Payment API lets you create and manage orders, process payments, handle refunds, and manage payment links.

## Base URL

| | URL |
|---|---|
| **API** | `https://api.costplus.online/v1` |

> [!NOTE]
> There is no separate sandbox URL. Sandbox vs production mode is determined by the API key — use a key from a sandbox website in the [Merchant Portal](https://dashboard.costplus.io/) for testing, or a production website for live transactions.

## Authentication

All API requests use **HTTP Basic Auth**. Use your API key as the username with an empty password:

```bash
curl -u {api_key}: https://api.costplus.online/v1/orders/
```

See the [Authentication guide](/docs/getting-started/authentication) for detailed setup instructions.

## API Sections

### Orders API
Create, retrieve, update, and cancel payment orders. Includes refund, capture, and void operations.

### Payment Links API
Create and manage reusable payment links that support multiple payment attempts.

### Search API
Search across your orders using flexible criteria.

### Self Service API
Manage your project settings and retrieve supported currencies.

### Webhooks
Receive real-time notifications when order or transaction statuses change. See the [Webhook Event Schemas](/api-reference/webhooks) page for payload details.

## Usage Guides

For step-by-step walkthroughs of common integration patterns, see the [Guides](/docs/guides) section:

- [Hosted Payment Page](/docs/guides/hosted-payment-page) — accept payments via redirect
- [Recurring Payments](/docs/guides/recurring-payments) — set up subscriptions and scheduled billing
- [One-Click Payments](/docs/guides/one-click-payments) — fast checkout for returning customers
- [Auth / Capture / Void](/docs/guides/auth-capture-void) — two-step payment flows
- [Refunds](/docs/guides/refunds) — process full and partial refunds
- [Webhooks](/docs/guides/webhooks) — receive real-time status notifications
- [Payment Links](/docs/guides/payment-links) — create reusable payment links


---

## Cancel an order
<https://docs.costplus.io/api-reference/orders/cancelOrder>

> Cancel an order if it has not been completed yet.

# Cancel an order

Cancel an order if it has not been completed yet.

## Endpoint

```
DELETE /orders/{id}
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/{id}/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | Order identifier |

## Responses

### 200 — The cancelled order

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X DELETE https://api.costplus.online/v1/orders/4851e31c-4137-4e91-95ef-1df945ee76a2/
```


---

## Capture by amount
<https://docs.costplus.io/api-reference/orders/captureByAmount>

> Capture a specific amount from an authorized transaction.

# Capture by amount

Capture a specific amount from an authorized transaction.

## Endpoint

```
POST /orders/{id}/transactions/{transaction_id}/captures/amount
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/{id}/transactions/{transaction_id}/captures/amount/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | Order identifier |
| path | `transaction_id` | string | yes | Transaction identifier |
| header | `if-match` | string | no | ETag for optimistic locking. Only process if the order version matches. |

## Request body

Content-Type: `application/json` (required)

Fields:

- `description` — string. Capture description
- `amount` — integer. Amount to capture in minor units

Example:

```json
{
  "description": "Partial capture",
  "amount": 2500
}
```

## Responses

### 200 — The updated order

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

### 412 — ETag version mismatch — the order was modified since last fetched

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X POST 'https://api.costplus.online/v1/orders/{id}/transactions/{transaction_id}/captures/amount/' \
  -H 'Content-Type: application/json' \
  -H 'If-Match: <etag-from-GET-order>' \
  -d '{ "amount": 2500, "description": "Partial capture" }'
```


---

## Capture by order line
<https://docs.costplus.io/api-reference/orders/captureByOrderLine>

> Capture specific order lines from an authorized transaction.

# Capture by order line

Capture specific order lines from an authorized transaction.

## Endpoint

```
POST /orders/{id}/transactions/{transaction_id}/captures/orderlines
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/{id}/transactions/{transaction_id}/captures/orderlines/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | Order identifier |
| path | `transaction_id` | string | yes | Transaction identifier |
| header | `if-match` | string | no | ETag for optimistic locking. Only process if the order version matches. |

## Request body

Content-Type: `application/json` (required)

Fields:

- `description` — string. Capture description
- `order_line` — object

## Responses

### 200 — The updated order

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

### 412 — ETag version mismatch — the order was modified since last fetched

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X POST 'https://api.costplus.online/v1/orders/{id}/transactions/{transaction_id}/captures/orderlines/' \
  -H 'Content-Type: application/json' \
  -H 'If-Match: <etag-from-GET-order>' \
  -d '{ "order_lines": [ { "id": "line-1", "quantity": 1 } ] }'
```


---

## Create a new order
<https://docs.costplus.io/api-reference/orders/createOrder>

> Create a new payment order.

The response shape depends on the request body:
- If you send a `transactions` array, each transaction object in the response contains its own `payment_url`. Redirect the customer to that URL.
- If you omit `transactions`, the order has a top-level `order_url` instead — the hosted page lets the customer pick from all enabled payment methods.

See the [Hosted Payment Page guide](/docs/guides/hosted-payment-page) for both flows side by side.


# Create a new order

Create a new payment order.

The response shape depends on the request body:
- If you send a `transactions` array, each transaction object in the response contains its own `payment_url`. Redirect the customer to that URL.
- If you omit `transactions`, the order has a top-level `order_url` instead — the hosted page lets the customer pick from all enabled payment methods.

See the [Hosted Payment Page guide](/docs/guides/hosted-payment-page) for both flows side by side.

## Endpoint

```
POST /orders
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/
```

## Request body

Content-Type: `application/json` (required)

Fields:

- `id` — string. Order identifier
- `project_id` — string. Project identifier
- `merchant_id` — string. Merchant identifier used to create the order
- `order_url` — string. The order URL if available
- `refund_of_order_id` — string. Order ID of the original order, if this is a refund
- `refund_orders` — array. Order IDs of related refund orders
- `refunded_amount` — integer. Amount refunded on this order
- `chargeback_of_order_id` — string. Order ID of the original order, if this is a chargeback
- `chargeback_orders` — array. Order IDs of related chargeback orders
- `reversal_of_order_id` — string. Order ID of the original order, if this is a reversal
- `reversal_orders` — array. Order IDs of related reversal orders
- `related_order_id` — string. Order ID of the original order
- `related_orders` — array. Order IDs of related orders
- `related_payment_link_id` — string. ID of the related payment link
- `merchant_order_id` — string,null. Merchant's internal order identifier
- `merchant_bulk_id` — string,null. Merchant bulk ID (for grouping transactions for payouts)
- `merchant_name` — string,null. Merchant name (for payouts)
- `merchant_iban` — string,null. Merchant IBAN (for payouts)
- `merchant_bic` — string,null. Merchant BIC (for payouts)
- `created` — string. Creation timestamp (ISO 8601)
- `modified` — string,null. Last modified timestamp (ISO 8601)
- `completed` — string,null. Completion timestamp (ISO 8601)
- `expiration_period` — string,null. Order expiration interval (ISO 8601 duration, e.g. PT30M)
- `capture_mode` — string. Capture mode. `delayed` requires `expiration_period`.
- `currency` — string (required). ISO 4217 currency code
- `amount` — integer (required). Order amount in minor units (e.g. cents), including VAT
- `description` — string. Order description
- `status` — string. Current order status
- `flags` — array. Order flags
- `locale` — string,null. Language for the hosted payment page. Supported: `en-GB`, `de-DE`, `nl-NL`, `nl-BE`, `fr-BE`, `sv-SE`, `no-NO`, `da-DK`
- `payment_methods` — array. Filter available payment methods for this order. Omit to show all enabled methods, or pass a single value to restrict to one method. To offer multiple specific methods, use the `transactions` array instead (one entry per method).
- `transactions` — array. Collection of transactions
- `return_url` — string,null. URL to redirect the customer after a successful or non-failure payment. Acts as the default redirect for all statuses when `failure_url` is not provided.
- `failure_url` — string,null. URL to redirect the customer when the order status is `cancelled`, `expired`, or `error`. Only used when both `return_url` and `failure_url` are provided.
- `webhook_url` — string. URL for status change notifications
- `order_lines` — array. Order line items
- `customer` — object. Customer details
- `client` — object. Client integration details
- `extra` — object. Free-form data stored with the order

Example:

```json
{
  "currency": "EUR",
  "amount": 1295,
  "merchant_order_id": "my-order-id-1",
  "description": "My amazing order",
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    {
      "payment_method": "credit-card"
    }
  ]
}
```

## Responses

### 200 — The created order

```json
{
  "id": "4851e31c-4137-4e91-95ef-1df945ee76a2",
  "merchant_order_id": "my-order-id-1",
  "status": "new",
  "currency": "EUR",
  "amount": 1295,
  "description": "My amazing order",
  "created": "2026-01-15T12:00:05.433502+00:00",
  "modified": "2026-01-15T12:00:05.553125+00:00",
  "expiration_period": "PT1H",
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    {
      "id": "d291f03f-a406-428a-967a-4895a46e03fd",
      "payment_method": "credit-card",
      "status": "new",
      "amount": 1295,
      "currency": "EUR",
      "channel": "ecom",
      "credit_debit": "credit",
      "is_capturable": false,
      "expiration_period": "PT30M",
      "payment_url": "https://api.costplus.online/pay/4851e31c.../select-payment-method/credit-card/d291f03f.../",
      "payment_method_details": {}
    }
  ]
}
```

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X POST https://api.costplus.online/v1/orders/ \
  -H 'Content-Type: application/json' \
  -d '{
    "amount": 1295,
    "currency": "EUR",
    "merchant_order_id": "ORDER-1",
    "description": "Test order",
    "return_url": "https://example.com/return",
    "failure_url": "https://example.com/cancel",
    "webhook_url": "https://example.com/webhook",
    "transactions": [{ "payment_method": "credit-card" }]
  }'
```


---

## Create a refund
<https://docs.costplus.io/api-reference/orders/createRefund>

> Create a full or partial refund for an order.

# Create a refund

Create a full or partial refund for an order.

## Endpoint

```
POST /orders/{id}/refunds
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/{id}/refunds/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | Order identifier |

## Request body

Content-Type: `application/json` (required)

Fields:

- `amount` — integer. Refund amount in minor units
- `description` — string,null. Refund description
- `merchant_order_id` — string. Merchant's refund reference
- `order_lines` — array. Order lines to refund

Example:

```json
{
  "amount": 500,
  "description": "Refund for damaged item",
  "merchant_order_id": "refund-001"
}
```

## Responses

### 200 — The order with the refund applied

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X POST https://api.costplus.online/v1/orders/4851e31c-4137-4e91-95ef-1df945ee76a2/refunds/ \
  -H 'Content-Type: application/json' \
  -d '{ "amount": 500, "description": "Refund for damaged item" }'
```


---

## Tokenize card details
<https://docs.costplus.io/api-reference/orders/createToken>

> Exchange a card PAN and setup token for a vault token. This endpoint is used as part of the custom card entry form flow — after creating an order with `setup_token: true`, use this endpoint to securely vault the card details.

**Note:** This endpoint is authenticated via the `setup_token`, not your API key. No `Authorization` header is required.


# Tokenize card details

Exchange a card PAN and setup token for a vault token. This endpoint is used as part of the custom card entry form flow — after creating an order with `setup_token: true`, use this endpoint to securely vault the card details.

**Note:** This endpoint is authenticated via the `setup_token`, not your API key. No `Authorization` header is required.

## Endpoint

```
POST /tokens/
```

## Authentication

Public endpoint — no Basic Auth required.

## Request body

Content-Type: `application/json` (required)

Fields:

- `pan` — string (required). The full card number (PAN)
- `expiry_date` — string (required). Card expiry date in MMYY format
- `setup_token` — string (required). The setup token received from the order creation response

## Responses

### 200 — Card successfully tokenized

### 400 — Invalid request (bad PAN, expired setup token, etc.)

## cURL

```bash
curl -X POST https://api.costplus.online/v1/tokens/ \
  -H 'Content-Type: application/json' \
  -d '{ "pan": "4111111111111111", "expiry_date": "1228", "setup_token": "SETUP_TOKEN_FROM_ORDER" }'
```


---

## Get a single order
<https://docs.costplus.io/api-reference/orders/getOrder>

> Retrieve the full details of an order including its transactions.

Use `?fields[]=amount_details` to include a financial breakdown (captured, capturable, refunded, refundable, voided, voidable amounts) — useful for partial capture and refund scenarios.


# Get a single order

Retrieve the full details of an order including its transactions.

Use `?fields[]=amount_details` to include a financial breakdown (captured, capturable, refunded, refundable, voided, voidable amounts) — useful for partial capture and refund scenarios.

## Endpoint

```
GET /orders/{id}
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/{id}/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | Order identifier |
| query | `fields[]` | string | no | Request extra fields: `amount_details` (financial breakdown) or `order_line_details` (per-line capture/refund status) |

## Responses

### 200 — The order

```json
{
  "id": "4851e31c-4137-4e91-95ef-1df945ee76a2",
  "merchant_order_id": "my-order-id-1",
  "status": "completed",
  "currency": "EUR",
  "amount": 1295,
  "description": "My amazing order",
  "created": "2026-01-15T12:00:05.433502+00:00",
  "modified": "2026-01-15T12:02:30.123456+00:00",
  "completed": "2026-01-15T12:02:30.123456+00:00",
  "expiration_period": "PT1H",
  "return_url": "https://www.example.com",
  "webhook_url": "https://www.example.com/webhook",
  "transactions": [
    {
      "id": "d291f03f-a406-428a-967a-4895a46e03fd",
      "payment_method": "credit-card",
      "status": "completed",
      "amount": 1295,
      "currency": "EUR",
      "channel": "ecom",
      "credit_debit": "credit",
      "is_capturable": false,
      "payment_method_details": {
        "truncated_pan": "1111",
        "card_expiry": "122028"
      }
    }
  ]
}
```

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  'https://api.costplus.online/v1/orders/4851e31c-4137-4e91-95ef-1df945ee76a2/?fields[]=amount_details'
```


---

## List orders by date range
<https://docs.costplus.io/api-reference/orders/listOrders>

> Retrieve a collection of orders created within the specified period.

# List orders by date range

Retrieve a collection of orders created within the specified period.

## Endpoint

```
GET /orders
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | `created_since` | string | yes | Start of the period (ISO 8601) |
| query | `created_until` | string | yes | End of the period (ISO 8601) |

## Responses

### 200 — A collection of orders

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  'https://api.costplus.online/v1/orders/?created_since=2026-01-01T00:00:00Z&created_until=2026-02-01T00:00:00Z'
```


---

## List refunds for an order
<https://docs.costplus.io/api-reference/orders/listRefunds>

> Retrieve all refund orders associated with the specified order.

# List refunds for an order

Retrieve all refund orders associated with the specified order.

## Endpoint

```
GET /orders/{id}/refunds
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/{id}/refunds/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | Order identifier |

## Responses

### 200 — A collection of refund orders

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  https://api.costplus.online/v1/orders/4851e31c-4137-4e91-95ef-1df945ee76a2/refunds/
```


---

## Update an order
<https://docs.costplus.io/api-reference/orders/updateOrder>

> Update the properties of an existing order.

# Update an order

Update the properties of an existing order.

## Endpoint

```
PUT /orders/{id}
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/{id}/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | Order identifier |

## Request body

Content-Type: `application/json` (required)

Fields:

- `id` — string. Order identifier
- `project_id` — string. Project identifier
- `merchant_id` — string. Merchant identifier used to create the order
- `order_url` — string. The order URL if available
- `refund_of_order_id` — string. Order ID of the original order, if this is a refund
- `refund_orders` — array. Order IDs of related refund orders
- `refunded_amount` — integer. Amount refunded on this order
- `chargeback_of_order_id` — string. Order ID of the original order, if this is a chargeback
- `chargeback_orders` — array. Order IDs of related chargeback orders
- `reversal_of_order_id` — string. Order ID of the original order, if this is a reversal
- `reversal_orders` — array. Order IDs of related reversal orders
- `related_order_id` — string. Order ID of the original order
- `related_orders` — array. Order IDs of related orders
- `related_payment_link_id` — string. ID of the related payment link
- `merchant_order_id` — string,null. Merchant's internal order identifier
- `merchant_bulk_id` — string,null. Merchant bulk ID (for grouping transactions for payouts)
- `merchant_name` — string,null. Merchant name (for payouts)
- `merchant_iban` — string,null. Merchant IBAN (for payouts)
- `merchant_bic` — string,null. Merchant BIC (for payouts)
- `created` — string. Creation timestamp (ISO 8601)
- `modified` — string,null. Last modified timestamp (ISO 8601)
- `completed` — string,null. Completion timestamp (ISO 8601)
- `expiration_period` — string,null. Order expiration interval (ISO 8601 duration, e.g. PT30M)
- `capture_mode` — string. Capture mode. `delayed` requires `expiration_period`.
- `currency` — string (required). ISO 4217 currency code
- `amount` — integer (required). Order amount in minor units (e.g. cents), including VAT
- `description` — string. Order description
- `status` — string. Current order status
- `flags` — array. Order flags
- `locale` — string,null. Language for the hosted payment page. Supported: `en-GB`, `de-DE`, `nl-NL`, `nl-BE`, `fr-BE`, `sv-SE`, `no-NO`, `da-DK`
- `payment_methods` — array. Filter available payment methods for this order. Omit to show all enabled methods, or pass a single value to restrict to one method. To offer multiple specific methods, use the `transactions` array instead (one entry per method).
- `transactions` — array. Collection of transactions
- `return_url` — string,null. URL to redirect the customer after a successful or non-failure payment. Acts as the default redirect for all statuses when `failure_url` is not provided.
- `failure_url` — string,null. URL to redirect the customer when the order status is `cancelled`, `expired`, or `error`. Only used when both `return_url` and `failure_url` are provided.
- `webhook_url` — string. URL for status change notifications
- `order_lines` — array. Order line items
- `customer` — object. Customer details
- `client` — object. Client integration details
- `extra` — object. Free-form data stored with the order

## Responses

### 200 — The updated order

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X PUT https://api.costplus.online/v1/orders/4851e31c-4137-4e91-95ef-1df945ee76a2/ \
  -H 'Content-Type: application/json' \
  -d '{ "description": "Updated description" }'
```


---

## Void by amount
<https://docs.costplus.io/api-reference/orders/voidByAmount>

> Void a specific amount from an authorized transaction.

# Void by amount

Void a specific amount from an authorized transaction.

## Endpoint

```
POST /orders/{id}/transactions/{transaction_id}/voids/amount
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/{id}/transactions/{transaction_id}/voids/amount/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | Order identifier |
| path | `transaction_id` | string | yes | Transaction identifier |
| header | `if-match` | string | no | ETag for optimistic locking. Only process if the order version matches. |

## Request body

Content-Type: `application/json` (required)

Fields:

- `description` — string. Void description
- `amount` — integer. Amount to void in minor units

Example:

```json
{
  "description": "Partial void",
  "amount": 1500
}
```

## Responses

### 200 — The updated order

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

### 412 — ETag version mismatch — the order was modified since last fetched

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X POST 'https://api.costplus.online/v1/orders/{id}/transactions/{transaction_id}/voids/amount/' \
  -H 'Content-Type: application/json' \
  -H 'If-Match: <etag-from-GET-order>' \
  -d '{ "amount": 1500, "description": "Partial void" }'
```


---

## Void by order line
<https://docs.costplus.io/api-reference/orders/voidByOrderLine>

> Void specific order lines from an authorized transaction.

# Void by order line

Void specific order lines from an authorized transaction.

## Endpoint

```
POST /orders/{id}/transactions/{transaction_id}/voids/orderlines
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/orders/{id}/transactions/{transaction_id}/voids/orderlines/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | Order identifier |
| path | `transaction_id` | string | yes | Transaction identifier |
| header | `if-match` | string | no | ETag for optimistic locking. Only process if the order version matches. |

## Request body

Content-Type: `application/json` (required)

Fields:

- `description` — string. Void description
- `order_line` — object

## Responses

### 200 — The updated order

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

### 412 — ETag version mismatch — the order was modified since last fetched

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X POST 'https://api.costplus.online/v1/orders/{id}/transactions/{transaction_id}/voids/orderlines/' \
  -H 'Content-Type: application/json' \
  -H 'If-Match: <etag-from-GET-order>' \
  -d '{ "order_lines": [ { "id": "line-1", "quantity": 1 } ] }'
```


---

## Create a payment link
<https://docs.costplus.io/api-reference/payment-links/createPaymentLink>

> Create a new reusable payment link. The response includes a `payment_url` to share with the customer.
The customer can attempt payment multiple times (up to 25 attempts) until the link expires or payment succeeds.


# Create a payment link

Create a new reusable payment link. The response includes a `payment_url` to share with the customer.
The customer can attempt payment multiple times (up to 25 attempts) until the link expires or payment succeeds.

## Endpoint

```
POST /paymentlinks/
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/paymentlinks//
```

## Request body

Content-Type: `application/json` (required)

Fields:

- `id` — string. Payment link identifier
- `merchant_order_id` — string (required). Merchant's internal order ID
- `description` — string,null. Payment link description
- `amount` — integer (required). Amount in minor units (including VAT)
- `currency` — string (required). ISO 4217 currency code
- `expiration_period` — string,null. Expiration interval (ISO 8601 duration)
- `payment_url` — string,null. Payment URL to share with customer
- `payment_methods` — array. Limit available payment methods for this payment link. Supports multiple values.
- `created` — string. Creation timestamp
- `modified` — string. Last modified timestamp
- `completed_order_id` — string. ID of the completed order
- `status` — string. Payment link status
- `reason` — string. Reason for current status
- `completed` — string. Completion timestamp
- `orders` — object. Orders associated with this payment link
- `customer` — object. Customer details

Example:

```json
{
  "merchant_order_id": "invoice-1234",
  "amount": 995,
  "currency": "EUR",
  "description": "Invoice #1234"
}
```

## Responses

### 201 — The created payment link

```json
{
  "id": "e6eecc6a-47c5-4948-bcc0-d8b73f5c55a1",
  "merchant_order_id": "invoice-1234",
  "amount": 995,
  "currency": "EUR",
  "description": "Invoice #1234",
  "expiration_period": "P30D",
  "payment_url": "https://api.costplus.online/paymentlinks/e6eecc6a-47c5-4948-bcc0-d8b73f5c55a1/",
  "status": "new",
  "reason": "Payment Link was created, not yet visited",
  "orders": {},
  "created": "2026-01-15T12:00:00.000000Z"
}
```

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X POST https://api.costplus.online/v1/paymentlinks/ \
  -H 'Content-Type: application/json' \
  -d '{ "merchant_order_id": "invoice-1234", "amount": 995, "currency": "EUR", "description": "Invoice #1234" }'
```


---

## Get a payment link
<https://docs.costplus.io/api-reference/payment-links/getPaymentLink>

> Retrieve the details of a payment link, including all associated orders grouped by status.

# Get a payment link

Retrieve the details of a payment link, including all associated orders grouped by status.

## Endpoint

```
GET /paymentlinks/{id}
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/paymentlinks/{id}/
```

## Parameters

| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | `id` | string | yes | The payment link ID (returned when the link was created) |

## Responses

### 200 — The payment link

```json
{
  "id": "e6eecc6a-47c5-4948-bcc0-d8b73f5c55a1",
  "merchant_order_id": "invoice-1234",
  "amount": 995,
  "currency": "EUR",
  "description": "Invoice #1234",
  "expiration_period": "P30D",
  "payment_url": "https://api.costplus.online/paymentlinks/e6eecc6a-47c5-4948-bcc0-d8b73f5c55a1/",
  "status": "completed",
  "reason": "Completed",
  "completed": "2026-01-15T12:05:30.123456+00:00",
  "completed_order_id": "3bb663cc-2a20-400d-8bf6-18d9695d0c66",
  "orders": {
    "error": [
      "0d79014c-0aaa-4fd6-87c5-c8cfa5f5ac69"
    ],
    "completed": [
      "3bb663cc-2a20-400d-8bf6-18d9695d0c66"
    ]
  },
  "created": "2026-01-15T12:00:00.000000Z",
  "modified": "2026-01-15T12:05:35.654321Z"
}
```

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  https://api.costplus.online/v1/paymentlinks/e6eecc6a-47c5-4948-bcc0-d8b73f5c55a1/
```


---

## Search orders
<https://docs.costplus.io/api-reference/search/searchOrders>

> Search across orders using free-form criteria.

# Search orders

Search across orders using free-form criteria.

## Endpoint

```
POST /search/orders
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/search/orders/
```

## Request body

Content-Type: `application/json` (required)

## Responses

### 200 — Search results

### 401 — Invalid or missing API key

### 403 — No authorization for the requested resource

## cURL

```bash
curl -u YOUR_API_KEY: \
  -X POST https://api.costplus.online/v1/search/orders/ \
  -H 'Content-Type: application/json' \
  -d '{ "merchant_order_id": "ORDER-1" }'
```


---

## Get supported currencies
<https://docs.costplus.io/api-reference/self-service/getSupportedCurrencies>

> List supported currencies for each available payment method in your project.

# Get supported currencies

List supported currencies for each available payment method in your project.

## Endpoint

```
GET /merchants/self/projects/self/currencies
```

## Authentication

HTTP Basic — use your API key as the username with an empty password.

```bash
curl -u YOUR_API_KEY: https://api.costplus.online/v1/merchants/self/projects/self/currencies/
```

## Responses

### 200 — Supported currencies per payment method

## cURL

```bash
curl -u YOUR_API_KEY: \
  https://api.costplus.online/v1/merchants/self/projects/self/currencies/
```


---

## Webhook Event Schemas
<https://docs.costplus.io/api-reference/webhooks>

> Payload formats for Cost+ webhook notifications

Cost+ sends webhook notifications to your configured `webhook_url` when order or transaction statuses change. This page documents the event payload formats.

For webhook configuration, retry logic, and best practices, see the [Webhooks guide](/docs/guides/webhooks).

## OrderStatusChangedEvent

Sent when an order's status changes (e.g., from `new` to `completed`).

```json title="Example payload"
{
  "event": "status_changed",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "order_id": "b9ae6d70-1234-5678-9abc-def012345678"
}
```

| Field | Type | Description |
|---|---|---|
| `event` | string | Always `"status_changed"` |
| `project_id` | string (UUID) | The project this order belongs to |
| `order_id` | string (UUID) | The order whose status changed |

> [!WARNING]
> The webhook payload only contains the order ID — **not** the new status. Always verify the current status by calling [Get Order](/api-reference/orders/getOrder) before taking action (e.g., shipping goods or granting access).

## TransactionStatusChangedEvent

Sent when an individual transaction's status changes within an order.

```json title="Example payload"
{
  "event": "transaction_status_changed",
  "merchant_id": "f1e2d3c4-b5a6-7890-fedc-ba0987654321",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "order_id": "b9ae6d70-1234-5678-9abc-def012345678",
  "transaction_id": "c8d7e6f5-4321-0987-6543-210fedcba098",
  "transaction_status": "completed"
}
```

| Field | Type | Description |
|---|---|---|
| `event` | string | Always `"transaction_status_changed"` |
| `merchant_id` | string (UUID) | Your merchant identifier |
| `project_id` | string (UUID) | The project this order belongs to |
| `order_id` | string (UUID) | The parent order |
| `transaction_id` | string (UUID) | The specific transaction whose status changed |
| `transaction_status` | string | New status: `new`, `pending`, `processing`, `accepted`, `captured`, `completed`, `cancelled`, `error`, or `expired` |

## Delivery & Retries

- Cost+ retries up to **10 times**, spaced **2 minutes apart**
- First attempt times out after **4 seconds**; subsequent retries after **10 seconds**
- Your endpoint must return a **2xx** status code to acknowledge receipt
- If all 10 retries fail, the event is marked as failed

## Webhook URL Configuration

You can set the webhook URL in two ways:

1. **Per order** — include `webhook_url` in the [Create Order](/api-reference/orders/createOrder) request
2. **Project-level** — configure a default webhook URL in the [Merchant Portal](https://dashboard.costplus.io/) under your website settings


---
