# API Reference

## Introduction

The **Subscriptions API** allows merchants to manage subscription lifecycles — creating customers, memberships, and sessions — through a secure REST interface. Built for seamless integration with Paytiko subscription services.

***

## Prerequisites

Before integrating with the Paytiko Subscriptions API, ensure that the following requirements are met:

* You must have **valid API credentials** (issued by Paytiko).
* All communication must be over **HTTPS only**.
* You must know your `processorKey`, and be able to retrieve and persist the `customerKey` and `membershipKey` via the API.
* Your backend must support **JWT-based authentication** for including the `Authorization: Bearer <token>` header.
* You should implement **secure storage** of credentials, tokens, and keys (never expose secrets to the client/browser).
* You must configure and expose a **publicly accessible webhook endpoint** to receive subscription and payment event notifications from Paytiko.
* You should handle **idempotency** when processing webhook events (e.g., avoid duplicate handling).
* Ensure your system’s **time zone** and **clocks** are synchronized (UTC recommended), since subscription cycles and `endDate` validation are time-sensitive.
* Your customer and product data should meet **validation requirements** (ISO country codes, valid email/phone formats, well-formed URIs).
* You must implement **error handling** for API responses (HTTP 4xx, 5xx) to gracefully manage failed requests.
* For checkout and portal sessions, ensure you can **redirect customers** to the hosted Paytiko pages and handle return URLs.

***

## Best Practices

To build a **robust and reliable integration**, consider the following recommendations:

* **Token Management**:
  * Refresh tokens before they expire (tokens are short-lived).
  * Do not hardcode tokens; request them dynamically when needed.
* **Webhook Reliability**:
  * Treat all webhook requests as **asynchronous events**.
  * Always return **HTTP 200 OK** after successful processing.
  * Implement a **retry strategy**: if your system fails temporarily, Paytiko may resend webhooks — handle duplicates safely.
* **Security**:
  * Verify incoming webhook requests (e.g., by checking signatures or IP allowlists if provided by Paytiko).
  * Use HTTPS everywhere; never log sensitive data such as passwords or full card numbers.
* **Data Integrity**:
  * Validate incoming fields against your business logic (currency, amounts, interval counts).
  * Store `customerKey` and `membershipKey` in your database to link Paytiko records with your own user accounts.
* **Error Handling**:
  * Retry failed API calls using **exponential backoff**.
  * Log both request and response data (excluding sensitive information) for debugging and reconciliation.

***

## Technical Flow

This section describes the **end-to-end lifecycle** of how a subscription is created, purchased, and billed using the Paytiko Subscriptions API. The process covers authentication, customer creation, checkout, payment confirmation, invoicing, and recurring billing.

1. **Customer requests a subscription**\
   The flow begins when a customer chooses a subscription plan on the merchant’s website or application.
2. **Merchant authenticates**\
   The merchant uses its Paytiko-issued API credentials to obtain a secure access token. This token is required for all subsequent API requests.
3. **Merchant creates a customer**\
   Using the customer’s details (name, email, phone, billing info), the merchant calls Paytiko to register a new customer. Paytiko returns a unique `customerKey`, which the merchant stores in its own database.
4. **Merchant creates a membership**\
   The merchant associates the customer with a subscription plan by creating a membership. This defines pricing, billing intervals, and product details. Paytiko returns a `membershipKey`.
5. **Merchant initiates a checkout session**\
   To complete the purchase, the merchant creates a checkout session linked to the `customerKey` and `membershipKey`. Paytiko responds with a `redirectUrl`, and the merchant redirects the customer to the hosted checkout page.
6. **Customer confirms identity and pays**\
   The customer interacts with Paytiko’s secure checkout to confirm their identity and complete payment.
7. **Paytiko notifies the merchant**\
   Upon successful payment (or failure), Paytiko sends a webhook notification to the merchant. At the same time, an invoice or receipt is emailed directly to the customer.
8. **Recurring billing cycles run automatically**\
   On each billing cycle, Paytiko processes the recurring charge. The merchant receives webhook notifications with charge results, while the customer receives invoices by email.

{% @mermaid/diagram content="sequenceDiagram
    autonumber
    participant C as Customer
    participant M as Merchant
    participant P as Paytiko

    %% 1) Customer requests a subscription
    C->>M: Requests a subscription

    %% 2) Authentication
    M->>P: Authenticates
    P-->>M: Returns token

    %% 3) Customer Creation
    M->>P: Creates Customer
    P-->>M: Returns customer key

    %% 4) Membership Creation
    M->>P: Creates Membership
    P-->>M: Returns membership key

    %% 5) Checkout Initiation
    M->>P: Initiates Checkout Session
    P-->>M: Returns redirectUrl
    M-->>C: Redirects to hosted checkout

    %% 6) Customer Checkout
    C->>P: Confirms identity and pays
    P-->>C: Shows payment result

    %% 7) Notifications
    P-->>M: Sends webhook notification
    P-->>C: Sends invoice to customer email

    %% 8) Recurring Billing
    loop Each Billing Cycle
        P->>P: Processes recurring charge
        P-->>M: Sends webhook (success/failure)
        P-->>C: Sends invoice to customer email
    end

" fullWidth="false" %}

***

## Endpoints

This section provides detailed documentation for all available **Paytiko Subscriptions API** endpoints.\
Each endpoint description includes:

* **Method & Path** – The HTTP method (e.g., `POST`, `GET`) and full URI.
* **Overview** – A short description of the purpose of the endpoint.
* **Headers** – Required and optional HTTP headers.
* **Request Body** – JSON model with all fields and validation rules.
* **Response** – Example success and error responses.

