curl --request POST \
--url https://api.paj.cash/pub/v2/payment \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"amount": 100,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"method": "TOKEN"
}
'import requests
url = "https://api.paj.cash/pub/v2/payment"
payload = {
"amount": 100,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"method": "TOKEN"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
amount: 100,
mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
method: 'TOKEN'
})
};
fetch('https://api.paj.cash/pub/v2/payment', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.paj.cash/pub/v2/payment",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'amount' => 100,
'mint' => 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'method' => 'TOKEN'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.paj.cash/pub/v2/payment"
payload := strings.NewReader("{\n \"amount\": 100,\n \"mint\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",\n \"method\": \"TOKEN\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.paj.cash/pub/v2/payment")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"amount\": 100,\n \"mint\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",\n \"method\": \"TOKEN\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.paj.cash/pub/v2/payment")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"amount\": 100,\n \"mint\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",\n \"method\": \"TOKEN\"\n}"
response = http.request(request)
puts response.read_body{
"id": "507f1f77bcf86cd799439011",
"method": "FIAT",
"status": "INIT",
"amount": 100,
"totalAmount": 101,
"fee": 1,
"expiresAt": "2023-11-07T05:31:56Z",
"fiat": {
"currency": "NGN",
"accountNumber": "<string>",
"accountName": "<string>",
"bank": {
"id": "Bank of America",
"name": "Bank of America",
"country": "NG",
"code": "001",
"logo": "https://example.com/logo.png"
}
},
"token": {
"chain": "SOLANA",
"mint": "<string>",
"address": "<string>"
}
}Create a payment
Opens a payment collected on behalf of the API key’s business and returns the funding instructions. method defaults to TOKEN, which requires chain and mint and returns the deposit address under token. For method=FIAT a currency must be supplied and the response carries the virtual account details under fiat. The payment stays AWAITING until the funds land, and a platform fee is charged on top of amount — the payer funds totalAmount and your balance is credited the amount share. Once it settles the payment is posted to the paymentWebhookURL configured on your API keys.
curl --request POST \
--url https://api.paj.cash/pub/v2/payment \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"amount": 100,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"method": "TOKEN"
}
'import requests
url = "https://api.paj.cash/pub/v2/payment"
payload = {
"amount": 100,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"method": "TOKEN"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
amount: 100,
mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
method: 'TOKEN'
})
};
fetch('https://api.paj.cash/pub/v2/payment', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.paj.cash/pub/v2/payment",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'amount' => 100,
'mint' => 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'method' => 'TOKEN'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.paj.cash/pub/v2/payment"
payload := strings.NewReader("{\n \"amount\": 100,\n \"mint\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",\n \"method\": \"TOKEN\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.paj.cash/pub/v2/payment")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"amount\": 100,\n \"mint\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",\n \"method\": \"TOKEN\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.paj.cash/pub/v2/payment")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"amount\": 100,\n \"mint\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",\n \"method\": \"TOKEN\"\n}"
response = http.request(request)
puts response.read_body{
"id": "507f1f77bcf86cd799439011",
"method": "FIAT",
"status": "INIT",
"amount": 100,
"totalAmount": 101,
"fee": 1,
"expiresAt": "2023-11-07T05:31:56Z",
"fiat": {
"currency": "NGN",
"accountNumber": "<string>",
"accountName": "<string>",
"bank": {
"id": "Bank of America",
"name": "Bank of America",
"country": "NG",
"code": "001",
"logo": "https://example.com/logo.png"
}
},
"token": {
"chain": "SOLANA",
"mint": "<string>",
"address": "<string>"
}
}x-api-key header already does,
and the payment is opened for whichever business the key belongs to.
Being paid on-chain
method defaults to TOKEN, so the common case is a short request:
{
"amount": 25,
"chain": "SOLANA",
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
}
amount is in whole units of the token — 25 means 25 USDC, not 25 base
units. The response comes back with a token object holding the address to
pay, which is reserved for this payment and no other.
Token payments open on SOLANA and the five EVM chains — ETHEREUM, BASE,
BSC, MONAD and ARC. TON is not supported for payments yet, even though it
appears in the chain enum — a TOKEN payment on TON is a 400. Use TON for
onramp and
offramp orders only. Supported
chains lists the USDC contract on each.
Your balance is held in USDC. A payment in USDC is credited as it lands; a
payment in any other token is swapped to USDC first, and the swap’s output is
what is credited.
mint, on the chain named in chain, to that address
and nothing else. A different token arriving at the address is not recognised
as paying the payment, and recovering it is a manual process.Being paid in fiat
Setmethod to FIAT and supply a currency, and the response carries a
fiat object instead: a virtual account number, the account name, and the bank
it sits with. Your payer transfers into it exactly as they would for an onramp
order.
TOKEN for
anything that needs to reach your balance automatically.The platform fee
Paj charges a fee on each payment, currently 1% ofamount, and adds it on
top rather than taking it out. The response shows the split:
amount— what you asked for, and the share that reaches your balance.fee— Paj’s share, in the same units. Absent when no fee applies.totalAmount—amountplusfee, and what the payer must actually send.
totalAmount, not amount. A payer who sends only amount
has underpaid, and the payment settles for less than you asked — the fee is
taken proportionally from whatever arrives, so a 25 USDC payment funded with
exactly 25 credits you about 24.75.
The payment window
Every payment carries anexpiresAt, 30 minutes out. Money that arrives inside
that window settles normally; a payment that reaches its expiry unpaid moves to
EXPIRED, and for on-chain payments the deposit address is released.
That release is the reason the deadline matters. An address belonging to an
expired payment is no longer being watched on that payment’s behalf, so tokens
sent late are not credited automatically. Show the countdown to whoever is
paying, and open a fresh payment rather than reusing an address once the first
one lapses.
Following the payment
status starts at AWAITING. When a deposit is seen it becomes PROCESSING
while the funds are swept into Paj’s pool, then SUCCESSFUL once that
completes and your balance has been credited. EXPIRED and ERROR are
terminal.
You do not have to poll for that. Set a paymentWebhookURL on your API key and
Paj POSTs the settled payment to it — the same body this endpoint returned,
with status now SUCCESSFUL, so you can match it to the payment you opened by
id. It is separate from the rampWebhookURL that carries onramp and offramp
order updates, because the two payloads differ; a key can have one, both, or
neither.
If several of your keys name a payment webhook, each distinct URL is called.
Any 2xx from your endpoint counts as delivered. Anything else — a 4xx, a
5xx, or a connection that never opens — is retried every 30 seconds, up to
five attempts in total, after which the payment is left settled and the delivery
is abandoned. Return a 2xx as soon as you have durably recorded the payment
and do the rest of your work afterwards; a slow handler that eventually answers
500 costs you the next two minutes of retries.
Retries live in the process that saw the settlement, so a deploy mid-window ends
them. Treat the webhook as a prompt rather than a ledger, and let your own
records be the authority.
The credited amount follows what actually arrived, not what you asked for. If a
payer sends 24.5 USDC against a payment whose totalAmount is 25.25, the
payment settles for 24.5 and your balance is credited that less the fee —
reconcile against the settled amount rather than assuming the requested one.
Your balance accumulates across payments and is withdrawn from the Paj
dashboard, not through this API.Authorizations
Body
Amount to collect, in whole units. Must be positive.
100
Fiat currency to charge. Required when method=FIAT; ignored for TOKEN.
NGN, GHS, TZS, KES, ZAR, USD Chain to receive the token on. Required when method=TOKEN; ignored for FIAT.
SOLANA, MONAD, ETHEREUM, BSC, BASE, ARC, ZCASH, TON Mint/contract address of the token on the selected chain. Required when method=TOKEN.
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
How the payment is funded. Defaults to TOKEN, which issues a deposit address and requires chain and mint. FIAT issues a virtual account and requires currency.
FIAT, TOKEN Response
The pending payment with funding instructions (fiat or token).
Payment id. Subscribe to live updates with it.
"507f1f77bcf86cd799439011"
FIAT, TOKEN INIT, AWAITING, PROCESSING, SUCCESSFUL, EXPIRED, ERROR Requested amount, in whole units of the currency or token (the stored base units scaled back down by details.decimals).
100
What the payer must fund: amount plus any fee. Equal to amount when no fee was charged.
101
Platform fee charged on top of amount, in the same units. Absent when the payment carries no fee.
1
When the payment window closes.
Fiat funding details. Present when method=FIAT.
Show child attributes
Show child attributes
On-chain funding details. Present when method=TOKEN.
Show child attributes
Show child attributes