# Host-to-Host (H2H) integration for Heleket merchants

This guide describes accepting crypto payments through direct server-to-server interaction with the Heleket API. Unlike the ready-made payment page, your backend creates invoices, receives payment details, and handles payment status notifications. You can render the payment form on your side or use the URL returned in the API response.

## 1\. What you need before starting

You need two values from your account to accept payments:

| Value | Where to get |
| --- | --- |
| Merchant ID | Merchant UUID. Go to Business → Merchants → Merchant settings. |
| Payment API key | Generated in the merchant settings after moderation. |

### How to get the payment API key

1.  Go to Business → Merchants → Create merchant and enter a name.
2.  Submit an application, enter the website URL, and confirm the domain.
3.  Wait until the merchant is approved.
4.  Copy the payment API key and Merchant ID from Settings.

> The payout API key is issued separately in Settings → API, requires two-factor authentication, and is not needed to accept payments.

Base endpoint for all requests:

`https://api.heleket.com/`

All API requests use POST with JSON and must be signed.

## 2\. Request authentication and signature

Each request is authenticated with two HTTP headers:

| Header | Value |
| --- | --- |
| merchant | Your Merchant ID (UUID). |
| sign | Request body signature. |
| Content-Type | `application/json` |

The signature is an MD5 hash of the base64-encoded JSON request body concatenated with your API key.

```
$body = json_encode($data);
$sign = md5(base64_encode($body) . $API_KEY);
```

For requests without body parameters, calculate the signature from an empty string:

```
$sign = md5(base64_encode('') . $API_KEY);
```

> Important: slash escaping. PHP escapes / in JSON (\\/) by default, while many other languages do not. The signature is calculated from the same string sent in the request body, so the string used for signing and the request body must be byte-for-byte identical. In non-PHP stacks, escape slashes manually; otherwise, the signature will not match (the same issue occurs when verifying webhooks; see section 5).

### Signed request example

```
curl https://api.heleket.com/v1/payment \
  -X POST \
  -H 'merchant: 8b03432e-385b-4670-8d06-064591096795' \
  -H 'sign: fe99035f86fa436181717b302b95bacff1' \
  -H 'Content-Type: application/json' \
  -d '{"amount":"15","currency":"USD","order_id":"1"}'
```

## 3\. H2H flow

1.  The customer places an order with you → you save the order\_id on your side
2.  Your server → POST /v1/payment → Heleket returns the uuid, address, amount, and url
3.  You show the customer the payment details / address / QR code (or the url link)
4.  The customer pays on the blockchain
5.  Heleket → POST to your url\_callback (webhook) whenever the status changes
6.  You verify the webhook signature → update the order
7.  (Fallback) periodically poll POST /v1/payment/info by order\_id

> Do not rely only on the webhook: always keep a fallback through /v1/payment/info (section 6) in case the callback is not delivered.

## 4\. Creating a payment

Endpoint:

**POST** `https://api.heleket.com/v1/payment`

### Main request parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| amount | string | yes | Amount to pay. Use a period as the decimal separator, for example, 10.28. |
| currency | string | yes | Invoice currency code (fiat or cryptocurrency), for example, USD, USDT, or BTC. |
| order\_id | string | yes | Your order identifier. It may contain only letters, digits, \_ and -. It must be unique. |
| network | string | no | Blockchain network code, for example, tron, bsc, or eth. |
| to\_currency | string | no | Target cryptocurrency for amount conversion (always a cryptocurrency code, not fiat). |
| url\_callback | string | no | URL to which Heleket sends status webhooks. It is effectively required for H2H. |
| url\_return | string | no | Where to return the customer from the payment form before payment. |
| url\_success | string | no | Where to return the customer after successful payment. |
| lifetime | integer | no | Invoice lifetime in seconds (300–43200; 3600 by default). |
| subtract | integer | no | Percentage of the fee to pass on to the customer (0–100). |
| accuracy\_payment\_percent | numeric | no | Allowed underpayment percentage (0–5): the invoice is marked as paid when the underpayment is within this limit. |
| is\_payment\_multiple | boolean | no | Allow the remaining balance to be paid. The default is true. |
| additional\_data | string | no | An arbitrary string for your use (not visible to the customer), up to 255 characters. |
| currencies | array | no | Allowlist of currencies and networks available for payment. |
| except\_currencies | array | no | Blocklist of currencies and networks. |
| discount\_percent | integer | no | Discount (positive) or surcharge (negative), from -99 to 100. |
| is\_refresh | boolean | no | Refresh an expired invoice with the same order\_id (new address and expiration time). |

