> For the complete documentation index, see [llms.txt](https://docs.doku.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.doku.com/subscription-and-billing/flexibill/subscription-and-billing/host-to-host-integration.md).

# Host to Host Integration

Integrate FlexiBill directly into your business application or website using the API. Use this approach to programmatically create bills, register subscriptions, and manage billing without going through the dashboard.

## Features & Benefits

**🔗 Full Programmatic Control**

Create bills and subscriptions directly from your application backend — ideal for customer onboarding flows that are fully integrated into your system.

**📤 Multi-Channel Delivery via API**

Deliver invoices to customers via Email and/or WhatsApp in a single API call, without any additional configuration on the dashboard.

**🔁 Recurring and One-Time Support**

The API supports both one-time bill creation and recurring billing with flexible cycle configuration.

## How It Works

| **Step 1**                      | **Step 2**                                      | **Step 3**                                                                                                        |
| ------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Register and activate FlexiBill | Integrate the API into your application backend | Call the endpoint to create a bill or subscription — FlexiBill delivers the invoice to the customer automatically |

## API Reference

{% hint style="warning" %}
All API requests must include valid authentication headers. See the [Authentication](#authentication) tab below.
{% endhint %}

{% tabs %}
{% tab title="Authentication" %}
All API requests require the following headers:

<table><thead><tr><th width="187.18359375">Header</th><th>Description</th></tr></thead><tbody><tr><td><code>Client-Id</code></td><td>Your merchant Client ID from the DOKU Dashboard</td></tr><tr><td><code>Request-Id</code></td><td>A unique UUID per request</td></tr><tr><td><code>Request-Timestamp</code></td><td>UTC timestamp in ISO 8601 format</td></tr><tr><td><code>Signature</code></td><td>HMAC-SHA256 signature for request authentication</td></tr></tbody></table>

**Example headers:**

```
Client-Id: BRN-0228-1748939308074
Request-Id: 69e22f33-e7f3-49b9-9302-f0df1f6059b9
Request-Timestamp: 2025-06-16T03:32:26Z
Signature: HMACSHA256=WNdiEPJEZAW1vLthXygOkzMuCEadiEVK0saLjtFRqqs=
```

👉 Learn more about [how to generate signature](https://developers.doku.com/get-started-with-doku-api/signature-component/non-snap/signature-component-from-request-header)
{% endtab %}

{% tab title="Generate Bill" %}
Create a new bill (one-time or recurring) that will be issued and delivered to a customer.

**Endpoint:**

<table><thead><tr><th width="115.90234375"></th><th>Sandbox</th><th>Production</th></tr></thead><tbody><tr><td><strong>Method</strong></td><td><code>POST</code></td><td><code>POST</code></td></tr><tr><td><strong>URL</strong></td><td><code>https://api-sandbox.doku.com/bill-collection-core/v1/generate/invoice</code></td><td><code>https://api.doku.com/bill-collection-core/v1/generate/invoice</code></td></tr></tbody></table>

**Request body:**

```json
{
  "bill_identifier": "00001111122112",
  "bill_title": "Gym Monthly Fee",
  "bill_description": "Gym Fee April",
  "bill_type": "ONE_TIME_BILL",
  "amount": 10000,
  "currency": "IDR",
  "beneficiary_id": "TRIAL-00003",
  "beneficiary_name": "John Smith",
  "beneficiary_phone": "6287880777777",
  "beneficiary_email": "john.smith@email.com",
  "distribution_channel": ["EMAIL", "WHATSAPP"],
  "invoice_term": "7 days",
  "grace_period": "7 days",
  "template_id_invoice": "TMP-240318-58821",
  "template_invoice_param": [
    "beneficiary_name",
    "bill_identifier",
    "bill_service_title",
    "amount",
    "payment_link"
  ],
  "template_id_receipt": "TMP-RECEIPT-001",
  "template_receipt_param": [
    "beneficiary_name",
    "bill_identifier",
    "bill_service_title",
    "receipt_link"
  ]
}
```

For a **recurring bill**, add the `schedule` object:

```json
{
  "bill_type": "RECURRING",
  "schedule": {
    "bill_cycle": {
      "type": "INTERVAL",
      "interval": "1m"
    },
    "start_date": "2025-11-12T15:19:00Z",
    "end_date": {
      "type": "AFTER_X_TIMES",
      "count": 12
    }
  }
}
```

**Request parameters:**

<table><thead><tr><th>Parameter</th><th width="110.66015625">Required</th><th width="95.3359375">Max Length</th><th width="96.54296875">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>bill_identifier</code></td><td>✅</td><td>20</td><td>String</td><td>Unique bill reference ID. No spaces allowed.</td></tr><tr><td><code>bill_title</code></td><td>✅</td><td>256</td><td>String</td><td>Name of the billed service.</td></tr><tr><td><code>bill_description</code></td><td>Optional</td><td>256</td><td>String</td><td>Additional service description.</td></tr><tr><td><code>bill_type</code></td><td>✅</td><td>—</td><td>Enum</td><td><code>ONE_TIME_BILL</code> or <code>RECURRING</code></td></tr><tr><td><code>amount</code></td><td>✅</td><td>12</td><td>Double</td><td>Bill amount.</td></tr><tr><td><code>currency</code></td><td>✅</td><td>3</td><td>String</td><td>Currency code (e.g., <code>IDR</code>,<code>MYR</code>).</td></tr><tr><td><code>beneficiary_id</code></td><td>✅</td><td>20</td><td>String</td><td>Unique customer reference ID.</td></tr><tr><td><code>beneficiary_name</code></td><td>✅</td><td>128</td><td>String</td><td>Customer full name.</td></tr><tr><td><code>beneficiary_phone</code></td><td>✅</td><td>128</td><td>Numeric String</td><td>Customer phone with country code. Indonesia: <code>62</code>, Malaysia: <code>60</code>.</td></tr><tr><td><code>beneficiary_email</code></td><td>✅</td><td>256</td><td>String</td><td>Customer email address.</td></tr><tr><td><code>distribution_channel</code></td><td>✅</td><td>—</td><td>Array</td><td>One or both: <code>EMAIL</code>, <code>WHATSAPP</code>. Determines how the invoice and receipt are delivered to the customer.</td></tr><tr><td><code>invoice_term</code></td><td>Optional</td><td>128</td><td>String</td><td>Payment deadline window. Examples: <code>due upon receipt</code>, <code>7 days</code>, <code>end of month</code>. Max 90 days.</td></tr><tr><td><code>grace_period</code></td><td>Optional</td><td>128</td><td>String</td><td>Buffer after due date. Examples: <code>no grace period</code>, <code>7 days</code>. Max 90 days.</td></tr><tr><td><code>template_id_invoice</code></td><td>If <code>WHATSAPP</code></td><td>—</td><td>String</td><td>WhatsApp template ID for invoice delivery. Required when <code>distribution_channel</code> includes <code>WHATSAPP</code>.</td></tr><tr><td><code>template_invoice_param</code></td><td>If <code>WHATSAPP</code></td><td>—</td><td>Array</td><td>Ordered list of data fields mapped to the WhatsApp invoice template variables. Must match the variable order defined in the template. Required when <code>template_id_invoice</code> is provided.</td></tr><tr><td><code>template_id_receipt</code></td><td>If <code>WHATSAPP</code></td><td>—</td><td>String</td><td>WhatsApp template ID for receipt delivery. Required when <code>distribution_channel</code> includes <code>WHATSAPP</code>. </td></tr><tr><td><code>template_receipt_param</code></td><td>If <code>WHATSAPP</code></td><td>—</td><td>Array</td><td>Ordered list of data fields mapped to the WhatsApp receipt template variables. Must match the variable order defined in the template. Required when <code>template_id_receipt</code> is provided.</td></tr></tbody></table>

{% hint style="info" %}
**WhatsApp template parameters** — `template_id_invoice`, `template_invoice_param`, `template_id_receipt`, and `template_receipt_param` are only required when `distribution_channel` includes `WHATSAPP`. If you are sending via `EMAIL` only, omit these four fields entirely. WhatsApp templates must be pre-approved and registered in the DOKU Dashboard before use via PAYCHAT
{% endhint %}

**Schedule parameters (Recurring only):**

<table><thead><tr><th>Parameter</th><th width="150.97265625">Required</th><th width="92.8984375">Type</th><th>Format</th><th width="244.06640625">Description</th></tr></thead><tbody><tr><td><code>schedule.bill_cycle.type</code></td><td>✅</td><td>Enum</td><td>—</td><td><code>INTERVAL</code> — bills are issued at a fixed time interval. <code>SPECIFIC_DATE</code> — bills are issued on a set day each cycle.</td></tr><tr><td><code>schedule.bill_cycle.interval</code></td><td>If INTERVAL</td><td>String</td><td><code>{number}{unit}</code></td><td>Billing interval. Unit: <code>d</code> (day), <code>m</code> (month), <code>y</code> (year). Examples: <code>7d</code>, <code>1m</code>, <code>1y</code>. Range: 1–999.</td></tr><tr><td><code>schedule.start_date</code></td><td>✅</td><td>String</td><td>ISO 8601 UTC</td><td>First bill issuance date. Must be ≥ request timestamp.</td></tr><tr><td><code>schedule.end_date.type</code></td><td>✅</td><td>Enum</td><td>—</td><td><code>AFTER_X_TIMES</code> — stops after N cycles. <code>SPECIFIC_DATE</code> — stops on a calendar date. <code>NEVER</code> — runs until manually canceled.</td></tr><tr><td><code>schedule.end_date.count</code></td><td>If AFTER_X_TIMES</td><td>Integer</td><td>—</td><td>Number of billing cycles before the recurring bill stops. Must be > 0.</td></tr><tr><td><code>schedule.end_date.specific_date</code></td><td>If SPECIFIC_DATE</td><td>String</td><td>ISO 8601 UTC</td><td>The date the recurring bill ends. Must be ≥ <code>start_date</code>.</td></tr></tbody></table>

**Response (HTTP 200):**

```json
{
  "code": "SUCCESS",
  "message": "success",
  "data": {
    "bill_identifier": "00001111122112",
    "bill_title": "Gym Monthly Fee",
    "amount": 10000,
    "currency": "IDR",
    "beneficiary_id": "TRIAL-00003",
    "beneficiary_name": "John Smith",
    "beneficiary_phone": "6287880777777",
    "beneficiary_email": "john.smith@email.com",
    "state": "INVOICE_SENT",
    "status": "UNPAID",
    "invoice_number": "INV-00001111122112-hwb52"
  }
}
```

**Response parameters:**

<table><thead><tr><th width="277.83203125">Parameter</th><th>Description</th></tr></thead><tbody><tr><td><code>data.bill_identifier</code></td><td>Unique bill reference ID echoed from the request</td></tr><tr><td><code>data.bill_title</code></td><td>Name of the billed service</td></tr><tr><td><code>data.amount</code></td><td>Bill amount</td></tr><tr><td><code>data.currency</code></td><td>Currency code</td></tr><tr><td><code>data.beneficiary_id</code></td><td>Unique customer reference ID</td></tr><tr><td><code>data.beneficiary_name</code></td><td>Customer name</td></tr><tr><td><code>data.beneficiary_phone</code></td><td>Customer phone with country code</td></tr><tr><td><code>data.beneficiary_email</code></td><td>Customer email</td></tr><tr><td><code>data.state</code></td><td>Current invoice delivery state. See invoice delivery states below.</td></tr><tr><td><code>data.status</code></td><td>Payment status of the invoice. <code>UNPAID</code> on creation.</td></tr><tr><td><code>data.invoice_number</code></td><td>System-generated invoice number for this bill</td></tr></tbody></table>

**Invoice delivery states (`data.state`):**

<table><thead><tr><th width="185.65625">State</th><th>Description</th></tr></thead><tbody><tr><td><code>INVOICE_IN_PROCESS</code></td><td>The system is generating the invoice.</td></tr><tr><td><code>INVOICE_CREATED</code></td><td>Invoice has been created and is queued for delivery.</td></tr><tr><td><code>INVOICE_SENT</code></td><td>Invoice has been successfully delivered to the customer.</td></tr><tr><td><code>INVOICE_ERROR</code></td><td>Delivery failed — typically due to insufficient deposit balance. Top up your deposit; the system will automatically retry.</td></tr></tbody></table>

***

#### Response Scenarios

{% tabs %}
{% tab title="✅ Success" %}
**HTTP 200 — Bill generated and invoice delivered.**

```json
{
  "code": "SUCCESS",
  "message": "success",
  "data": {
    "bill_identifier": "00001111122112",
    "bill_title": "Gym Monthly Fee",
    "amount": 10000,
    "currency": "IDR",
    "beneficiary_id": "TRIAL-00003",
    "beneficiary_name": "John Smith",
    "beneficiary_phone": "6287880777777",
    "beneficiary_email": "john.smith@email.com",
    "state": "INVOICE_SENT",
    "status": "UNPAID",
    "invoice_number": "INV-00001111122112-hwb52"
  }
}
```

**What happens next:**

* The invoice is delivered to the customer via the channels specified in `distribution_channel`
* The bill is visible in the DOKU Dashboard under Subscription and Billing → Bill
* Monitor payment status via Webhook or the dashboard
  {% endtab %}

{% tab title="❌ Invalid Distribution Channel" %}
**HTTP 400 — Unsupported or missing `distribution_channel`.**

The `distribution_channel` array is empty, contains an invalid value, or `WHATSAPP` is specified but WhatsApp has not been activated for the merchant account.

```json
{
    "error": {
        "code": "invalid_parameter",
        "type": "invalid_request_error",
        "message": "Invalid distribution channel"
    }
}
```

**Resolution:** Use `["EMAIL"]` if WhatsApp has not been activated. To enable WhatsApp delivery, activate DOKU PayChat from the DOKU Dashboard.
{% endtab %}

{% tab title="❌ Missing Template Whatsapp" %}
**HTTP 400 —  Missing `template`.**

The `template` array is empty, contains an invalid value when `WHATSAPP` is specified as distribution channel.

```json
{
    "error": {
        "code": "invalid_parameter",
        "type": "invalid_request_error",
        "message": "Missing necessary parameter template id"
    }
}
```

**Resolution:** Use `["EMAIL"]` if WhatsApp template has not been approved. Or check template has been approved in Paychat dashboard.
{% endtab %}

{% tab title="❌ Invalid Recurring Schedule" %}
**HTTP 400 — Recurring bill schedule is invalid.**

The `start_date` is in the past, or the `end_date` configuration is inconsistent. Applies to `bill_type: RECURRING` only.

```json
{
    "error": {
        "code": "invalid_parameter",
        "type": "invalid_request_error",
        "message": "Invalid schedule.end_date.specific_date"
    }
}
```

```json
{
    "error": {
        "code": "invalid_parameter",
        "type": "invalid_request_error",
        "message": "schedule.billCycle.type is invalid"
    }
}
```

**Common causes and resolution:**

| Cause                                                             | Resolution                                                                                                       |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `start_date` is in the past                                       | Set `start_date` to the current datetime or a future datetime                                                    |
| `end_date.specific_date` is before `start_date`                   | Ensure `specific_date` is ≥ `start_date`                                                                         |
| `bill_cycle.interval` format is incorrect                         | Use `{number}{unit}` — e.g., `1m`, `7d`, `1y`. Supported units: `d` (day), `m` (month), `y` (year). Range: 1–999 |
| `end_date.type` is `SPECIFIC_DATE` but `specific_date` is missing | Include `specific_date` when type is `SPECIFIC_DATE`                                                             |
| {% endtab %}                                                      |                                                                                                                  |

{% tab title="❌ Authentication Error" %}
**HTTP 401 — Request authentication failed.**

```json
{
    "error": {
        "code": "api_key_expired",
        "type": "authentication_error",
        "message": "Permission is not authorized"
    }
}
```

**Resolution:** Verify all required headers are present — `Client-Id`, `Request-Id`, `Request-Timestamp`, and `Signature`. The `Request-Timestamp` must be within an acceptable clock skew of the server time. → See the [Authentication](#authentication) tab.
{% endtab %}
{% endtabs %}
{% endtab %}

{% tab title="Create Subscription" %}
Register a customer subscription with the specified plan, schedule, and customer profile.

**Endpoint:**

<table><thead><tr><th width="106.9453125"></th><th>Sandbox</th><th>Production</th></tr></thead><tbody><tr><td><strong>Method</strong></td><td><code>POST</code></td><td><code>POST</code></td></tr><tr><td><strong>URL</strong></td><td><code>https://api-sandbox.doku.com/subscription-core/v1/subscription</code></td><td><code>https://api.doku.com/subscription-core/v1/subscription</code></td></tr></tbody></table>

***

#### ⚠️ Prerequisites

Before executing this API request, the following entities must be pre-registered via the DOKU Dashboard back-office. Passing unregistered codes will result in a validation error.

| Entity         | Dashboard Path                                     | Passed in API as  | Required           |
| -------------- | -------------------------------------------------- | ----------------- | ------------------ |
| **Collection** | Subscription and Billing → Collection → Collection | `collection_code` | ✅ Always           |
| **Plan**       | Subscription and Billing → Collection → Plan       | `plans[].code`    | ✅ Always           |
| **Coupon**     | Subscription and Billing → Collection → Coupon     | `coupons[].code`  | If applying coupon |
| **Tax**        | Subscription and Billing → Collection → Tax        | `taxes[].code`    | If applying tax    |

{% hint style="warning" %}
The `collection_code`, `plans[].code`, `taxes[].code`, and `coupons[].code` values must exactly match the codes registered in the dashboard. These codes are **case-sensitive**. There is no API endpoint to create collections, plans, taxes, or coupons — they must be configured in the dashboard first.
{% endhint %}

***

#### Request Body

```json
{
  "subscription_code": "SUBSCRIPTIONS25000001",
  "auto_generate_id": true,
  "plan_type": "SINGLE_PLAN",
  "collection_code": "COLLECTIONS25000001",
  "plans": [
    {
      "code": "PLANS25000001",
      "qty": 1,
      "amount": 10000.00
    }
  ],
  "coupons": [
    {
      "code": "COUPON25000001"
    }
  ],
  "taxes": [
    {
      "code": "TAXES25000001"
    }
  ],
  "currency": "IDR",
  "total_amount": 11000.00,
  "plan_schedule": {
    "start_date": "2025-09-02T00:00:00Z",
    "end_date": {
      "type": "AFTER_X_TIMES",
      "count": 12
    }
  },
  "customers": [
    {
      "ext_id":"STUDENT001013",
      "email": "john.smith@email.com",
      "name": "John Smith",
      "phone_number": "088812341234",
      "calling_code": "62"
    }
  ]
}
```

***

#### Request Parameters

**Top-level fields:**

<table><thead><tr><th>Parameter</th><th width="107.55859375">Required</th><th width="95.8671875">Max Length</th><th width="87.67578125">Type</th><th width="233.60546875">Description</th></tr></thead><tbody><tr><td><code>subscription_code</code></td><td>Optional</td><td>20</td><td>String</td><td>Unique subscription ID. Must be unique across the merchant account. Can be omitted if <code>auto_generate_id</code> is <code>true</code>. No spaces allowed.</td></tr><tr><td><code>auto_generate_id</code></td><td>Optional</td><td>—</td><td>Boolean</td><td>If <code>true</code>, the system auto-generates a unique subscription code. Ignored if <code>subscription_code</code> is provided.</td></tr><tr><td><code>plan_type</code></td><td>✅</td><td>—</td><td>Enum</td><td><code>SINGLE_PLAN</code> or <code>BUNDLING_PLAN</code></td></tr><tr><td><code>collection_code</code></td><td>✅</td><td>50</td><td>String</td><td>Code of the pre-registered collection. Must exist in the dashboard.</td></tr><tr><td><code>plans</code></td><td>✅</td><td>—</td><td>Array</td><td>At least 1 plan object required. Maximum 10 plans. All plans must belong to the same collection and share the same billing cycle.</td></tr><tr><td><code>plans[].code</code></td><td>✅</td><td>20</td><td>String</td><td>Code of the pre-registered plan. Must exist in the dashboard under the specified collection.</td></tr><tr><td><code>plans[].qty</code></td><td>✅</td><td>6</td><td>Integer</td><td>Quantity. Must be > 0. Relevant for Per Unit pricing plans.</td></tr><tr><td><code>plans[].amount</code></td><td>If open-amount plan</td><td>—</td><td>Decimal</td><td>Required only for Open Amount pricing plans. Ignored for all other pricing models.</td></tr><tr><td><code>coupons</code></td><td>Optional</td><td>—</td><td>Array</td><td>Discount coupons to apply. Pass the pre-registered coupon code.</td></tr><tr><td><code>coupons[].code</code></td><td>If coupons present</td><td>20</td><td>String</td><td>Code of the pre-registered coupon. Must exist in the dashboard, be within validity period, and have available stock.</td></tr><tr><td><code>taxes</code></td><td>Optional</td><td>—</td><td>Array</td><td>Taxes to apply. Pass the pre-registered tax code.</td></tr><tr><td><code>taxes[].code</code></td><td>If taxes present</td><td>20</td><td>String</td><td>Code of the pre-registered tax. Must exist in the dashboard under the specified collection.</td></tr><tr><td><code>currency</code></td><td>✅</td><td>3</td><td>String</td><td>ISO 4217 currency code (e.g., <code>IDR</code>, <code>MYR</code>).</td></tr><tr><td><code>total_amount</code></td><td>✅</td><td>—</td><td>Decimal</td><td>Grand total per billing cycle: plan price(s) minus coupon discount plus tax. For <code>MYR</code> must be ≥ 2.00. Must match the server-calculated total — a mismatch will return a validation error.</td></tr></tbody></table>

**Schedule parameters (`plan_schedule`):**

<table><thead><tr><th>Parameter</th><th width="102.59765625">Required</th><th width="87.83984375">Type</th><th width="137.27734375">Format</th><th width="221.71484375">Description</th></tr></thead><tbody><tr><td><code>plan_schedule.start_date</code></td><td>✅</td><td>String</td><td>ISO 8601 UTC</td><td>Subscription activation datetime. Must be ≥ request timestamp. Cannot be a past date.</td></tr><tr><td><code>plan_schedule.end_date.type</code></td><td>✅</td><td>Enum</td><td>—</td><td><code>SPECIFIC_DATE</code> — ends on a calendar date. <code>AFTER_X_TIMES</code> — ends after N billing cycles. <code>NEVER</code> — runs until manually canceled.</td></tr><tr><td><code>plan_schedule.end_date.specific_date</code></td><td>If SPECIFIC_DATE</td><td>String</td><td>ISO 8601 UTC</td><td>The date the subscription ends. Must be ≥ <code>start_date</code>.</td></tr><tr><td><code>plan_schedule.end_date.count</code></td><td>If AFTER_X_TIMES</td><td>Integer</td><td>—</td><td>Number of billing cycles before the subscription expires automatically. Must be > 0.</td></tr></tbody></table>

**Customer parameters (`customers[]`):**

<table><thead><tr><th width="159.09375">Parameter</th><th width="109.08203125">Required</th><th width="94.08984375">Max Length</th><th width="89.1484375">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>customers[].id</code></td><td>Optional</td><td>20</td><td>String</td><td>Customer ID in the DOKU member database (Member Center). Provide this to link the subscription to an existing member record.</td></tr><tr><td><code>customers[].ext_id</code></td><td>Optional</td><td>20</td><td>String</td><td>Customer reference ID in the merchant's own system. Used for cross-referencing with internal records.</td></tr><tr><td><code>customers[].email</code></td><td>✅</td><td>100</td><td>String</td><td>Valid RFC 5322 email address. Invoices and receipts are delivered to this address.</td></tr><tr><td><code>customers[].name</code></td><td>✅</td><td>100</td><td>String</td><td>Customer full name.</td></tr><tr><td><code>customers[].phone_number</code></td><td>✅</td><td>15</td><td>String</td><td>Phone number excluding country calling code.</td></tr><tr><td><code>customers[].calling_code</code></td><td>✅</td><td>2</td><td>String</td><td>Country calling code: <code>62</code> (Indonesia), <code>60</code> (Malaysia).</td></tr><tr><td><code>customers[].additional_data</code></td><td>Optional</td><td>—</td><td>Object</td><td>Container for custom registration metadata (e.g., member group data, form responses).</td></tr></tbody></table>

***

#### Response Scenarios

{% tabs %}
{% tab title="✅ Success" %}
**HTTP 201 — Subscription created successfully.**

The subscription has been registered. The first invoice will be issued automatically on `plan_schedule.start_date`.

```json
{
    "code": "success",
    "type": "success",
    "message": "Success",
    "data": "SUB26000158"
}
```

**What happens next:**

* The subscription is visible in the DOKU Dashboard under Subscription and Billing → Subscription
* The first invoice is generated on the start date and delivered to the customer via the configured distribution channel
* Subsequent invoices are issued automatically at each billing cycle
  {% endtab %}

{% tab title="❌ Unregistered Code" %}
**HTTP 400 — Collection, plan, tax, or coupon code not found.**

One or more of the entity codes passed in the request do not exist in the dashboard, or do not belong to the specified collection. It can also happen when coupon is ineligible.

```json
{
  "error": {
    "code": "invalid_parameter",
    "type": "invalid_request_error",
    "message": "One or more requested plans not found in collection"
  }
}
```

```json
{
  "error": {
    "code": "failed_creating_subscription",
    "type": "failed_creating_subscription",
    "message": "Plan stock is empty: PLAN26000008"
  }
}
```

```json
{
    "error": {
        "code": "failed_creating_subscription",
        "type": "failed_creating_subscription",
        "message": "Active or eligible coupon not found for code: CEDARCREEK"
    }
} 
```

```json
{
    "error": {
        "code": "failed_creating_subscription",
        "type": "failed_creating_subscription",
        "message": "Coupon stock empty: CPN26000090"
    }
}
```

**Common causes and resolution:**

<table><thead><tr><th width="264.4765625">Cause</th><th>Resolution</th></tr></thead><tbody><tr><td><code>collection_code</code> does not exist</td><td>Create the collection in the dashboard: Subscription and Billing → Collection → Collection</td></tr><tr><td><code>plans[].code</code> does not exist</td><td>Create the plan in the dashboard: Subscription and Billing → Collection → Plan</td></tr><tr><td><code>plans[].code</code> exists but belongs to a different collection</td><td>Verify the plan belongs to the collection passed in <code>collection_code</code></td></tr><tr><td><code>taxes[].code</code> does not exist</td><td>Create the tax in the dashboard: Subscription and Billing → Collection → Tax</td></tr><tr><td><code>coupons[].code</code> does not exist or has expired</td><td>Create or reactivate the coupon in the dashboard: Subscription and Billing → Collection → Coupon</td></tr><tr><td>Code is correct but wrong casing</td><td>All codes are case-sensitive — verify the exact string including uppercase/lowercase</td></tr></tbody></table>
{% endtab %}

{% tab title="❌ Invalid Schedule" %}
**HTTP 400 — Schedule configuration is invalid.**

The `plan_schedule.start_date` is in the past, or the `end_date` configuration is inconsistent.

```json
{
    "error": {
        "code": "invalid_parameter",
        "type": "invalid_request_error",
        "message": "Invalid date format. Must be in ISO8601 format"
    }
}
```

```json
{
    "error": {
        "code": "invalid_parameter",
        "type": "invalid_request_error",
        "message": "plan_schedule.end_date.specific_date specificDate is required when type is SPECIFIC_DATE"
    }
}
```

**Common causes and resolution:**

| Cause                                                             | Resolution                                                      |
| ----------------------------------------------------------------- | --------------------------------------------------------------- |
| `start_date` is in the past                                       | Set `start_date` to the current datetime or a future datetime   |
| `end_date.specific_date` is before `start_date`                   | Ensure `specific_date` is ≥ `start_date`                        |
| `end_date.count` is 0 or negative                                 | Set `count` to a positive integer ≥ 1                           |
| `end_date.type` is `SPECIFIC_DATE` but `specific_date` is missing | Include `specific_date` when `end_date.type` is `SPECIFIC_DATE` |
| `end_date.type` is `AFTER_X_TIMES` but `count` is missing         | Include `count` when `end_date.type` is `AFTER_X_TIMES`         |
| {% endtab %}                                                      |                                                                 |

{% tab title="❌ Total Amount Mismatch" %}
**HTTP 400 — `total_amount` does not match the server-calculated total.**

The `total_amount` passed in the request does not match the amount calculated by the server based on the plan price(s), coupon discount, and tax rate.

```json
{
  "error": {
    "code": "Payment amount mismatch",
    "type": "Payment amount mismatch",
    "message": "Payment amount mismatch"
  }
}
```

**Resolution:** Recalculate `total_amount` using the following order before submitting:

> Plan price(s) × qty → Coupon discount deducted → Subtotal → Tax applied on subtotal → `total_amount`

Verify each component's value against the registered plan price, coupon discount value, and tax rate in the dashboard before resubmitting.
{% endtab %}

{% tab title="❌ Currency not match" %}
**HTTP 400 — `currency`  does not match the merchant's registered business country.**

The `currency` value passed in the request is not valid for this merchant account. The accepted currency is determined by the country in which the merchant's business account is registered — it cannot be overridden per request.

```json
{
  "error": {
    "code": "invalid_parameter",
    "type": "invalid_request_error",
    "message": "Currency must match with business country"
  }
}
```

**Currency by registered business country:**

<table><thead><tr><th width="321.9609375">Registered Business Country</th><th>Required Currency</th></tr></thead><tbody><tr><td>🇮🇩 Indonesia</td><td><code>IDR</code></td></tr><tr><td>🇲🇾 Malaysia</td><td><code>MYR</code></td></tr></tbody></table>

**Common causes and resolution:**

<table><thead><tr><th width="215.9140625">Cause</th><th>Resolution</th></tr></thead><tbody><tr><td>Wrong currency code for business country</td><td>Use the currency that matches your merchant account's registered country — <code>IDR</code> for Indonesia, <code>MYR</code> for Malaysia</td></tr><tr><td>Currency code is lowercase or mixed case</td><td><code>currency</code> is case-sensitive — use uppercase: <code>"IDR"</code> or <code>"MYR"</code>, not <code>"idr"</code> or <code>"Myr"</code></td></tr><tr><td>Attempting to use a third currency</td><td>Only the currency matching the registered business country is accepted. Multi-currency is not supported within a single merchant account</td></tr></tbody></table>

{% hint style="info" %}
Your merchant account's registered business country is set at the time of KYB (Know Your Business) verification on DOKU Dashboard and cannot be changed without re-verification. If you need to process transactions in a different currency, a separate merchant account registered in the corresponding country is required.
{% endhint %}
{% endtab %}

{% tab title="❌ Invalid Customer Data" %}
**HTTP 400 — Customer email or phone number format is invalid.**

One or more fields in the `customers[]` object failed format validation. The subscription is not created.

```json
{
  "error": {
    "code": "invalid_parameter",
    "type": "invalid_request_error",
    "message": "customers[0].email must be a well-formed email address"
  }
}
```

```json
{
  "error": {
    "code": "invalid_parameter",
    "type": "invalid_request_error",
    "message": "customers[0].phone_number must match \"^[0-9]*$\""
  }
}
```

**Common causes and resolution:**

<table><thead><tr><th width="276.0859375">Case</th><th>Resolution</th></tr></thead><tbody><tr><td>Missing <code>@</code> symbol — e.g., <code>johnsmithatemail.com</code></td><td>Ensure the email contains exactly one <code>@</code> separating local part and domain</td></tr><tr><td>Missing domain after <code>@</code> — e.g., <code>john@</code></td><td>Provide a complete domain — e.g., <code>john@email.com</code></td></tr><tr><td>Missing local part before <code>@</code> — e.g., <code>@email.com</code></td><td>Provide the local part before <code>@</code></td></tr><tr><td>Consecutive dots in domain — e.g., <code>john@email..com</code></td><td>Remove duplicate dots in the domain portion</td></tr><tr><td>Space in address — e.g., <code>john smith@email.com</code></td><td>Remove all spaces — email addresses do not allow spaces</td></tr><tr><td>Exceeds max length</td><td>Max 100 characters</td></tr><tr><td>Calling code included — e.g., <code>phone_number: "6281234567890"</code> with <code>calling_code: "62"</code></td><td>Pass the calling code in <code>calling_code</code> only — e.g., <code>phone_number: "81234567890"</code>, <code>calling_code: "62"</code></td></tr><tr><td>Non-numeric characters — e.g., <code>"+6281234567"</code> or <code>"0812-3456-7890"</code></td><td>Strip all non-numeric characters including <code>+</code>, <code>-</code>, spaces, and parentheses. Digits only</td></tr><tr><td>Unsupported value — e.g., <code>calling_code: "1"</code></td><td>Accepted values: <code>"62"</code> (Indonesia) and <code>"60"</code> (Malaysia) only</td></tr><tr><td>Exceeds max length</td><td>Max 15 characters, excluding calling code</td></tr></tbody></table>
{% endtab %}

{% tab title="❌ Account Suspended" %}
**HTTP 404 — Merchant account is suspended.**

The merchant account has been suspended by DOKU. No new transactions — subscriptions, bills, or any other — can be created until the suspension is lifted. The request payload is valid; the error is at the account level, not the request level.

```json
{
  "error": {
    "code": "not_found",
    "type": "not_found",
    "message": "Merchant not found or invalid finance status"
  }
}
```

**Resolution:**

This error cannot be resolved by modifying the request. Contact your **DOKU Account Manager** or **DOKU Support** directly to identify the cause of the suspension and the steps required to reinstate the account.

{% hint style="info" %}
Do not retry the request repeatedly while the account is in suspended state — the request will continue to fail regardless of payload content until the account suspension is lifted by DOKU.&#x20;
{% endhint %}
{% endtab %}

{% tab title="❌ Authentication Error" %}
**HTTP 401 — Request authentication failed.**

The authentication headers are missing, malformed, or the signature does not match.

```json
{
  "error": {
    "code": "invalid_signature",
    "message": "Invalid Header Signature",
    "type": "invalid_request_error"
  }
}
```

**Resolution:** Verify all required headers are present and correctly formed — `Client-Id`, `Request-Id`, `Request-Timestamp`, and `Signature`. The `Request-Timestamp` must be within an acceptable clock skew of the server time. Regenerate the signature if in doubt. → See the [Authentication](#authentication) tab.
{% endtab %}
{% endtabs %}
{% endtab %}
{% endtabs %}

## Use Cases

{% tabs %}
{% tab title="ISP" %}

<table><thead><tr><th width="167.1875">Customer POV</th><th>Use Case Details</th></tr></thead><tbody><tr><td></td><td><strong>Description</strong> An internet service provider wants to integrate FlexiBill billing into their customer activation portal — when a customer activates a package, the system automatically creates a subscription and sends the first invoice. <strong>Solution</strong> Integrate the Create Subscription API into the activation portal backend. When a customer selects a package and submits, the system calls the FlexiBill API to create the subscription and deliver the first invoice programmatically. <strong>How It Works</strong> Customer selects package in portal → backend calls Create Subscription API → FlexiBill creates subscription and delivers first invoice → billing runs automatically each cycle. <strong>Features Used</strong> Create Subscription API, Generate Bill API, Webhook</td></tr></tbody></table>
{% endtab %}

{% tab title="SaaS" %}

<table><thead><tr><th width="175.85546875">Customer POV</th><th>Use Case Details</th></tr></thead><tbody><tr><td></td><td><strong>Description</strong> A SaaS platform wants to bill customers based on the number of active users each month (per-seat billing). <strong>Solution</strong> At the end of each month, the SaaS system counts active users and calls the Generate Bill API with the dynamically calculated amount. <strong>How It Works</strong> System counts active users → calls Generate Bill API with calculated amount → FlexiBill delivers invoice to customer → customer pays via payment link. <strong>Features Used</strong> Generate Bill API (One-Time Bill), Webhook</td></tr></tbody></table>
{% endtab %}
{% endtabs %}

## Merchant & Customer Experience

#### Merchant View

Merchants (developers) integrate the API into their system backend:

* Use the **Sandbox endpoint** for development and testing
* Switch to the **Production endpoint** after testing is complete
* Monitor bills and subscriptions created via API in the DOKU Dashboard just like any other billing

#### Customer View

The customer experience is identical to dashboard-initiated billing:

* Receives invoice via Email or WhatsApp
* Pays via the payment link
* Receives receipt after successful payment

## Terms & Conditions

* Merchant must be registered with a **corporate business account** on DOKU Dashboard
* Business must be **KYB verified** before using the production API
* All requests must use **HTTPS** and include valid authentication headers

## FAQ

<details>

<summary>What is the difference between the Sandbox and Production endpoints?</summary>

The Sandbox endpoint is used for development and testing — no real transactions are processed and no invoices are delivered to actual customers. The Production endpoint processes live transactions and delivers real invoices. Always develop and test in Sandbox first, then switch the base URL to Production when going live. Your `Client-Id` and `Client-Secret` are different between environments — use the correct credentials for each.

</details>

<details>

<summary>Do bills and subscriptions created via API appear in the DOKU Dashboard?</summary>

Yes. All bills and subscriptions created via the API are immediately visible in the DOKU Dashboard — under Subscription and Billing → Bill and Subscription and Billing → Subscription respectively — and are manageable the same way as records created through the dashboard.

</details>

<details>

<summary>How do I obtain my Client-Id and generate the Signature?</summary>

Your `Client-Id` is available in the DOKU Dashboard under the Integrations menu. The `Signature` is an HMAC-SHA256 hash generated from your `Client-Secret` and a canonical request string composed of the request headers and body. Full instructions for signature generation are available in the DOKU API documentation.

</details>

<details>

<summary>The collection, plan, or coupon code I am passing is returning a validation error. What should I check?</summary>

All entity codes (`collection_code`, `plans[].code`, `taxes[].code`, `coupons[].code`) must be pre-registered in the DOKU Dashboard before they can be referenced via the API. There is no API endpoint to create these entities programmatically. Check the following in order:

1. The entity exists in the dashboard under the correct menu path
2. The code string is an exact match — these values are **case-sensitive**
3. For plans: the plan belongs to the collection specified in `collection_code`
4. For coupons: the coupon is active, within its validity period, and has available stock
5. For taxes: the tax is associated with the collection specified in `collection_code`

</details>

<details>

<summary>How should I calculate `total_amount` before submitting the Create Subscription request?</summary>

Calculate `total_amount` using the following order:

> Plan price(s) × qty → Coupon discount deducted → Subtotal → Tax rate applied on subtotal → `total_amount`

The value you send must match the server-calculated total exactly. A mismatch will return a `INVALID_REQUEST` error with `field: total_amount`. Verify the registered plan price, coupon discount value (flat or percentage), and tax rate in the dashboard before computing.

</details>

<details>

<summary>Can I pass both `subscription_code` and `auto_generate_id: true` in the same request?</summary>

Yes, but the `subscription_code` takes precedence. When `subscription_code` is present, the system uses that value and ignores `auto_generate_id`. Only omit `subscription_code` entirely when you want the system to auto-generate one. The generated code is not returned in the response body — look it up in the dashboard or via webhook if you need it.

</details>

<details>

<summary>What happens if I submit a Create Subscription request with a `start_date` in the past?</summary>

The request will return an `INVALID_REQUEST` error with `field: plan_schedule.start_date`. The `start_date` must be greater than or equal to the request timestamp. Past dates are not accepted. If you need the subscription to start immediately, set `start_date` to the current UTC time.

</details>

<details>

<summary>My Generate Bill request returned `state: INVOICE_ERROR`. What should I do?</summary>

`INVOICE_ERROR` means the invoice was created but delivery to the customer failed — the most common cause is insufficient deposit balance in your DOKU merchant account. Top up your deposit balance from the DOKU Dashboard. Once balance is restored, the system will automatically retry delivery. You can also monitor the current state of the invoice from the dashboard under Subscription and Billing → Bill.

</details>

<details>

<summary>How do I handle duplicate requests safely for Generate Bill?</summary>

The `bill_identifier` field acts as an idempotency key — the same identifier cannot be used twice within a merchant account. If your request times out or you are unsure whether it was processed, do **not** immediately retry with the same `bill_identifier`. First query the bill's status via the dashboard or inquiry endpoint to confirm whether the original request succeeded. Only generate a new bill (with a new identifier) if you have confirmed the original was not processed.

</details>

<details>

<summary>What is the difference between Generate Bill and Create Subscription for recurring billing?</summary>

Both support recurring billing, but they serve different purposes:

**Generate Bill (RECURRING)** — You define the schedule and the system handles recurring invoice issuance. The billing configuration (amount, cycle, customer) is passed directly in the API call with no dependency on dashboard-configured entities. Use this for custom, ad-hoc, or dynamically-calculated billing where amounts may vary per cycle.

**Create Subscription** — The subscription is bound to a pre-registered Collection and Plan. Pricing, billing cycle, tax, and coupon are all defined in the dashboard. Use this for structured product/service offerings where the plan configuration is reusable and managed centrally.

</details>

<details>

<summary>How do I know when a customer has paid their invoice?</summary>

Set up a Webhook to receive real-time payment notifications. When a customer completes payment, FlexiBill sends a `PAID` event to your configured webhook endpoint with the invoice number and payment details. You can also poll the bill status from the dashboard. → [Webhook](broken://pages/e5325b61ccd5f47ff8164ce256cd39840d32aa1a)

</details>

<details>

<summary>Can I cancel or modify a subscription created via the API?</summary>

Subscriptions created via the API can be viewed and managed from the DOKU Dashboard. Cancellation is performed from the dashboard — there is no dedicated cancel subscription API endpoint. Plan, collection, and schedule fields cannot be changed after a subscription is created. To change the plan for a customer, cancel the existing subscription from the dashboard and create a new one via the API with the updated configuration.

</details>


---

# 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.doku.com/subscription-and-billing/flexibill/subscription-and-billing/host-to-host-integration.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.