All requests must be authenticated using a **JWT bearer token** obtained via the [Authentication endpoint](#authentication).

***

### Try It Yourself — Postman Collection

To get started quickly, you can import our **Postman Collection**, which contains ready-to-use requests for all endpoints:

* Preconfigured with request URLs and headers
* Includes example payloads for authentication, customer creation, membership creation, and checkout
* Helps you test the flow end-to-end before integrating into your system

{% embed url="https://elements.getpostman.com/redirect?entityId=16521666-6e22d852-a3fc-406e-aeae-45249f945d19&entityType=collection" %}

***

### Authentication

<mark style="color:green;">`POST`</mark> `/api/subscriptions/v1/auth`

The authentication endpoint is used to obtain a **JWT bearer token**. This token is required in the `Authorization` header for all other API requests.

**Request Headers**

<table><thead><tr><th>Name</th><th width="229">Value</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td><code>Content-Type</code></td><td><code>application/json</code></td><td>true</td></tr></tbody></table>

**Request Body**

<table><thead><tr><th>Name</th><th>Type</th><th>Description</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td><code>userName</code></td><td>string</td><td>User name</td><td>true</td></tr><tr><td><code>password</code></td><td>string</td><td>Password</td><td>true</td></tr></tbody></table>

**Response**

{% tabs %}
{% tab title="200 [OK]" %}
```json
{
    "token": "<token>",
    "expiresIn": 720 // seconds.
}
```
{% endtab %}

{% tab title="400 [Bad Request]" %}
```json
{
    "errors": {
        "Password": [
            "The Password field is required."
        ]
    },
    "title": "One or more validation errors occurred.",
    "status": 400
}
```
{% endtab %}

{% tab title="401 [Unauthorized]" %}
```json
{
    "title": "Unauthorized",
    "status": 401
}
```
{% endtab %}
{% endtabs %}

***

### Customers

#### Create Customer

<mark style="color:green;">`POST`</mark> `/api/subscriptions/v1/customer`

This endpoint registers a new **customer** in the subscription system. The customer is the entity that owns memberships, billing details, and sessions.&#x20;

**Use case**: When a new end user signs up, you must create their customer profile before adding  memberships.

**Customer uniqueness**\
A customer is uniquely identified by their **email address within the scope of a single merchant**.

{% hint style="warning" %}
### This request is **idempotent**

* If a customer with the same email already exists, the API will **return the existing** `customerKey`.
* If no such customer exists, the API will **create a new record and return the new** `customerKey`.
* **No duplication error** will be raised when re-sending the same request with the same email.
{% endhint %}

**Request Headers**

<table><thead><tr><th>Name</th><th>Value</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td><code>Content-Type</code></td><td><code>application/json</code></td><td>true</td></tr><tr><td><code>Authorization</code></td><td>Bearer &#x3C;token></td><td>true</td></tr></tbody></table>

**Request Body**

<table><thead><tr><th width="164">Name</th><th width="143">Type</th><th width="108" data-type="checkbox">Required</th><th width="343">Description</th></tr></thead><tbody><tr><td><code>processorKey</code></td><td>string (UUID)</td><td>true</td><td><p><strong>Processor identifier.</strong></p><p>Must be a <strong>non-empty GUID</strong> (canonical UUID).</p></td></tr><tr><td><code>firstName</code></td><td>string</td><td>true</td><td><p><strong>Customer first name.</strong></p><p>Must match <code>^\p{L}+(?:[ -]\p{L}+)*$</code> → letters only (Unicode), hyphens and spaces.</p></td></tr><tr><td><code>lastName</code></td><td>string</td><td>true</td><td><p><strong>Customer last name.</strong></p><p>Must match <code>^\p{L}+(?:[ -]\p{L}+)*$</code> → letters only (Unicode), hyphens and spaces.</p></td></tr><tr><td><code>email</code></td><td>string</td><td>true</td><td><p><strong>Customer email.</strong></p><p>Must pass server email validation, standard <code>name@domain.tld</code>.</p></td></tr><tr><td><code>phone</code></td><td>string</td><td>true</td><td><p><strong>Customer phone number.</strong></p><p>Must match the <strong>E.164 international format</strong> (regex pattern is <code>^+[1-9]\d{7,14}$).</code><br></p><ul><li>Must begin with a <strong>“+” sign</strong> followed by the <strong>country code</strong> and <strong>subscriber number</strong></li><li>Cannot start with <code>0</code> after the <code>+</code></li><li>Must be between <strong>8 and 15 digits total</strong> (excluding the <code>+</code>)</li><li>No spaces, hyphens, or special characters allowed.</li></ul></td></tr><tr><td><code>countryCode</code></td><td>string</td><td>true</td><td><p><strong>Country (ISO-3166-1 alpha-2).</strong></p><p>Exactly <strong>2 letters</strong> and must be recognized by the server’s country list; case-insensitive (use uppercase like <code>US</code>, <code>PL</code>).</p></td></tr><tr><td><code>address1</code></td><td>string</td><td>false</td><td><p><strong>Street address line 1.</strong></p><p>Optional; <strong>no additional validation</strong> in current model.</p></td></tr><tr><td><code>address2</code></td><td>string</td><td>false</td><td><p><strong>Apartment/Suite.</strong></p><p>Optional; <strong>no additional validation</strong>.</p></td></tr><tr><td><code>city</code></td><td>string</td><td>false</td><td><p><strong>City/Locality.</strong></p><p>Optional; <strong>no additional validation</strong>.</p></td></tr><tr><td><code>state</code></td><td>string</td><td>false</td><td><p><strong>State/Region/Province.</strong></p><p>Optional; <strong>no additional validation</strong>.</p></td></tr><tr><td><code>zipCode</code></td><td>string</td><td>false</td><td><p><strong>Postal/ZIP code.</strong></p><p>Optional; <strong>no additional validation</strong>.</p></td></tr><tr><td><code>taxIds</code></td><td>array</td><td>false</td><td><p><strong>Business tax identifiers.</strong> Present in cURL examples but <strong>not in the provided C# model/validator</strong>.</p><p></p><p>Each item is typically <code>{ "type": string, "value": string }</code>. No explicit server-side validation rules provided in the shared code.</p></td></tr></tbody></table>

<details>

<summary><strong>Request Body Sample</strong> </summary>

```json
{
  "processorKey": "e05b87aa-b22e-4c57-9e99-b39fc219dd4f",
  "firstName": "Alice",
  "lastName": "Smith",
  "email": "alice.smith@example.com",
  "phone": "+447700900123",
  "countryCode": "GB",
  "address1": "221B Baker Street",
  "address2": "Flat 2",
  "city": "London",
  "state": "London",
  "zipCode": "NW16XE",
  "taxIds": [
    {
      "type": "VAT",
      "value": "GB123456789"
    }
  ]
}
```

</details>

**Response**

{% tabs %}
{% tab title="200 [OK]" %}
```json
{
    "result": {
        "customerKey": "cstmr_cc0067638c2240999c53824a45db0360"
    }
}
```
{% endtab %}

{% tab title="400 [Bad Request]" %}


```json
{
    "errors": {
        "CountryCode": [
            "'Country Code' must not be empty.",
            "Country code must be 2 ISO characters when provided."
        ]
    },
    "title": "One or more validation errors occurred.",
    "status": 400
}
```
{% endtab %}

{% tab title="401 [Unauthorized]" %}
```json
{
    "title": "Unauthorized",
    "status": 401
}
```
{% endtab %}
{% endtabs %}

***

#### Get Customer by Key

<mark style="color:orange;">`GET`</mark> `/api/subscriptions/v1/customer/{customerKey}`

Retrieves full details of a previously created customer.

**Use case**: Fetch customer information to display in your UI or confirm that a customer exists before assigning memberships.

**Headers**

<table><thead><tr><th>Name</th><th>Value</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td><code>Authorization</code></td><td>Bearer &#x3C;token></td><td>true</td></tr></tbody></table>

**Response**

{% tabs %}
{% tab title="200 [OK]" %}
```json
{
    "result": {
        "key": "cstmr_737120202a0241888b307766308cb330",
        "firstName": "Alice",
        "lastName": "Smith",
        "countryCode": "GB",
        "email": "alice.smith@example.com",
        "phone": "+447700900123",
        "address1": "221B Baker Street",
        "address2": "Flat 2",
        "city": "London",
        "state": "London",
        "zipCode": "NW16XE",
        "taxIds": [],
        "createdDate": "2025-08-21T09:35:18.818714+00:00",
        "updatedDate": null
    }
}
```
{% endtab %}

{% tab title="400 [Bad Request]" %}
```json
{
    "errorMessage": "Subscription customer with key = 'cstmr_737120202a0241888b307766308cb331' was not found."
}
```
{% endtab %}

{% tab title="401 [Unauthorized]" %}
```json
{
    "title": "Unauthorized",
    "status": 401
}
```
{% endtab %}
{% endtabs %}

***

### Memberships

#### Create Membership

<mark style="color:green;">`POST`</mark> `/api/subscriptions/v1/membership`

This endpoint registers a new **membership** for a customer.\
A membership defines the subscription relationship, including product details, pricing, and billing cycle.

**Use case**: When a customer purchases a subscription plan, the merchant must create a membership linked to that customer, ensuring the subscription can be billed and managed.

**Membership uniqueness**\
A membership is uniquely identified by its `orderId` (external order identifier) **within the scope of a single customer**.

{% hint style="warning" %}
### This request is **idempotent**

* If a membership with the same `orderId` already exists for the customer, the API will **return the existing** `membershipKey`.
* If no such membership exists, the API will **create a new record and return the new** `membershipKey`.
* **No duplication error** will be raised when re-sending the same request with the same `orderId`.
{% endhint %}

**Request Headers**

<table><thead><tr><th>Name</th><th>Value</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td><code>Content-Type</code></td><td><code>application/json</code></td><td>true</td></tr><tr><td><code>Authorization</code></td><td>Bearer &#x3C;token></td><td>true</td></tr></tbody></table>

**Request Body**

<table><thead><tr><th width="188">Name</th><th width="117">Type</th><th width="109" data-type="checkbox">Required</th><th>Description &#x26; Validation Rules</th></tr></thead><tbody><tr><td><code>customerKey</code></td><td>string</td><td>true</td><td><p><strong>Customer identifier.</strong></p><p>Must start with prefix <strong><code>cstmr_</code></strong>. Used to link the membership to an existing customer.</p></td></tr><tr><td><code>subscriptionName</code></td><td>string</td><td>true</td><td><p><strong>Human-readable plan name.</strong></p><p>Must be <strong>non-empty</strong>. Shown in invoices/portal and useful for your internal reporting.</p></td></tr><tr><td><code>endDate</code></td><td>string</td><td>false</td><td><p><strong>Optional subscription end date.</strong></p><ul><li>Must be in <strong>UTC ISO-8601</strong> format (e.g. <code>YYYY-MM-DDTHH:mm:ss</code>).</li><li>Must be at least <strong>1 hour ahead of the current UTC time</strong>.</li><li>If provided, <strong>no new billing cycles</strong> will start after this moment.</li><li>If not provided, the subscription will be <strong>infinite</strong> and continue to run until it is explicitly <strong>cancelled</strong>.</li></ul><p><strong>Valid examples:</strong></p><ul><li><code>2025-09-01T12:00:00</code> → valid, 1 September 2025 at 12:00 UTC</li><li><code>2025-08-21T19:45:00</code> → valid if current UTC &#x3C; 18:45 UTC (at least +1h ahead)</li></ul></td></tr><tr><td><code>productInfo</code></td><td>object</td><td>true</td><td><p><strong>Product details.</strong></p><p>See <a href="#productinfo">productInfo</a>.</p></td></tr><tr><td><code>pricingInfo</code></td><td>object</td><td>true</td><td><p><strong>Pricing details.</strong></p><p>See <a href="#pricinginfo">pricingInfo</a>.</p></td></tr><tr><td><code>billingCycle</code></td><td>object</td><td>true</td><td><p><strong>Recurrence definition.</strong></p><p>See <a href="#billingcycle">billingCycle</a>.</p></td></tr><tr><td><code>webhookUrl</code></td><td>string</td><td>true</td><td><p><strong>Callback for lifecycle events.</strong></p><p>Must be a <strong>well-formed absolute URL</strong> (HTTPS only).</p><p></p><p>Paytiko posts events like payment success/failure, subscription status changes, etc. </p><p></p><p>Your endpoint <strong>must return 2xx</strong> on success and be <strong>idempotent</strong>.</p></td></tr><tr><td><code>chargeEmitterQueue</code></td><td>string</td><td>false</td><td><p><strong>Optional test/seed events.</strong></p><p>If provided, <strong>cannot be an empty array</strong>. Each item must have <code>transactionStatus</code> <strong><code>Success</code> or a negative enum</strong> value (per server).</p><p></p><p>Intended for <strong>test mode</strong> and QA scenarios.</p><p>See <a href="#chargeemitterqueue">chargeEmitterQueue</a>.</p></td></tr></tbody></table>

#### **productInfo**

<table><thead><tr><th width="159">Name</th><th width="110">Type</th><th width="100" data-type="checkbox">Required</th><th width="374">Description &#x26; Validation Rules</th></tr></thead><tbody><tr><td><code>orderId</code></td><td>string</td><td>true</td><td><p><strong>External order ID</strong> from your system.</p><p>Required to be <strong>unique</strong> to prevent accidental duplication.</p></td></tr><tr><td><code>description</code></td><td>string</td><td>false</td><td><p><strong>Product description.</strong></p><p>Optional, free-text <strong>product/order description</strong> (shown in UIs/invoices if supported).</p></td></tr></tbody></table>

#### **pricingInfo**

<table><thead><tr><th width="151">Name</th><th width="112">Type</th><th width="109" data-type="checkbox">Required</th><th width="367">Description &#x26; Validation Rules</th></tr></thead><tbody><tr><td><code>currency</code></td><td>string</td><td>true</td><td><strong>Currency code.</strong> Must be a <strong>3-letter ISO 4217</strong> code (e.g., <code>USD</code>, <code>EUR</code>).</td></tr><tr><td><code>totalAmount</code></td><td>string</td><td>true</td><td><p><strong>Total subscription amount.</strong> Must be <strong>greater than 0</strong>.</p><p>If <code>pricePerUnit</code> and <code>quantity</code> are provided, then: <code>totalAmount = pricePerUnit × quantity</code>.</p></td></tr><tr><td><code>pricePerUnit</code></td><td>number</td><td>false</td><td><p><strong>Price per unit.</strong></p><p>Must be <strong>greater than 0</strong> if provided. If provided together with <code>quantity</code>, their multiplication <strong>must equal</strong> <code>totalAmount</code>.</p></td></tr><tr><td><code>quantity</code></td><td>integer</td><td>false</td><td><p><strong>Quantity of units.</strong></p><p>Must be <strong>greater than 0</strong> if provided. If provided together with <code>pricePerUnit</code>, their multiplication <strong>must equal</strong> <code>totalAmount</code>.</p></td></tr></tbody></table>

#### **billingCycle**

<table><thead><tr><th width="168">Name</th><th width="117">Type</th><th width="105" data-type="checkbox">Required</th><th width="344">Description &#x26; Validation Rules</th></tr></thead><tbody><tr><td><code>intervalType</code></td><td>string</td><td>true</td><td><p><strong>Billing interval.</strong></p><p>Defines the <strong>base recurrence unit</strong>.</p><p>Must be one of: <code>Daily</code>, <code>Weekly</code>, <code>Monthly</code>, <code>Annually</code>.</p></td></tr><tr><td><code>intervalCount</code></td><td>integer</td><td>false</td><td><p><strong>Interval multiplier.</strong></p><p>Optional. Defines how many <code>intervalType</code> units between charges. Must be an integer <strong>≥1</strong> if provided.<br><strong>If not provided, defaults to <code>1</code>.</strong></p><p><br>Examples:<br><code>intervalType=Weekly</code>,  <code>intervalCount=2</code> → every 2 </p><p></p><p><code>intervalType=Monthly</code>,  <code>intervalCount=3</code> → every 3 months (quarterly)</p></td></tr></tbody></table>

{% hint style="success" %}
#### Monthly Billing Date Edge-Cases are Safely-Handled

When a subscription renews **monthly**, special rules apply if the chosen billing day does not exist in the following month:

* **31st of the month** → In shorter months (Feb, Apr, Jun, Sep, Nov), \
  renewal happens on the **last available day** (e.g., Jan 31 → Feb 28, Apr 30).
* **30th of the month** → If the next month has only 29 or 28 days, it shifts to the **last day**\
  (e.g., May 30 → Jun 30 → Jul 30 → Aug 30 → Sep 30, but Feb only has 28/29).
* Once adjusted, **all future renewals continue from that shifted day** \
  (e.g., Jan 31 → Feb 28 → Mar 28 → Apr 28 …).
* **Leap years are respected** (e.g., Jan 31 → Feb 29 → Mar 29).

**Key takeaway**\
Subscriptions billed on the **30th or 31st** will always renew on the **last available day of shorter months**. Customers will still be billed once per month, with consistent timing.
{% endhint %}

#### chargeEmitterQueue\[]

<table><thead><tr><th width="171">Name</th><th width="110">Type</th><th width="107" data-type="checkbox">Required</th><th width="304">Description &#x26; Validation Rules</th></tr></thead><tbody><tr><td><code>transactionStatus</code></td><td>string</td><td>true</td><td><p><strong>Transaction status filter.</strong></p><p>Must be either:</p><ul><li><code>Success</code> → charge is processed successfully.</li><li><code>Rejected</code> → charge is simulated as failed.</li></ul></td></tr><tr><td><code>description</code></td><td>string</td><td>false</td><td><p><strong>Error message</strong> returned when <code>transactionStatus</code> is <code>Rejected</code>.</p><p></p><p>Defaults to <code>"Insufficient funds."</code> if not provided.</p></td></tr></tbody></table>

When testing subscriptions in **sandbox/test mode**, the payment gateway normally supports only **success-resulting test cards**. That means all recurring charges would succeed by default.

To simulate real-world billing conditions (e.g., declines, retries, eventual success), you can provide a `chargeEmitterQueue` — an array that defines the outcome of each scheduled charge.

#### How it works

* Each item in the array represents the **result of the next charge attempt** in sequence.
* Allowed values are:
  * `success` → charge is processed successfully.
  * `rejected` → charge fails with a simulated decline.
* Once all items in the array are consumed, the emitter stops and **all subsequent charges default to** `success` (unless you provide a longer array).
* This applies **only to recurring charges** triggered by billing cycles.
* The **initial checkout payment is not affected** by `chargeEmitter` (it always follows test card rules).

#### Expanation

* **1st charge** → `success` → The first billing cycle succeeds.
* **2nd charge** → `rejected` → The next billing attempt is rejected.
* **3rd charge** → `rejected` → Another failure is simulated.
* **4th charge** → `rejected` → Third consecutive failure.
* **5th charge** → `success` → Billing finally succeeds again.

#### Rules

* The queue defines **charge behavior only** (not the initial checkout payment).
* If the queue has fewer items than the number of cycles, all remaining charges default to `success`.
* If no `chargeEmitter` is provided, **all charges default to** `success`.

{% hint style="info" %}
#### Key points to remember

* If `chargeEmitterQueue` is **not provided**, all charges are assumed **successful**.
* You can use this feature to **test retry logic, dunning flows, and webhook handling** without needing special test cards.
* Make sure your webhook implementation is idempotent, since rejected charges will trigger failure events you may want to retry.
{% endhint %}

<details>

<summary><strong>Request Body Sample</strong></summary>

```json
{
  "customerKey": "{{customerKey}}",
  "subscriptionName": "Premium Plan",
  "endDate": "2025-08-21T19:45:00Z",
  "productInfo": {
    "orderId": "bff0757d-5aee-406a-b165-461cce42f349",
    "description": "My product"
  },
  "pricingInfo": {
    "totalAmount": 99.98,
    "currency": "USD",
    "pricePerUnit": 49.99,
    "quantity": 2
  },
  "billingCycle": {
    "intervalType": "Daily",
    "intervalCount": 1
  },
  "webhookUrl": "https://webhook.example.com/subscription/callback"
}
```

</details>

**Response**

{% tabs %}
{% tab title="200 [OK]" %}
```json
{
    "result": {
        "membershipKey": "mmbrsp_cc0067638c2240999c53824a45db0360"
    }
}
```
{% endtab %}

{% tab title="400 [Bad Request]" %}


```json
{
    "errors": {
        "SubscriptionName": [
            "Subscription name is required."
        ]
    },
    "title": "One or more validation errors occurred.",
    "status": 400
}
```
{% endtab %}

{% tab title="401 [Unauthorized]" %}
```json
{
    "title": "Unauthorized",
    "status": 401
}
```
{% endtab %}
{% endtabs %}

***

#### Get Membership by Key

<mark style="color:orange;">`GET`</mark> `/api/subscriptions/v1/membership/{membershipKey}`

Retrieves the full details of a previously created **membership**.

**Use cases:**

* Fetch membership information to display in your UI (e.g., plan details, pricing, status).
* Verify that a membership exists and check its current state (active, cancelled, expired) before performing further actions.

**Request Headers**

<table><thead><tr><th>Name</th><th>Value</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td><code>Authorization</code></td><td>Bearer &#x3C;token></td><td>true</td></tr></tbody></table>

**Response**

{% tabs %}
{% tab title="200 [OK]" %}
```json
{
    "result": {
        "key": "mmbrshp_3cf7488e4c4545a998634c664161665a",
        "customerKey": "cstmr_737120202a0241888b307766308cb330",
        "name": "Premium Plan",
        "status": "Incomplete",
        "settings": {
            "intervalType": "Daily",
            "intervalCount": 1
        },
        "invoices": [],
        "createdDate": "2025-08-21T10:08:11.684394+00:00",
        "updatedDate": null,
        "activationDate": null,
        "endDate": "2027-08-21T19:45:00+00:00",
        "nextRenewalDate": null,
        "orderId": "bff0757d-5aee-406a-b165-461cce42f349",
        "productDescription": "My product",
        "totalAmount": 99.98,
        "pricePerUnit": 49.99,
        "quantity": 2,
        "currency": "USD"
    }
}
```
{% endtab %}

{% tab title="400 [Bad Request]" %}
```json
{
    "errorMessage": "Subscription customer with key = 'cstmr_737120202a0241888b307766308cb331' was not found."
}
```
{% endtab %}

{% tab title="401 [Unauthorized]" %}
```json
{
    "title": "Unauthorized",
    "status": 401
}
```
{% endtab %}
{% endtabs %}

***

#### Cancel Membership

<mark style="color:$success;">`POST`</mark> `/api/subscriptions/v1/membership/cancel/{membershipKey}`

Cancels an **active membership** immediately.\
After cancellation:

* No new billing cycles will be started.
* Any **open invoices** (not yet finalized/paid) will be automatically transitioned to status **`Void`**.
* Already paid or failed invoices remain unchanged.

#### Use cases

* Let customers stop their subscription through your UI (e.g., “Cancel Subscription” button).
* Programmatically end a trial or paid membership when business rules require.
* Safely retry cancel calls without risk of duplication or errors.

**Request Headers**

<table><thead><tr><th>Name</th><th>Value</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td><code>Authorization</code></td><td>Bearer &#x3C;token></td><td>true</td></tr></tbody></table>

**Response**

{% tabs %}
{% tab title="200 [OK]" %}
```json
{} // Empty object.
```
{% endtab %}

{% tab title="400 [Bad Request]" %}
```json
{
    "errorMessage": "Membership is already cancelled."
}
```
{% endtab %}

{% tab title="401 [Unauthorized]" %}
```json
{
    "title": "Unauthorized",
    "status": 401
}
```
{% endtab %}
{% endtabs %}

### UI Sessions

#### Create Checkout Session

<mark style="color:green;">`POST`</mark> `/api/subscriptions/v1/checkout/session`

Creates a **hosted checkout session** for a specific customer and membership. It returns a **redirect URL** where the customer completes identity confirmation and payment.

#### How it works

1. Your backend calls this endpoint with a valid `customerKey` and `membershipKey`.
2. The API returns  `checkoutRedirectUrl`.
3. You **redirect** the customer’s browser to that URL.
4. The customer completes checkout on Paytiko’s hosted page.
5. Your system is notified via webhooks configured on the membership (e.g., payment succeeded/failed), and the customer receives an invoice email.

#### Use cases

* Start the **initial payment** for a newly created membership.
* Re-issue a checkout when a previous session **expired**.
* Provide a **pay now** link from your app or email.

**Request Headers**

<table><thead><tr><th>Name</th><th>Value</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td><code>Content-Type</code></td><td><code>application/json</code></td><td>true</td></tr><tr><td><code>Authorization</code></td><td>Bearer &#x3C;token></td><td>true</td></tr></tbody></table>

**Request Body**

<table><thead><tr><th width="184">Name</th><th width="143">Type</th><th width="108" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>customerKey</code></td><td>string</td><td>true</td><td><p><strong>Customer identifier.</strong></p><p>Must start with <code>cstmr_</code>.</p></td></tr><tr><td><code>membershipKey</code></td><td>string</td><td>true</td><td><p><strong>Membership identifier.</strong></p><p>Must start with <code>mmbrshp_</code>.</p></td></tr><tr><td><code>returnToWebsiteLink</code></td><td>string (URL)</td><td>false</td><td><strong>Optional absolute return URL.</strong><br>A full URL where the customer is redirected after leaving the portal.<br>Must be a valid absolute URL (e.g., <code>https://example.com/return</code>).</td></tr></tbody></table>

<details>

<summary><strong>Request Body Sample</strong> </summary>

```json
{
  "customerKey": "{{customerKey}}",
  "membershipKey": "{{membershipKey}}",
  "returnToWebsiteLink": "https://app.example.com/account/billing"
}
```

</details>

**Response**

{% tabs %}
{% tab title="200 [OK]" %}
```json
{
    "result": {
        "checkoutRedirectLink": "{checkoutBaseUrl}/daf8f4f7-5a4d-4b9e-9fc3-959781c226b3"
    }
}
```
{% endtab %}

{% tab title="400 [Bad Request]" %}


```json
{
    "errors": {
        "CustomerKey": [
            "Customer key is required."
        ]
    },
    "title": "One or more validation errors occurred.",
    "status": 400
}
```
{% endtab %}

{% tab title="401 [Unauthorized]" %}
```json
{
    "title": "Unauthorized",
    "status": 401
}
```
{% endtab %}
{% endtabs %}

***

#### Create Portal Session

<mark style="color:green;">`POST`</mark> `/api/subscriptions/v1/checkout/session`

Creates a **customer portal** session URL. The portal is a hosted, self-service page where customers can **view and manage** their subscriptions (e.g., see plan details, invoices, update payment method—exact features depend on your configuration).

#### How it works

1. Your backend calls this endpoint with the customer’s key.
2. The API returns `portalRedirectUrl`.
3. You redirect or link the customer to this URL to self-manage their billing profile.

#### Common use cases

* Provide a “**Manage subscription**” link in your app.
* Let customers **view invoices** or update payment details without building UI yourself.
* Offer a **self-service** experience that reduces support workload.

**Request Headers**

<table><thead><tr><th>Name</th><th>Value</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td><code>Content-Type</code></td><td><code>application/json</code></td><td>true</td></tr><tr><td><code>Authorization</code></td><td>Bearer &#x3C;token></td><td>true</td></tr></tbody></table>

**Request Body**

<table><thead><tr><th width="184">Name</th><th width="143">Type</th><th width="108" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>customerKey</code></td><td>string</td><td>true</td><td><p><strong>Customer identifier.</strong></p><p>Must start with <code>cstmr_</code>.</p></td></tr><tr><td><code>returnToWebsiteLink</code></td><td>string (URL)</td><td>false</td><td><strong>Optional absolute return URL.</strong><br>A full URL where the customer is redirected after leaving the portal.<br>Must be a valid absolute URL (e.g., <code>https://example.com/return</code>).</td></tr></tbody></table>

<details>

<summary><strong>Request Body Sample</strong> </summary>

```json
{
  "customerKey": "{{customerKey}}",
  "returnToWebsiteLink": "https://app.example.com/account/billing"
}
```

</details>

**Response**

{% tabs %}
{% tab title="200 [OK]" %}
```json
{
    "result": {
        "portalRedirectLink": "{portalBaseUrl}/f1092c5c-b384-4058-aedb-6b9350fc58a7"
    }
}
```
{% endtab %}

{% tab title="400 [Bad Request]" %}


```json
{
    "errors": {
        "CustomerKey": [
            "Customer key is required."
        ]
    },
    "title": "One or more validation errors occurred.",
    "status": 400
}
```
{% endtab %}

{% tab title="401 [Unauthorized]" %}
```json
{
    "title": "Unauthorized",
    "status": 401
}
```
{% endtab %}
{% endtabs %}

***

## Webhooks

### Overview

Paytiko sends webhooks to notify your system about important subscription events.\
There are two categories of webhook events:

1. **Transaction status updates** — covers payments, refunds, charge attempts, etc.
   *   In the webhook payload, the `Action` field will be set to:

       ```
       "TRANSACTION_DATA_UPDATE"
       ```
   * Full reference for transaction webhooks can be found [**here**](../../web-sdk#webhook-control).
2. **Membership status updates** — covers subscription lifecycle changes (e.g., activation, past due, cancellation).
   *   In the webhook payload, the `Action` field will be set to:

       ```
       "SUBSCRIPTION_STATUS_CHANGE"
       ```
   * Detailed documentation for membership webhooks is provided **below in this section**.

***

### Membership Status Update Webhook

**What is it?**\
A webhook sent whenever a membership (subscription) **changes status**.\
The payload includes the updated membership object and recent invoices for context.

**How it works**

* Your `webhookUrl` (set during **Create Membership**) receives an HTTP POST with JSON.
* The `Status` field in the payload reflects the **new, updated** subscription status.
* You should update your local records and trigger any downstream logic (access enable/disable, emails, etc.).

**Possible membership statuses**

* `Active`
* `PastDue`
* `Unpaid`
* `IncompleteExpired`
* `Canceled`
* `Completed`

> Only the **status** changes between events; the rest of the schema remains the same.

### Delivery & Retries

Paytiko attempts to deliver each webhook event by sending an HTTP `POST` request to the merchant’s configured `webhookUrl`.

* If your endpoint responds with an HTTP **2xx** status code, the delivery is considered successful.
* If your endpoint responds with a **non-2xx status** (e.g., 4xx, 5xx) or times out, the system will retry delivery.

**Retry policy**

* Each webhook delivery has **1 initial attempt plus up to 6 retry attempts** (7 attempts total).
*   A **custom backoff schedule** is applied:

    * The initial attempt is sent immediately.
    * The first retry is delayed by **5 minutes**.
    * The second retry is delayed by **30 minutes**.
    * Each subsequent retry doubles the delay (**1 hour, 2 hours, 4 hours, 8 hours**).

    Attempt #1 — Initial — Immediate\
    Attempt #2 — Retry #1 — 5 minutes\
    Attempt #3 — Retry #2 — 30 minutes\
    Attempt #4 — Retry #3 — 1 hour\
    Attempt #5 — Retry #4 — 2 hours\
    Attempt #6 — Retry #5 — 4 hours\
    Attempt #7 — Retry #6 — 8 hours
* If all retry attempts fail, the event is marked as **undeliverable** and no further retries will be attempted.
* All retry scheduling is based on **UTC time**, ensuring consistent behavior across time zones and daylight saving changes.

**Best practices**

* Always return a **2xx** response as quickly as possible — queue the work internally if needed.
* Log incoming `ActionId` and `Membership.Key` values so you can **reconcile missed events** if a retry sequence is exhausted.
* Keep your webhook endpoint highly available and resilient to ensure timely state synchronization.

<details>

<summary>Webhook  Payload Structure Sample</summary>

```json
{
  "Action": "SUBSCRIPTION_STATUS_CHANGE",
  "ActionId": "516e1e6f-ecca-4f45-a4d6-abaa14245668",
  "OrderId": "6a954f17-0eae-475c-9f51-8fc215abb512",
  "Membership": {
    "Id": 748,
    "Key": "mmbrshp_e68995caf18a4147ac730244d365d104",
    "CustomerKey": "cstmr_25ab3cc46c7a49c388e6a0bda78d669e",
    "Name": "Premium Plan",
    "Status": "Completed",
    "OrderId": "6a954f17-0eae-475c-9f51-8fc215abb512",
    "AccountId": 10155924,
    "MerchantId": 4,
    "CreatedDate": "2025-08-15T15:55:40.505067+00:00",
    "UpdatedDate": "2025-08-15T18:00:13.74996+00:00",
    "ActivationDate": "2025-08-15T15:56:17.35422+00:00",
    "ProductDescription": "Email Trigger Events and Displaying nd Date",
    "TotalAmount": 34,
    "Currency": "USD",
    "Quantity": 1,
    "PricePerUnit": 34,
    "InvoiceVector": [
      {
        "Id": 1186,
        "Key": "invc_8018d3df77f94e33bfd65950cda1edf3",
        "ReceiptKey": "rcpt_0102d50765244c13acdb89b7b8f352fc",
        "Amount": 34,
        "Currency": "USD",
        "Status": "Paid",
        "CreatedDate": "2025-08-15T15:56:08.825856+00:00",
        "UpdatedDate": "2025-08-15T15:56:17.331371+00:00",
        "Period": {
          "StartDate": "2025-08-15T15:56:08.825833+00:00",
          "EndDate": "2025-08-15T16:56:08.825833+00:00"
        },
        "Attempts": {
          "LastAttemptDate": null,
          "AttemptedRetryCount": 0
        },
        "PaymentMethod": {
          "PaymentMethodId": 252,
          "PaymentMethodMaskedPan": "411111******1111"
        }
      },
      {
        "Id": 1187,
        "Key": "invc_13a2a15000224a289905824a3029e1b2",
        "ReceiptKey": "rcpt_dcd689547af441a683f0fb98cc9d4088",
        "Amount": 34,
        "Currency": "USD",
        "Status": "Paid",
        "CreatedDate": "2025-08-15T17:00:26.566213+00:00",
        "UpdatedDate": "2025-08-15T17:00:32.562065+00:00",
        "Period": {
          "StartDate": "2025-08-15T16:56:17.35422+00:00",
          "EndDate": "2025-08-15T17:56:17.35422+00:00"
        },
        "Attempts": {
          "LastAttemptDate": "2025-08-15T17:00:26.558356+00:00",
          "AttemptedRetryCount": 0
        },
        "PaymentMethod": {
          "PaymentMethodId": 252,
          "PaymentMethodMaskedPan": "411111******1111"
        }
      }
    ]
  },
  "IssuedAt": "2025-08-15T18:00:13.7940023+00:00",
  "Signature": "f3d1d474f20f71e9aeea0dbfe2a3b6284f48f06a63ed40d83936eea11995e71b"
}
```

</details>

{% hint style="info" %}
#### Payload Handling Recommendations

* **Respond with 2xx quickly.** Process heavy work asynchronously.
* **Idempotency:** use `ActionId` (or your own event ID map) to ignore duplicates.
* **Security:** verify authenticity (e.g., shared secret/signature) if your integration provides one.
* **Audit:** log `Action`, `ActionId`, `Membership.Key`, `Membership.Status`, and `IssuedAt`.
* **State sync:** treat webhook as **source of truth** for membership state changes and update your DB accordingly.
{% endhint %}

***

### Signature Verification

For security, every webhook request is signed so that your system can verify it originates from Paytiko and has not been tampered with.

**How it’s generated**

The signature is generated using **SHA-256 hashing** of the merchant’s secret key together with the membership’s `OrderId`.

Formula:

```
Signature = SHA256("{SecretKey}:{OrderId}")
```

* `SecretKey` — your merchant’s private secret key (never exposed publicly).
* `OrderId` — the order identifier associated with the membership or transaction.

**Example**

If your merchant secret key is:

```
sk_test_123456
```

and the `OrderId` in the webhook payload is:

```
6a954f17-0eae-475c-9f51-8fc215abb512
```

then the string to be hashed is:

```
sk_test_123456:6a954f17-0eae-475c-9f51-8fc215abb512
```

The resulting SHA-256 hash will be the signature value you should compare against the one provided in the webhook headers.

**Validation Steps**

1. Extract the `OrderId` from the webhook payload.
2. Concatenate your **SecretKey** and `OrderId` with a colon (`:`).
3. Generate the SHA-256 hash of this string.
4. Compare the result with the signature sent in the webhook header.
   * If they match → the webhook is authentic.
   * If not → reject the webhook.

### IP Whitelisting

In addition to signature validation, you must also **whitelist Paytiko’s official webhook source IP addresses** in your firewall or API gateway.

**Why this is required**

* Webhooks are sent over the public internet, and malicious actors could attempt to mimic Paytiko’s payload format.
* Even if a payload looks correct, if it does not come from Paytiko’s trusted infrastructure, it must be rejected.
* IP whitelisting ensures your server accepts webhook traffic **only** from Paytiko’s infrastructure.

**How it works**

* Paytiko will provide a list of static outbound IP addresses.
* Your system must configure these IPs as **allowed sources** for webhook requests.
* Requests from any other IP must be blocked before signature validation is even attempted.

## FAQ

<details>

<summary>❓ How are customers identified?</summary>

Customers are uniquely identified **by email** within the scope of a single merchant.\
Creating a customer with the same email is **idempotent** — the API will return the existing `customerKey` instead of creating a duplicate.

</details>

<details>

<summary>❓ How are memberships identified?</summary>

Memberships are uniquely identified **by `orderId`**.\
Creating a membership with the same `orderId` is also **idempotent** — the API will return the existing membership instead of creating a duplicate.

</details>

<details>

<summary>❓What happens if I don’t provide <code>endDate</code> for a membership?</summary>

If `endDate` is omitted, the membership will be **infinite** and continue until it is explicitly **cancelled**.\
If provided, it must be at least **1 hour ahead** of current UTC time.

</details>

<details>

<summary>❓ How is pricing validated?</summary>

* `totalAmount` must equal **`pricePerUnit × quantity`** (when both are provided).
* All amounts must be **greater than 0**.
* Currency must be a valid **3-letter ISO code** (e.g., `USD`, `EUR`).

</details>

<details>

<summary>❓ How does billing cycle calculation work?</summary>

* `intervalType` defines the base recurrence unit (`Daily`, `Weekly`, `Monthly`, `Annually`).
* `intervalCount` multiplies the base interval (default = `1` if omitted).
  * Example: `intervalType=Weekly`, `intervalCount=2` → every 2 weeks.
* Edge cases: For **end-of-month memberships**, if the current month has fewer days, the system will always bill on the **last available day of the month**.

</details>

<details>

<summary>❓ Can memberships be cancelled at the end of the billing cycle instead of immediately?</summary>

No. The `Cancel Membership` endpoint always cancels **immediately**.\
If you want “cancel at end of period” behavior, you must track that in your system and call the API when the period ends.

</details>

<details>

<summary>❓ What happens to invoices when a membership is cancelled?</summary>

* **Open invoices** → automatically marked as `Void`.
* **Paid invoices** → remain unchanged.
* **Failed invoices** → remain unchanged and will not be retried.

</details>

<details>

<summary>❓ How does webhook delivery work?</summary>

* By `webhookUrl` provided, the system will send **event notifications** (e.g., payment success/failure).
* The URL must be a valid **absolute HTTPS URL**.
* Merchants should implement **idempotency** in their webhook handlers, as the same event may be retried if delivery fails.

</details>

<details>

<summary>❓ Is the API idempotent?</summary>

Only the following API endpoints are idempotent:

* Creating customers by `email` is idempotent.
* Creating memberships by `orderId` is idempotent.

</details>

<details>

<summary>❓ What format is required for phone numbers?</summary>

* Example: `+48123456789`
* Must contain **8–15 digits**, no spaces or formatting characters.

</details>

<details>

<summary>❓ What test data should I use?</summary>

* Use **fake but valid-looking** data (emails, phone numbers, addresses).
* For phone, always use a **non-existent but valid E.164 number** (e.g., `+48111222333`).
* For webhook URLs, use test endpoints such as `https://webhook.site/...`.

</details>
