> For the complete documentation index, see [llms.txt](https://docs.akinon.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.akinon.com/technical-guides/akinon-subscription-system.md).

# Akinon Subscription System

This document explains the requirements for the Extension and its integration with Commerce for setting up the subscription system on Akinon Commerce using the Checkout.com payment method.

{% hint style="info" %}
This subscription system works with the Checkout.com payment method. Checkout.com supports card tokenization and recurring payment capabilities, which are required for the subscription system.
{% endhint %}

## <mark style="color:red;">Architecture</mark>

The system consists of three main components:

* **Commerce (Shop):** Order management, checkout flow, and IPN processing.
* **Extension:** Subscription management, recurring payment triggering, and Checkout.com API calls.
* **Checkout.com:** Card tokenization, payment processing, and recurring payment support.

The **Customer (Frontend)** interacts with **Commerce (Shop)**. Commerce integrates with the **Extension** for subscription management and with **Checkout.com (CKO)** for payment processing.

### <mark style="color:red;">Subscription Flow</mark>

**1. Initial Order (Customer-Initiated via Checkout.com)**

The customer is redirected to the Checkout.com payment page through the Commerce checkout flow.

The customer enters their card details and selects **"Save Card"** (`store_for_future_use: true`).

Checkout.com returns the `source_id` and `payment_id`.

Commerce creates the order. If `Order.extra_field.is_subscription` is set to `true`, the Extension registers the order as a subscription.

**2. Subscription Detection (Extension)**

The Extension listens for newly created orders and validates the following:

* `Order.extra_field.is_subscription == true`
* `OrderItem.product.attributes.subscription_period` is defined

**3. Subscription Creation**

If the order is identified as a subscription, the Extension creates a subscription record with the following information:

* `user_id`
* `product_id`
* `source_id`
* `payment_id`
* `period` (retrieved from the product attribute)
* `next_payment_date` (calculated based on the subscription period)

4. **Recurring Payment (Automatically Triggered by the Extension)**

When the `next_payment_date` is reached, the Extension automatically performs the following actions:

* Initiates a checkout process in Commerce on behalf of the customer.
* Creates a basket and selects the shipping address, shipping method, and payment method.
* Sends a **RECURRING** payment request to Checkout.com using the stored `source_id`.
* Sends an IPN to Commerce containing the payment result.
* Commerce creates a new order or updates the existing order accordingly.

**5. Failed Payment (Retry Mechanism)**

If a recurring payment fails, the Extension performs the following actions:

* Receives the failed payment response from Checkout.com.
* Sends a **DECLINED** IPN to Commerce for transaction logging.
* Retries the payment according to the configured retry policy.
* Notifies the customer if the maximum retry limit is exceeded.

### <mark style="color:red;">Responsibility Matrix</mark>

| Responsibility                                |    Commerce   | Extension |
| --------------------------------------------- | :-----------: | :-------: |
| Extension Gateway (POS) configuration         |       ✓       |     —     |
| PaymentOption configuration                   |       ✓       |     —     |
| Checkout API                                  |       ✓       |     —     |
| IPN endpoint                                  |       ✓       |     —     |
| Transaction logging                           | ✓ (automatic) |     —     |
| Order creation/update                         | ✓ (automatic) |     —     |
| Subscription management                       |       —       |     ✓     |
| Checkout initiation on behalf of the customer |       —       |     ✓     |
| Stored card management                        |       —       |     ✓     |
| Recurring payment processing                  |       —       |     ✓     |
| IPN submission                                |       —       |     ✓     |
| Retry mechanism                               |       —       |     ✓     |
| Customer notifications                        |       —       |     ✓     |
| Frontend integration                          |       —       |     ✓     |

## <mark style="color:red;">Commerce Configuration</mark>

### <mark style="color:red;">1. Create a POS Configuration</mark>

A POS record must be created in the Commerce Admin Panel for the Extension.

| Field            | Value                               | Description            |
| ---------------- | ----------------------------------- | ---------------------- |
| **Name**         | `Subscription Extension`            | Display name           |
| **Slug**         | `subscription-extension`            | Unique identifier      |
| **Gateway**      | `extension`                         | Extension gateway type |
| **Resource URL** | `https://extension.example.com/api` | Extension API endpoint |

**Config (JSON)**

```json
{
  "auth": {
    "username": "extension_webhook_user",
    "password": "extension_webhook_password"
  },
  "hash_key": "your_secret_hash_key",
  "payment_type": "SALE",
  "url": "https://extension.example.com/api"
}
```

**Configuration Parameters**

| Field           | Type     | Required | Description                                                               |
| --------------- | -------- | :------: | ------------------------------------------------------------------------- |
| `auth.username` | `string` |     ✓    | Username for IPN Basic Authentication                                     |
| `auth.password` | `string` |     ✓    | Password for IPN Basic Authentication                                     |
| `hash_key`      | `string` |     ✓    | Secret key used for request signing                                       |
| `payment_type`  | `string` |     ✓    | Payment type: `SALE` (capture immediately) or `AUTH` (authorization only) |
| `url`           | `string` |     ✓    | Extension API endpoint                                                    |

{% hint style="info" %}
The `auth.username` and `auth.password` values are the Basic Authentication credentials that the Extension uses when sending IPN requests to Commerce.
{% endhint %}

#### <mark style="color:red;">Sharing Authentication Credentials</mark>

These credentials are defined by the Commerce team and shared with the Extension team as follows:

1. The Commerce team defines the `auth.username` and `auth.password` values when creating the POS configuration.
2. These credentials are securely shared with the Extension team through a secure channel (e.g., encrypted email or a secret management solution).
3. The Extension uses these credentials in the **Basic Authentication** header when sending IPN requests to Commerce.

| Commerce (POS Configuration)   | Extension (IPN Request Header)                                  |
| ------------------------------ | --------------------------------------------------------------- |
| `auth.username: "ext_user"`    | `Authorization: Basic <base64("ext_user:ext_pass123")>`         |
| `auth.password: "ext_pass123"` | Uses the credentials defined in the Commerce POS configuration. |

{% hint style="info" %}
**Extension Developers:** Request these credentials from the Commerce integration team. Separate credentials will be provided for the **Staging** and **Production** environments.
{% endhint %}

### <mark style="color:red;">2. PaymentOption Configuration</mark>

A **PaymentOption** must be created in the Commerce Admin Panel for subscription payments.

| Field            | Value                  | Description                                     |
| ---------------- | ---------------------- | ----------------------------------------------- |
| **Name**         | `Subscription Payment` | Display name                                    |
| **Slug**         | `subscription-payment` | Unique identifier                               |
| **Payment Type** | `redirection`          | Redirect-based payment method                   |
| **Is Active**    | `true`                 | Indicates whether the payment option is enabled |

**Config (JSON):**

```json
{
    "pos_slug": "subscription-extension",
    "rule": {
        "klass": "omnishop.payments.rules.AnyRule",
        "params": {}
    }
}
```

## <mark style="color:red;">1. Initial Order (Customer-Initiated via Checkout.com)</mark>

The initial order is the customer's first purchase completed through the standard Commerce checkout flow using the Checkout.com payment method.

#### <mark style="color:red;">Flow</mark>

1. The customer adds a subscription product to the shopping cart.
2. The customer completes the checkout steps, including shipping address and shipping method.
3. The customer selects **Checkout.com** as the payment method.
4. The customer is redirected to the Checkout.com payment page.
5. The customer enters their card details and selects the **"Save Card"** option.
6. If the payment is successful, Commerce creates the order with the status set to **`approved`**.

### <mark style="color:red;">Order Data Structure</mark>

The following information is stored when the initial subscription order is created:

**Order.extra\_field:**

```
{
    "is_subscription": true,
    "checkout_source_id": "src_vtgfojwun22ubcew6iyuomjici",
    "checkout_payment_id": "pay_abc123xyz"
}
```

**OrderItem.product.attributes (Defined on the Product):**

```json
{
    "subscription_period": "monthly",
    "subscription_period_days": 30
}
```

### <mark style="color:red;">PreOrder.data Structure (Internal)</mark>

During the checkout process, Commerce stores the following information in the `PreOrder.data` field:

```
{
    "is_subscription": true,
    "is_renewal": false,
    "checkout_source_id": "src_vtgfojwun22ubcew6iyuomjici",
    "checkout_payment_id": "pay_abc123xyz",
    "redirection_started": true,
    "redirect_url": "/orders/redirection/"
}
```

| Field                 | Type     | Description                                                         |
| --------------------- | -------- | ------------------------------------------------------------------- |
| `is_subscription`     | `bool`   | Indicates whether the order is a subscription order.                |
| `is_renewal`          | `bool`   | Indicates whether the order is a renewal (recurring payment) order. |
| `checkout_source_id`  | `string` | Reference to the stored payment source (card).                      |
| `checkout_payment_id` | `string` | Reference to the initial payment.                                   |
| `redirection_started` | `bool`   | Indicates whether the payment redirection process has started.      |
| `redirect_url`        | `string` | Extension redirect URL.                                             |

{% hint style="info" %}
Orders with `is_renewal: true` are recurring payment orders created by the Extension. For the initial subscription order, `is_renewal` is always set to `false`.
{% endhint %}

### <mark style="color:red;">Checkout.com Initial Payment Payload</mark>

For the initial subscription order, Commerce sends the following payload to Checkout.com:

```json
{
    "payment_type": "Regular",
    "merchant_initiated": false,
    "source": {
        "type": "card",
        "number": "4242424242424242",
        "expiry_month": 12,
        "expiry_year": 2025,
        "cvv": "123",
        "store_for_future_use": true
    },
    "amount": 9999,
    "currency": "TRY"
}
```

{% hint style="info" %}
The `store_for_future_use: true` parameter instructs Checkout.com to securely store the customer's card, allowing it to be used for future recurring payments.
{% endhint %}

### <mark style="color:red;">Checkout.com Response</mark>

If the payment is successful, Checkout.com returns the following information:

```json
{
    "id": "pay_abc123xyz",
    "status": "Captured",
    "source": {
        "id": "src_vtgfojwun22ubcew6iyuomjici",
        "type": "card",
        "last4": "4242",
        "expiry_month": 12,
        "expiry_year": 2025
    }
}
```

| Field       | Description                                                                                                                         |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `source.id` | Reference to the stored payment source (card). This value is used for future recurring payments.                                    |
| `id`        | Identifier of the initial payment. This value is stored and later used as the `previous_payment_id` for recurring payment requests. |

### <mark style="color:red;">Extension Responsibilities</mark>

The Extension listens for newly created orders (via webhook or polling):

1. **Perform a subscription check:**

* Is `Order.extra_field.is_subscription == true`?
* Does the product attribute contain `subscription_period`?

2. **Create a subscription record:**

* `user_id`
* `order_id`
* `product_id`
* `source_id` (`Order.extra_field.checkout_source_id`)
* `first_payment_id` (`Order.extra_field.checkout_payment_id`)
* `period` (from the product attribute)
* `next_payment_date` (current date/time + subscription period)

## <mark style="color:red;">2. Recurring Payment (Triggered by the Extension)</mark>

When the subscription renewal date (`next_payment_date`) is reached, the Extension automatically creates an order on behalf of the customer and initiates a recurring payment through Checkout.com.

### <mark style="color:red;">Step 2.1: Generate a Customer Login Token</mark>

The Extension generates a one-time token to log in on behalf of the customer using administrator privileges:

**Method:** `POST`

**Path:** `/api/v1/remote/1/passwordless-login/`

**Authorization:** `Token <admin_token>`

**Content-Type:** `application/json`

```json
{
    "redirect_url": "/users/passwordless-login/<one_time_token>/"
}
```

{% hint style="info" %}
The `secret_key` value must be configured in Commerce settings as `PASSWORDLESS_LOGIN_SECRET_KEY`.
{% endhint %}

The customer session can be obtained by sending a **GET** request to this URL.

Alternatively, the `passwordless-login-with-token` endpoint can be used:

**Method:** `POST`

**Path:** `/users/passwordless-login-with-token/`

**Content-Type:** `application/json`

```json
{
    "user": <user_pk>,
    "token": "<one_time_token>"
}
```

**Response:**

```json
{
    "key": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
```

### <mark style="color:red;">Step 2.2: Create a Basket and Add the Product</mark>

The Extension creates a basket on behalf of the customer.

**Method:** `POST`

**Path:** `/baskets/basket/`

**Authorization:** `Token <customer_token>`

**Content-Type:** `application/json`

```json
{
    "product": <product_pk>,
    "quantity": 1,
    "attributes": {
        "is_renewal": true,
        "original_order_id": <first_order_pk>
    }
}
```

| Field                          | Type      | Required | Description                                |
| ------------------------------ | --------- | :------: | ------------------------------------------ |
| `product`                      | `integer` |     ✓    | Product ID                                 |
| `quantity`                     | `integer` |     ✓    | Quantity                                   |
| `attributes.is_renewal`        | `bool`    |     —    | Indicates whether this is a renewal order. |
| `attributes.original_order_id` | `integer` |     —    | ID of the original subscription order.     |

{% hint style="info" %}
The `is_renewal: true` and `original_order_id` values indicate that this order is a subscription renewal. These values are propagated to `Order.extra_field`.
{% endhint %}

### <mark style="color:red;">Step 2.3: Complete the Checkout Flow</mark>

The checkout flow is managed through a single endpoint. At each step, the current stage of the checkout process is specified using the `page` parameter.

#### <mark style="color:red;">Address Selection</mark>

**Method:** `POST`

**Path:** `/orders/checkout/`

**Authorization:** `Token <customer_token>`

**X-Requested-With:** `XMLHttpRequest`

**Content-Type:** `application/json`

```json
{
    "page": "AddressSelectionPage",
    "shipping_address": <address_pk>,
    "billing_address": <address_pk>
}
```

#### <mark style="color:red;">Shipping Method Selection</mark>

**Method:** `POST`

**Path:** `/orders/checkout/`

**Authorization:** `Token <customer_token>`

**X-Requested-With:** `XMLHttpRequest`

**Content-Type:** `application/json`

```json
{
    "page": "ShippingOptionSelectionPage",
    "shipping_option": <shipping_option_pk>
}
```

### <mark style="color:red;">Step 2.4: Select the Extension Payment Option</mark>

**Method:** `POST`

**Path:** `/orders/checkout/`

**Authorization:** `Token <customer_token>`

**X-Requested-With:** `XMLHttpRequest`

**Content-Type:** `application/json`

```json
{
    "page": "PaymentOptionSelectionPage",
    "payment_option": <subscription_payment_option_pk>
}
```

### <mark style="color:red;">Step 2.5: Initiate the Redirection Payment</mark>

**Method:** `POST`

**Path:** `/orders/checkout/`

**Authorization:** `Token <customer_token>`

**X-Requested-With:** `XMLHttpRequest`

**Content-Type:** `application/json`

```json
{
    "page": "RedirectionPaymentSelectedPage",
    "agreement": true
}
```

**Response:**

```json
{
    "next_page": "ThankYouPage",
    "page_context": {
        "redirect_url": "/orders/redirection/...",
        "remote_redirect_url": "https://extension.com/pay/...",
        "pre_order_number": "PRE-ORD-123456"
    }
}
```

{% hint style="warning" %}
Store the `pre_order_number` value. It will be used as the `order_id` when sending the IPN.
{% endhint %}

{% hint style="warning" %}
**Redirect Requirement:** You **must** send a **GET** request to the `redirect_url` returned in the response (or redirect the browser to this URL). This step transitions the order to the `payment_waiting` status in Commerce. If this step is skipped, sending the IPN may result in an **"Order not found"** or **"Transaction not found"** error.
{% endhint %}

{% hint style="info" %}
**For Automation:** The Extension backend should issue an HTTP **GET** request to the `redirect_url` and follow the resulting **302 redirect** automatically.
{% endhint %}

### <mark style="color:red;">PreOrder and Cache Mechanism</mark>

When the checkout flow is completed, Commerce performs the following actions:

* Creates a **PreOrder** (an Order has not yet been created).
* Stores the **PreOrder** in the cache using the basket hash.
* Creates a **Redirect Transaction** record.

**Transaction ID Formats**

Redirect Transaction: `REDIRECT_{pre_order_number}`

Webhook Transaction: `WEBHOOK_{pre_order_number}_{unique_id`

**Cache Mechanism**

Cache Key: `REDIRECTION_REDIRECT_{pre_order_number}`

Cache Value: `basket_hash` (ex: a1b2c3d4e5f6...)

**IPN Processing Flow**

When an IPN is received, Commerce performs the following steps:

* Finds the **Redirect Transaction** using the `order_id` (`pre_order_number`).
* Generates the cache key from the transaction ID:\
  `REDIRECTION_REDIRECT_{order_id}`
* Retrieves the `basket_hash` from the cache.
* Uses the `basket_hash` to retrieve the **PreOrder** from the OrderStore.
* Creates the actual **Order** from the retrieved **PreOrder**.

**Cache Expiration:** By default, the PreOrder cache is valid for **30 minutes**. The IPN must be sent within this time window.

### <mark style="color:red;">Step 2.6: Charge Recurring Payment via Checkout.com</mark>

The Extension initiates a recurring payment using the stored card (`source_id`) via the Checkout.com API.

**Method:** `POST`

**Base URL:** `https://api.checkout.com/payments`

**Authorization:** `Bearer <checkout_secret_key>`

**Content-Type:** `application/json`

```json
{
    "payment_type": "Recurring",
    "merchant_initiated": true,
    "source": {
        "type": "id",
        "id": "src_vtgfojwun22ubcew6iyuomjici",
        "stored": true
    },
    "previous_payment_id": "pay_abc123xyz",
    "amount": 9999,
    "currency": "TRY",
    "reference": "PRE-ORD-123456"
}
```

| Field                 | Description                                             |
| --------------------- | ------------------------------------------------------- |
| `payment_type`        | `"Recurring"` – Indicates a recurring payment           |
| `merchant_initiated`  | `true` – Payment initiated without customer interaction |
| `source.type`         | `"id"` – Indicates stored card reference                |
| `source.id`           | Stored `source_id` from the initial payment             |
| `source.stored`       | `true` – Confirms stored card usage                     |
| `previous_payment_id` | `payment_id` from the initial transaction               |
| `reference`           | Commerce PreOrder number                                |

### <mark style="color:red;">Checkout.com Response</mark>

**Successful**

```json
{
    "id": "pay_newpayment123",
    "status": "Captured",
    "approved": true,
    "response_code": "10000"
}
```

**Unsuccessful Response**

```json
{
    "id": "pay_failedpayment456",
    "status": "Declined",
    "approved": false,
    "response_code": "20005",
    "response_summary": "Insufficient Funds"
}
```

### <mark style="color:red;">Step 2.7: Send IPN to Commerce</mark>

Based on the payment result received from Checkout.com, an IPN is sent to Commerce.

#### <mark style="color:red;">Successful Payment</mark>

**Method:** `POST`

**Path:** `/orders/hooks/payment`

**Authorization:** `Basic <base64(username:password)>`

**Content-Type:** `application/json`

```json
{
    "event": "COMPLETED",
    "eventBody": {
        "order_id": "PRE-ORD-123456",
        "amount": "99.99",
        "installment_count": "1",
        "interest_fee": "0.00",
        "status": "RESOLVED",
        "substatus": "RESOLVED"
    }
}
```

#### <mark style="color:red;">Unsuccessful Payment</mark>

**Method:** `POST`

**Path:** `/orders/hooks/payment`

**Authorization:** `Basic <base64(username:password)>`

**Content-Type:** `application/json`

```json
{
    "event": "DECLINED",
    "eventBody": {
        "order_id": "PRE-ORD-123456",
        "amount": "99.99",
        "installment_count": "1",
        "interest_fee": "0.00",
        "status": "REJECTED",
        "substatus": "RESOLVED"
    }
}
```

## <mark style="color:red;">3. IPN (Instant Payment Notification) Format</mark>

### <mark style="color:red;">Endpoint</mark>

**Method:** `POST`\
**Path:** `/orders/hooks/payment`

### <mark style="color:red;">Authentication</mark>

Basic Authentication is used. The credentials are taken from the POS configuration shared by Commerce:

```
Authorization: Basic <base64(username:password)>
```

### <mark style="color:red;">Example (Python)</mark>

```python
import base64
 
username = "ext_user"  # Commerce'den alınan
password = "ext_pass123"  # Commerce'den alınan
 
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
headers = {
    "Authorization": f"Basic {credentials}",
    "Content-Type": "application/json"
}
```

{% hint style="info" %}
Please refer to the **“Auth Credentials Sharing”** section for details on where to obtain these values.
{% endhint %}

### <mark style="color:red;">IPN Event Body Fields</mark>

| Field               | Type     | Required | Description                                     |
| ------------------- | -------- | :------: | ----------------------------------------------- |
| `order_id`          | `string` |     ✓    | PreOrder number (e.g. `PRE-ORD-123456`)         |
| `amount`            | `string` |     ✓    | Paid amount                                     |
| `installment_count` | `string` |     ✓    | Number of installments (minimum `"1"`)          |
| `interest_fee`      | `string` |     ✓    | Installment interest fee (use `"0.00"` if none) |
| `status`            | `string` |     ✓    | Main status: `RESOLVED` or `REJECTED`           |
| `substatus`         | `string` |     ✓    | Detailed status: `RESOLVED`, `BANK_ERROR`, etc. |

### <mark style="color:red;">Successful Payment Example</mark>

```json
{
    "event": "COMPLETED",
    "eventBody": {
        "order_id": "PRE-ORD-123456",
        "amount": "99.99",
        "installment_count": "1",
        "interest_fee": "0.00",
        "status": "RESOLVED",
        "substatus": "RESOLVED"
    }
}
```

### <mark style="color:red;">Unsuccessful Payment Example</mark>

```json
{
    "event": "DECLINED",
    "eventBody": {
        "order_id": "PRE-ORD-123456",
        "amount": "99.99",
        "installment_count": "1",
        "interest_fee": "0.00",
        "status": "REJECTED",
        "substatus": "RESOLVED"
    }
}
```

### <mark style="color:red;">Pending (Async) Payment Example</mark>

If the payment has not been finalized yet:

```json
{
    "event": "INITIALIZED",
    "eventBody": {
        "order_id": "PRE-ORD-123456",
        "amount": "99.99",
        "installment_count": "1",
        "interest_fee": "0.00",
        "status": "PENDING",
        "substatus": "PAYMENT_WAITING"
    }
}
```

### <mark style="color:red;">Event Types</mark>

| Event         | Description                                         |
| ------------- | --------------------------------------------------- |
| `INITIALIZED` | Payment has been initiated and is awaiting a result |
| `COMPLETED`   | Payment has been successfully completed             |
| `DECLINED`    | Payment has been rejected                           |

### <mark style="color:red;">Status / Substatus Values</mark>

| Status     | Substatus         | Description                |
| ---------- | ----------------- | -------------------------- |
| `RESOLVED` | `RESOLVED`        | Successful payment         |
| `PENDING`  | `PAYMENT_WAITING` | Waiting for payment result |
| `REJECTED` | `RESOLVED`        | Payment rejected           |
| `REJECTED` | `BANK_ERROR`      | Bank error                 |

{% hint style="info" %}
Status and Substatus fields also accept numeric values:

* **Status:** `RESOLVED = 3`, `PENDING = 4`, `REJECTED = 2`
* **Substatus:** `RESOLVED = 7`, `PAYMENT_WAITING = 8`
  {% endhint %}

### <mark style="color:red;">IPN Response</mark>

When Commerce successfully receives the IPN:

* **HTTP Status:** `200 OK`

In case of an error:

* **HTTP Status:** `406 Not Acceptable`
* **Content-Type:** `application/json`

```json
{
    "errors": [
        {
            "field": "",
            "message": "Error message here",
            "code": "error_code"
        }
    ]
}
```

{% hint style="info" %}

### Two Different Retry Scenarios

IPN (Instant Payment Notification) is an automatic server-to-server notification of a change in payment status. There are two retry scenarios, and they should not be confused:

**406 “wait” signal (timing):** When Commerce considers the request too early, it returns HTTP 406. This is not an error; it means “wait a bit and resend.” Wait 60 seconds and resend the same IPN. Repeat until HTTP 200 OK is received (maximum 5 attempts).

**General delivery failure (retry / backoff):** If the IPN receives any response other than 200 OK or no response at all, the request is retried using exponential backoff up to a maximum of 5 times.

In both cases, the goal is to receive an HTTP 200 response.
{% endhint %}

### <mark style="color:red;">Possible Error Codes</mark>

| HTTP Status | Code                    | Description                      |
| ----------- | ----------------------- | -------------------------------- |
| `406`       | `webhook_300`           | General webhook processing error |
| `406`       | `authentication_failed` | Incorrect Basic Auth credentials |
| `406`       | `validation_error`      | Request body validation error    |

{% hint style="warning" %}
Continue resending the IPN until you receive an HTTP **200 OK** response (maximum 5 attempts with exponential backoff).
{% endhint %}

### <mark style="color:red;">Webhook Timing Rule</mark>

Commerce rejects webhooks that arrive too quickly. A minimum time interval must pass after the first webhook before another one can be processed.

Additionally, after the **RedirectionPaymentSelectedPage** step, either the redirect URL must be called or a short waiting period is required for the system to transition into the `payment_waiting` state.

**Default Waiting Time: 60 seconds**

**FIRST WEBHOOK (INITIALIZED)**

* Commerce processes and stores the request
* Returns **200 OK**
* → Wait **60 seconds** (Retry Logic)
* ↓

**SECOND WEBHOOK (COMPLETED)**

* Commerce processes the request
* Creates the order
* Returns **200 OK**

Sending a webhook before **45 seconds**:

* **HTTP 406 Not Acceptable**

```json
{
    "errors": [{"code": "webhook_300", "message": "Cant process webhook."}]
}
```

{% hint style="warning" %}
If you receive a **406 Not Acceptable** error when sending the IPN, this is not an error but a waiting signal.

1. Wait 60 seconds.
2. Resend the same IPN request.
3. Repeat until you receive **200 OK** (max 3 attempts).
   {% endhint %}

### <mark style="color:red;">Automatic Processes on the Commerce Side</mark>

When an IPN is received, Commerce automatically:

#### <mark style="color:red;">1. Creates a Transaction Record</mark>

A transaction record is created for each IPN.

**Transaction Types**

| Type       | `transaction_id` Format              | Description                        |
| ---------- | ------------------------------------ | ---------------------------------- |
| `redirect` | `REDIRECT_{order_number}`            | Created when checkout is initiated |
| `webhook`  | `WEBHOOK_{order_number}_{unique_id}` | Created for each IPN               |

**Transaction Fields**

| Field                | Value                                   |
| -------------------- | --------------------------------------- |
| `transaction_id`     | `WEBHOOK_{order_number}_{unique_id}`    |
| `transaction_type`   | `webhook`                               |
| `is_succeeded`       | `true/false` based on event             |
| `raw_request`        | IPN body (JSON string)                  |
| `raw_response`       | Commerce response body                  |
| `async_order_status` | `RESOLVED`, `PENDING`, `REJECTED`, etc. |
| `amount`             | Payment amount                          |

**Transaction Query:**\
Transaction history can be queried via the Admin API using the following endpoint:

`/api/v1/remote/1/transactions/?order={order_pk}`

### <mark style="color:red;">2. Update Order Status</mark>

| Event         | Status     | Order Status                       |
| ------------- | ---------- | ---------------------------------- |
| `COMPLETED`   | `RESOLVED` | `approved`                         |
| `INITIALIZED` | `PENDING`  | `payment_waiting`                  |
| `DECLINED`    | `REJECTED` | Order is not created / not updated |

### <mark style="color:red;">3. Create Order from PreOrder</mark>

On the first successful IPN, the real order is created using the PreOrder data:

* Basket, address, and shipping information from the PreOrder are copied.
* Subscription-related data from `PreOrder.data` is transferred to `Order.extra_field`.
* Order items are created.
* Stock is deducted.
* The order status is set to **`approved`**.

## <mark style="color:red;">4. Failed Payment and Retry Mechanism</mark>

### <mark style="color:red;">Extension Responsibilities</mark>

**1. Send IPN for each failed payment:**

```json
{
    "event": "DECLINED",
    "eventBody": {
        "order_id": "PRE-ORD-123456",
        "status": "REJECTED",
        ...
    }
}
```

This ensures that a transaction log is created in Commerce and failed attempts are properly recorded.

**2. Apply retry policy:**

Example: 3 attempts, 24-hour intervals

* Send a separate IPN for each attempt
* If all attempts fail, suspend the subscription

**3. Customer notification:**

* **Email:** “Your payment has failed. Please create a new order to continue your subscription.”
* **Push notification (optional)**
* **SMS (optional)**

### <mark style="color:red;">Frontend Integration</mark>

To display failed payment warnings on the customer account page:

**1. List orders:**

**Method:** `GET`\
**Path:** `/users/orders/?status=payment_waiting`

**Authorization:** `Token <customer_token>`

**2. Check transaction history:**

**Method:** `GET`\
**Path:** `/api/v1/remote/1/transactions/?order={order_pk}`

**Authorization:** `Token <admin_token>`

**3. Show warning if there is a failed transaction:**

* “Your payment has failed”
* “Update your card details” button

### <mark style="color:red;">Card Update Flow</mark>

The system does not support a direct “card update” feature. When a customer wants to change their payment method or the existing card becomes invalid:

* The existing subscription is canceled (either by the customer or automatically by the system after failed payments).
* The customer places a new subscription order using a new card.
* The old subscription remains in `cancelled` status, and the new subscription starts with `active` status.

## <mark style="color:red;">5. Subscription Lifecycle</mark>

### <mark style="color:red;">Status Transitions</mark>

A subscription moves between the following states:

* `active` → when payment time arrives, it transitions to `processing`
* `processing` → if payment is successful, it returns to `active`
* `processing` → if payment fails, it transitions to `retry_pending`
* `retry_pending` → if retry is successful, it returns to `active`
* `retry_pending` → if max retry limit is exceeded, it transitions to `suspended`
* `active / suspended` → when the period ends, is canceled, or a new order is required, it transitions to `completed` or `cancelled`

```

[active] --------------------------------------------+
   |                                                   |
   v (odeme zamani)                                    |
[processing] ---+-----------------------------------+ |
   |            |                                    | |
   | (basarili) | (basarisiz)                        | |
   v            v                                    | |
[active]   [retry_pending]                            | |
   |            |                                     | |
   |            | (retry basarili) -------------------+ |
   |            |                                       |
   |            | (max retry asildi)                    |
   |            v                                       |
   |       [suspended] --------------------------------+
   |                                                    |
   | (donem doldu / iptal / yeni siparis gerek)         |
   v                                                    v
[completed/cancelled] <-------------------------------+
```

### <mark style="color:red;">Data That Must Be Stored in the Extension</mark>

| Field                 | Type     | Description                                     |
| --------------------- | -------- | ----------------------------------------------- |
| `id`                  | UUID     | Subscription ID                                 |
| `user_id`             | Integer  | Commerce customer ID                            |
| `product_id`          | Integer  | Subscription product ID                         |
| `source_id`           | String   | Stored card reference                           |
| `first_payment_id`    | String   | Initial payment reference                       |
| `status`              | Enum     | `active`, `suspended`, `cancelled`, `completed` |
| `period`              | Enum     | `weekly`, `monthly`, `yearly`                   |
| `next_payment_date`   | DateTime | Next scheduled payment date                     |
| `iteration_count`     | Integer  | Number of completed renewals                    |
| `max_iterations`      | Integer  | Maximum renewals (`null` = unlimited)           |
| `retry_count`         | Integer  | Current failed attempt count                    |
| `max_retries`         | Integer  | Maximum retry attempts                          |
| `shipping_address_id` | Integer  | Shipping address ID                             |
| `billing_address_id`  | Integer  | Billing address ID                              |
| `created_at`          | DateTime | Creation timestamp                              |
| `updated_at`          | DateTime | Last update timestamp                           |

## <mark style="color:red;">Troubleshooting</mark>

### <mark style="color:red;">Common Errors</mark>

#### 1. HTTP 406 — “Can’t process webhook”

**Cause:** IPN is sent too early (Commerce has not finished the previous operation or timing restriction is active).

**Solution:**\
This is not an error, but a wait signal.

* Wait 60 seconds (`sleep`)
* Resend the request
* Retry up to **3–5 times** until successful

***

#### 2. HTTP 406 — “Authentication failed”

**Cause:** Incorrect Basic Auth credentials.

**Checks:**

* Is the `Authorization: Basic <base64>` header present?
* Is the Base64 value encoded in `username:password` format?
* Are `auth.username` and `auth.password` in the POS config correct?

***

#### 3. HTTP 406 — “Order not found” or “Transaction not found”

**Cause:** Invalid `order_id` or checkout not completed yet.

**Checks:**

* Does `order_id` match the `pre_order_number` from the checkout response?
* Was the final checkout step (`RedirectionPaymentSelectedPage`) completed successfully?
* Has the cache expiration (30 minutes) not passed?

***

#### 4. HTTP 406 — “Validation error”

**Cause:** Missing or invalid fields in the IPN body.

**Checks:**

* Are all required fields present? (`order_id`, `amount`, `installment_count`, `interest_fee`, `status`, `substatus`)
* Are values sent as strings? (e.g. `"99.99"` is correct, `99.99` is invalid)

***

#### 5. IPN successful but order not created

**Cause:** Not the first IPN or PreOrder not found in cache.

**Checks:**

* Has a successful IPN already been sent for this `order_id`? (idempotency)
* Has more than 30 minutes passed since checkout?
* Is the PreOrder still available in cache?

### <mark style="color:red;">Debug Tips</mark>

#### 1. Transaction Logs:

```
GET /api/v1/remote/1/transactions/?pos_id={pos_pk}&ordering=-created_date
```

#### 2. Order Status:

```
GET /api/v1/remote/1/orders/?number={order_number}
```

#### 3. IPN Request / Response:

Check the `raw_request` and `raw_response` fields in the Commerce transaction records.

### <mark style="color:red;">Important Notes and Best Practices</mark>

#### 1. Order ID / PreOrder Number

The `order_id` sent in the IPN must match the `pre_order_number` returned during the checkout process. This value is a temporary identifier assigned before the order is created.

***

#### 2. Idempotency

Multiple successful IPNs must not be sent for the same `order_id`. Commerce creates the order on the first successful IPN, and subsequent ones are ignored.

***

#### 3. Timing

After the first successful IPN, it may take a few seconds for Commerce to create the order.

Before checking the order status after sending a successful IPN, wait **5–10 seconds**.

***

#### 4. Test Environment

Before moving to production:

* Test all scenarios in the **staging environment**
  * Successful payment flow
  * Failed payment flow
  * Retry mechanism
  * IPN timeout and retry behavior

***

#### 5. Logging

Ensure all API calls and IPNs are logged:

* Request / response body
* HTTP status codes
* Timestamp
* Correlation ID (for tracing)

***

#### 6. Security

* Store POS authentication credentials securely
* Use HTTPS
* Apply IP whitelisting if possible

***

### <mark style="color:red;">API Reference Documentation</mark>

| Topic                         | URL                                                                     |
| ----------------------------- | ----------------------------------------------------------------------- |
| Basket API                    | <https://apidocs.akinon.com/commerce/basket>                            |
| Checkout - Address & Shipping | <https://apidocs.akinon.com/commerce/checkout/shipping-related>         |
| Checkout - Payment            | <https://apidocs.akinon.com/commerce/checkout/payment-related>          |
| Passwordless Login            | <https://apidocs.akinon.com/commerce/users/password-and-otp-operations> |
| Webhook / IPN                 | <https://apidocs.akinon.com/flows/payment-flows#webhook-ipn-post>       |
| Orders API                    | <https://apidocs.akinon.com/commerce-openapis/orders>                   |
| Transactions API              | <https://apidocs.akinon.com/omnitron-openapis/ordertransaction>         |

### <mark style="color:red;">Checkout.com API References</mark>

| Topic              | URL                                                                                                                                                       |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Payments API       | <https://api-reference.checkout.com/tag/Payments> ([api-reference.checkout.com](https://api-reference.checkout.com/tag/Payments/?utm_source=chatgpt.com)) |
| Recurring Payments | <https://www.checkout.com/docs/payments/add-payment-methods/card-payments/payment-setup-api>                                                              |
| Tokenization       | <https://www.checkout.com/docs/payments/accept-payments/accept-a-payment-using-the-payments-api>                                                          |
| Testing            | <https://www.checkout.com/docs/testing>                                                                                                                   |

### <mark style="color:red;">Real Endpoints (Used in This Project)</mark>

| Endpoint                                | Method | Description                                      |
| --------------------------------------- | ------ | ------------------------------------------------ |
| `/api/v1/remote/1/passwordless-login/`  | POST   | Admin: Generates a one-time token for a customer |
| `/users/passwordless-login-with-token/` | POST   | Login using one-time token                       |
| `/baskets/basket/`                      | POST   | Add/update items in basket                       |
| `/orders/checkout/`                     | POST   | Checkout steps (all pages handled here)          |
| `/orders/hooks/payment`                 | POST   | IPN webhook endpoint                             |
| `/users/orders/`                        | GET    | List customer orders                             |
| `/api/v1/remote/1/transactions/`        | GET    | Transaction list (admin)                         |
| `/api/v1/remote/1/orders/`              | GET    | Order list (admin)                               |

## <mark style="color:red;">Checklist</mark>

#### <mark style="color:red;">Commerce Side (One-time setup)</mark>

* [ ] Extension Gateway POS configuration created
* [ ] PaymentOption configuration created
* [ ] Auth credentials shared with Extension
* [ ] Test environment prepared

#### <mark style="color:red;">Extension Side</mark>

* [ ] Checkout.com API integration implemented
* [ ] Subscription detection after initial order implemented
* [ ] `source_id` and `payment_id` storage implemented
* [ ] Passwordless token generation implemented
* [ ] Automated checkout flow implemented
* [ ] Checkout.com recurring payment charging implemented
* [ ] Commerce IPN sending implemented
* [ ] Retry mechanism implemented
* [ ] Customer notification system implemented
* [ ] Subscription cancellation flow implemented
* [ ] All scenarios tested

***

#### <mark style="color:red;">Test Scenarios</mark>

* [ ] Successful initial payment → Subscription record created
* [ ] Successful recurring payment → Order created
* [ ] Failed payment → Retry mechanism triggered
* [ ] After max retries → Subscription set to `suspended`
* [ ] Cancellation / new order → Old subscription cancelled, new subscription activated

### <mark style="color:red;">Support</mark>

For technical questions:

* **Commerce API:** <api-support@akinon.com>
* **Extension development:** <extension-support@akinon.com>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.akinon.com/technical-guides/akinon-subscription-system.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