> About order\_id: if an invoice with this order\_id already exists, a new one will not be created—the existing payment details will be returned. This provides idempotency: repeating a request for the same order is safe.

> When the wallet address is returned immediately. The address field is populated on creation only when the payment currency is unambiguous: the cryptocurrency and network are specified (to\_currency + network), or the cryptocurrency has only one network (for example, BTC). Otherwise, the customer selects the currency and network on the payment page, and the address appears later.

### Request body examples

Minimal invoice for 15 USD (the customer selects the cryptocurrency and network):

```
{ "amount": "15", "currency": "USD", "order_id": "1" }
```

Invoice for 20 USDT on the TRON network—the address is returned immediately:

```
{ "amount": "20", "currency": "USDT", "order_id": "1", "network": "tron" }
```

Invoice for 25 USD payable only in USDT on any network:

```
{ "amount": "25", "currency": "USD", "order_id": "1", "to_currency": "USDT" }
```

### Response example

```
{
  "state": 0,
  "result": {
    "uuid": "1ec87133-b22d-4643-988f-cac29a6ac85d",
    "order_id": "3",
    "amount": "20000.00",
    "payment_amount": null,
    "payer_amount": "254.92",
    "payer_currency": "USDT",
    "currency": "RUB",
    "merchant_amount": "249.82816502",
    "network": "bsc",
    "address": "0x2b...",
    "txid": null,
    "payment_status": "check",
    "url": "https://pay.heleket.com/pay/1ec87133-b22d-4643-988f-cac29a6ac85d",
    "expired_at": 1753202502,
    "is_final": false,
    "commission": "5.09853397",
    "address_qr_code": "data:image/png;base64 ..."
  }
}
```

### Key response fields

| Field | Purpose |
| --- | --- |
| uuid | Invoice UUID in Heleket. Store it with the order. |
| address | Payment wallet address. It may be null until the currency is selected. |
| payer\_amount | Amount to pay in payer\_currency, including a discount or surcharge. |
| payer\_currency | Payment currency. Null means that the customer has not selected it yet. |
| merchant\_amount | Amount credited to your balance after fees. |
| payment\_status | Current status (see section 7). |
| url | Link to the Heleket payment page if you do not render the form yourself. |
| address\_qr\_code | Base64-encoded QR code of the payment address. |
| expired\_at | Invoice expiration Unix timestamp. |
| is\_final | Whether the invoice is finalized and can no longer be paid. |

state: 0 means success. For validation errors, state: 1 (section 8).

## 5\. Webhook payment status notifications

Heleket sends a POST webhook whenever the invoice status changes.

### Main webhook fields

| Field | Description |
| --- | --- |
| type | Type: payment or wallet. |
| uuid | Payment UUID. |
| order\_id | Your order identifier, used to find the order. |
| amount | Invoice amount. |
| payment\_amount | Amount actually paid by the customer. |
| payment\_amount\_usd | Amount actually paid in USD. |
| merchant\_amount | Amount credited to the balance after the fee. |
| commission | Heleket fee. |
| is\_final | Whether the invoice is finalized. |
| status | Payment status (see section 7). |
| from | Payer wallet address. |
| network | Payment network. |
| currency | Invoice currency. |
| payer\_currency | Currency actually used for payment. |
| txid | Blockchain transaction hash (may be absent for P2P or manual closing). |
| sign | Webhook signature used for verification. |

### Webhook payload example

```
{
  "type": "payment",
  "uuid": "62f88b36-a9d5-4fa6-aa26-e040c3dbf26d",
  "order_id": "97a75bf8eda5cca41ba9d2e104840fcd",
  "amount": "3.00000000",
  "payment_amount": "3.00000000",
  "merchant_amount": "2.94000000",
  "commission": "0.06000000",
  "is_final": true,
  "status": "paid",
  "from": "THgEWubVc8tPKXLJ4VZ5zbiiAK7AgqSeGH",
  "network": "tron",
  "currency": "TRX",
  "payer_currency": "TRX",
  "txid": "6f0d9c8374db57cac0d806251473de754f361c83a03cd805f74aa9da3193486b",
  "sign": "a76c0d77f3e8e1a419b138af04ab600a"
}
```

### Required webhook verification

Because you deliver a product or credit a user’s balance based on a webhook, you must ensure that the request came from Heleket. Verify it using both methods:

