On this page
Create
Create a reusable deposit identity for one payer, keyed by your own external ID, with one permanent address per supported network.
API key: Payment
A payment channel is a permanent deposit identity for one of your payers. Unlike an invoice, it has no amount, no expiry and no terminal state: you create it once, show its addresses, and every transfer that arrives becomes a deposit credited to your balance.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
string | Yes | Prefixed project identifier (prj_…). The project decides which tokens the channel accepts. |
external_id |
string | Yes | Your own stable identifier for the payer, at most 128 characters. Immutable once the channel exists. |
{
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"external_id": "customer-42"
}
Idempotency
The pair project_id + external_id, inside the environment of the API key you sign with, is the idempotency key. There is nothing else to send, so a replay has no payload conflict to resolve.
- —First call →
201 Createdwith aLocationheader pointing at the new channel. - —Every later call with the same pair →
200 OKwith the same channel. No duplicate is created. - —The same
external_idin sandbox and in production are two independent channels.
Send the payer identifier you already use in your own system, and call this endpoint on every checkout rather than caching the pc_ id yourself. A repeat is neither an error nor a duplicate: both statuses return a channel, and the 200 carries whatever addresses and tokens the channel has today. Retrying a request that timed out returns the original channel instead of opening a second one.
Code examples
Each tab uses the official SDK for that language. Signing, serialization, safe retries, and typed errors are handled by the SDK.
API_KEY_ID="pk_live_xxxxxxxxxxxx"
API_SECRET="sk_live_xxxxxxxxxxxx"
BODY='{"project_id":"prj_xxxxxxxxxxxx","external_id":"customer-42"}'
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIGNATURE=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/payment-channels '' "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)
curl -sS https://api.paymos.io/v1/payment-channels \
-H "Authorization: HMAC-SHA256 $API_KEY_ID:$SIGNATURE" \
-H "X-Request-Timestamp: $TS" \
-H "Content-Type: application/json" \
-d "$BODY"
import { Paymos } from '@paymos/sdk';
const paymos = new Paymos({
apiKey: process.env.PAYMOS_API_KEY,
apiSecret: process.env.PAYMOS_API_SECRET,
});
const channel = await paymos.paymentChannels.create({
projectId: 'prj_xxxxxxxxxxxx',
externalId: 'customer-42',
});
for (const rail of channel.networks) {
console.log(rail.network, rail.address ?? rail.status);
}
import os
from paymos import Paymos
paymos = Paymos(
api_key=os.environ["PAYMOS_API_KEY"],
api_secret=os.environ["PAYMOS_API_SECRET"],
)
channel = paymos.payment_channels.create(
project_id="prj_xxxxxxxxxxxx",
external_id="customer-42",
)
for rail in channel["networks"]:
print(rail["network"], rail.get("address", rail["status"]))
<?php
use Paymos\Client;
use Paymos\ClientConfig;
$paymos = new Client(new ClientConfig(
getenv('PAYMOS_API_KEY'),
getenv('PAYMOS_API_SECRET')
));
$channel = $paymos->paymentChannels()->create(array(
'project_id' => 'prj_xxxxxxxxxxxx',
'external_id' => 'customer-42',
));
foreach ($channel['networks'] as $rail) {
$address = isset($rail['address']) ? $rail['address'] : $rail['status'];
echo $rail['network'] . ' ' . $address . PHP_EOL;
}
package main
import (
"context"
"fmt"
"os"
paymos "github.com/Paymos-labs/go-sdk/v2"
)
func main() {
client, err := paymos.NewClient(os.Getenv("PAYMOS_API_KEY"), os.Getenv("PAYMOS_API_SECRET"))
if err != nil {
panic(err)
}
channel, err := client.PaymentChannels.Create(context.Background(), paymos.CreatePaymentChannelParams{
ProjectID: "prj_xxxxxxxxxxxx",
ExternalID: "customer-42",
})
if err != nil {
panic(err)
}
for _, rail := range channel.Networks {
fmt.Println(rail.Network, addressOrStatus(rail))
}
}
func addressOrStatus(rail paymos.PaymentChannelNetwork) string {
if rail.Address == nil {
return string(rail.Status)
}
return *rail.Address
}
using Paymos;
using var paymos = new PaymosClient(
Environment.GetEnvironmentVariable("PAYMOS_API_KEY")!,
Environment.GetEnvironmentVariable("PAYMOS_API_SECRET")!);
var channel = await paymos.PaymentChannels.CreateAsync(new CreatePaymentChannelRequest(
ProjectId: "prj_xxxxxxxxxxxx",
ExternalId: "customer-42"));
foreach (var rail in channel.Networks)
{
Console.WriteLine($"{rail.Network} {rail.Address ?? rail.Status}");
}
import io.paymos.CreatePaymentChannelRequest;
import io.paymos.PaymentChannel;
import io.paymos.PaymentChannelNetwork;
import io.paymos.PaymosClient;
PaymosClient paymos = new PaymosClient(
System.getenv("PAYMOS_API_KEY"),
System.getenv("PAYMOS_API_SECRET"));
PaymentChannel channel = paymos.paymentChannels.create(
new CreatePaymentChannelRequest("prj_xxxxxxxxxxxx", "customer-42"));
for (PaymentChannelNetwork rail : channel.networks()) {
System.out.println(rail.network() + " "
+ (rail.address() == null ? rail.status() : rail.address()));
}
require 'paymos'
paymos = Paymos::Client.new(
api_key: ENV.fetch('PAYMOS_API_KEY'),
api_secret: ENV.fetch('PAYMOS_API_SECRET')
)
channel = paymos.payment_channels.create(
project_id: 'prj_xxxxxxxxxxxx',
external_id: 'customer-42'
)
channel.networks.each do |rail|
puts "#{rail.network} #{rail.address || rail.status}"
end
use paymos::{CreatePaymentChannelRequest, PaymosClient};
let paymos = PaymosClient::new(
std::env::var("PAYMOS_API_KEY")?,
std::env::var("PAYMOS_API_SECRET")?,
)?;
let channel = paymos
.payment_channels()
.create(&CreatePaymentChannelRequest {
project_id: "prj_xxxxxxxxxxxx".to_owned(),
external_id: "customer-42".to_owned(),
})
.await?;
for rail in &channel.networks {
println!("{} {}", rail.network, rail.address.as_deref().unwrap_or(&rail.status));
}
Response (201 Created, or 200 on a replay)
{
"id": "pc_2QhKZv6mRt9WdA3nYpLbXf",
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"external_id": "customer-42",
"status": "active",
"is_accepting_payments": true,
"is_fully_provisioned": false,
"is_test": false,
"applied_fee_percent": 1.0,
"customer_fee_percent": 0,
"networks": [
{
"network": "TRC20",
"status": "active",
"address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"tokens": [
{ "symbol": "USDT", "minimum_deposit": "1.00" },
{ "symbol": "USDC", "minimum_deposit": "1.00" }
]
},
{
"network": "ERC20",
"status": "active",
"address": "0x9f2a6c1b4e8d7350ab11c9e5f0d2743b86ce41d9",
"tokens": [
{ "symbol": "USDT", "minimum_deposit": "12.00" },
{ "symbol": "USDC", "minimum_deposit": "12.00" }
]
},
{
"network": "SOL",
"status": "provisioning",
"tokens": [
{ "symbol": "USDC", "minimum_deposit": null }
]
}
],
"created_at": 1767225600,
"updated_at": 1767225780
}
Networks and addresses
A channel has no token list of its own. It accepts whatever the project accepts, so enabling a token on the project adds it to every channel of that project, and disabling it removes it everywhere. Manage the accepted set in Dashboard → Projects.
Each entry in networks describes one chain:
| Field | Meaning |
|---|---|
network |
Network code, for example TRC20 or ERC20. |
status |
active once the address exists, provisioning until then. |
address |
The permanent deposit address. Absent until that network finishes provisioning, which means not yet rather than not available. Once returned it never changes. |
tokens |
What that network accepts right now. Each entry is an object — symbol plus minimum_deposit — not a bare symbol. |
Two rules govern what you show a payer:
- —Present only
activenetworks that have a non-emptytokensarray. Aprovisioningentry has no address to show yet. - —Stop presenting a route the API no longer returns. Addresses are never reassigned, but a network you removed from the project disappears from the response, and money sent there is no longer expected.
An address, once returned, belongs to that channel forever. Cache it. Re-reading the channel returns the same value, and the same address serves every token on that network.
Each token entry carries its own minimum_deposit — the smallest transfer that route credits. Read it from the response you are about to render and show it beside the address; Retrieve explains the field, including what a null means.
Provisioning
A new production channel opens in provisioning with no addresses. Each network is provisioned independently and becomes usable the moment its own address exists — you do not have to wait for the last chain. The channel flips to active at the first ready address and stays there.
is_fully_provisioned tells you whether every required network is ready. A channel can be active and not fully provisioned; that is the normal state while the remaining chains catch up.
Sandbox channels are ready immediately: their addresses are derived locally and touch no chain.
Fees
applied_fee_percent and customer_fee_percent are your current rates. They are shown for reference and are not a quote — they change when your pricing changes.
The binding numbers live on each deposit. Every deposit records the rates that applied at the moment it was attributed and reports its own gross, fee and net. A pricing change never rewrites a payment you already received. There is no fee-preview route: create a sandbox deposit if you want to see the arithmetic.
Errors
| Code | HTTP | When |
|---|---|---|
payment_channels_disabled |
503 | Payment channels are not enabled for your account in this environment. |
payment_key_required |
403 | The request was signed with a Payout (rk_) key. |
project_not_found |
404 | project_id resolves to nothing visible to this credential. |
payment_channel_external_id_invalid |
400 | external_id is empty, blank or longer than 128 characters. |
payment_channel_project_has_no_supported_tokens |
409 | The project enables no token that a channel can collect. |
See Error Codes for the full catalogue.
What this API does not have
There is no update, delete, reassignment, callback-URL, per-channel token or fee-preview route. A channel's identity is immutable by design: an address a payer has saved must never start belonging to someone else. To retire a channel, block it.