1.  Allow callback requests only from the Heleket IP address: 31.133.220.8.
2.  Signature verification. The signature is calculated using the same algorithm as for requests, but is verified as follows:

```
// 1. Read the raw request body
$data = json_decode(file_get_contents('php://input'), true);

// 2. Extract and remove the signature from the array
$sign = $data['sign'];
unset($data['sign']);

// 3. Calculate the hash from the body (without sign) plus your payment API key
$hash = md5(base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE)) . $apiPaymentKey);

// 4. Compare
if (!hash_equals($hash, $sign)) {
    // invalid signature — reject
    http_response_code(400);
    exit;
}
```

> The same slash issue described in section 2 applies: when encoding JSON outside PHP, escape / manually (JSON.stringify(data).replace(/\\//g, "\\\\/") in JS); otherwise, the signature will not match.

### Webhook processing recommendations

-   Idempotency. The same status may be delivered more than once (including through manual resending). Use order\_id + status and do not deliver the product twice.
-   React to final statuses (paid, paid\_over), not intermediate ones.
-   Return HTTP 200 only after the webhook is processed successfully.
-   Compare payment\_amount and merchant\_amount with the expected order amount.

## 6\. Status polling as a webhook fallback

Endpoint:

**POST** `https://api.heleket.com/v1/payment/info`

Pass uuid or order\_id (if both are passed, order\_id takes priority).

```
curl https://api.heleket.com/v1/payment/info \
  -X POST \
  -H 'merchant: 8b03432e-385b-4670-8d06-064591096795' \
  -H 'sign: <body signature>' \
  -H 'Content-Type: application/json' \
  -d '{"order_id":"1"}'
```

The response contains the same payment object with its current payment\_status and is\_final values. Use this method for periodic reconciliation of “stuck” orders, not as the primary mechanism (webhooks are faster and require fewer requests).

## 7\. Payment statuses

| Status | Final | Meaning |
| --- | --- | --- |
| check | no | Waiting for the transaction to appear on the blockchain. |
| process | no | The payment is being processed. |
| confirm\_check | no | The transaction is visible; waiting for the required number of network confirmations. |
| wrong\_amount\_waiting | no | Underpayment with the option to pay the remaining balance. |
| paid | yes | The exact required amount was paid. Deliver the product. |
| paid\_over | yes | More than the required amount was paid. Deliver the product. |
| wrong\_amount | yes | The customer paid less than required. |
| fail | yes | Payment error. |
| cancel | yes | The payment was cancelled; the customer did not pay. |
| system\_fail | yes | System error. |
| locked | yes | Funds are locked under the AML program. |
| refund\_process | no | The refund is being processed. |
| refund\_paid | yes | The refund was completed. |
| refund\_fail | yes | Refund error. |

Treat paid and paid\_over as successful payments. confirm\_check may also appear in webhooks as an intermediate status.

## 8\. Error handling

Validation errors — HTTP 422, state: 1:

```
{ "state": 1, "errors": { "amount": ["validation.required"] } }
```

Common messages (state: 1, message field):

| Message | Reason |
| --- | --- |
| The network was not found | An unsupported network code was provided. |
| The currency was not found | An unsupported currency code was provided. |
| Not found service to\_currency | No payment service is available for to\_currency. |
| Minimum amount 0.5 USDT | The amount is below the minimum for the currency. |
| Maximum amount 10000000 USDT | The amount exceeds the maximum for the currency. |
| Wallet not found | No active merchant wallet is available for the payment currency. |
| You are forbidden | Payments are blocked. Contact support. |
| Gateway error / Server error | A temporary technical issue occurred and the payment is unavailable. |

Internal error — HTTP 500:

```
{ "message": "Server error, #1", "code": 500, "error": null }
```

Treat Gateway error, Server error, and HTTP 500 as temporary and retry with exponential backoff.

## 9\. Integration checklist

-   The merchant is approved, and the Merchant ID and payment API key are available.
-   Request signing is implemented, including forward-slash escaping.
-   Invoice creation uses POST /v1/payment with a unique order\_id and url\_callback.
-   The webhook endpoint is available over HTTPS and accepts POST requests.
-   Webhook requests are checked by IP address and signature.
-   Webhook processing is idempotent.
-   The product or service is delivered only for paid and paid\_over after amount verification.
-   Fallback polling through POST /v1/payment/info is implemented.
-   uuid, order\_id, txid, and statuses are logged.
-   API keys are stored in secrets, not in the codebase.

See the related documentation for currency and network codes, refunds, static wallets, and payouts.
