# Bridge
Source: https://docs.cngn.co/api-reference/bridge
POST https://api.cngn.co/v1/api/bridge
Move cNGN from one blockchain network to another
Bridges (swaps) cNGN between supported networks. The API returns a `receivableAddress` on the origin network. Send your cNGN there, and the equivalent amount is delivered to `destinationAddress` on the destination network.
Requires the **Swap** permission. The request body must be [encrypted](/guides/encryption). Get the fees and receivable amount first with [Bridge Quote](/api-reference/bridge-quote).
### Body parameters (plain payload, pre-encryption)
Network ID you are bridging **from**. See [Get Networks](/api-reference/get-networks).
Network ID you are bridging **to**.
Wallet address to receive cNGN on the destination network.
Address you will send from on the origin network.
HTTPS URL to notify when the bridge completes. See [Bridge completion callback](/guides/webhooks#bridge-completion-callback) for the payload.
### Response (decrypted `data`)
Deposit address on the origin network. Send the cNGN you want to bridge here.
Internal transaction ID.
Bridge reference for tracking.
```json Plain body (encrypt before sending) theme={null}
{
"originNetworkId": "1f2e3d4c-5b6a-7988-9a0b-c1d2e3f4a5b6",
"destinationNetworkId": "9b2e6a1f-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"destinationAddress": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
"senderAddress": "0x1fA2b3C4d5E6f7A8b9C0d1E2f3A4b5C6d7E8f9A0",
"callbackUrl": "https://yourapp.com/webhooks/cngn-bridge"
}
```
```bash cURL (wire format) theme={null}
curl -X POST "https://api.cngn.co/v1/api/bridge" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "",
"iv": ""
}'
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Swap was successfully",
"data": {
"receivableAddress": "0x2aB3c4D5e6F7a8B9c0D1e2F3a4B5c6D7e8F9a0B1",
"transactionId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"reference": "BR-5d4c3b2a"
}
}
```
```json 403 - Permission denied theme={null}
{
"status": false,
"message": "Permission denied"
}
```
# Bridge Quote
Source: https://docs.cngn.co/api-reference/bridge-quote
POST https://api.cngn.co/v1/api/bridge-quote
Get a fee quote before bridging cNGN between networks
Returns the amount you would receive and the fees charged for a bridge, before you commit to it with [Bridge](/api-reference/bridge).
Requires the **Swap** permission. The request body must be [encrypted](/guides/encryption).
### Body parameters (plain payload, pre-encryption)
Amount of cNGN you intend to bridge.
Network ID you are bridging **from**. See [Get Networks](/api-reference/get-networks).
Network ID you are bridging **to**.
Wallet address that would receive cNGN on the destination network.
### Response (decrypted `data`)
Amount of cNGN that will arrive at the destination after fees.
On-chain network fee for the bridge.
cNGN bridging service fee.
```json Plain body (encrypt before sending) theme={null}
{
"amount": 100000,
"originNetworkId": "1f2e3d4c-5b6a-7988-9a0b-c1d2e3f4a5b6",
"destinationNetworkId": "9b2e6a1f-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"destinationAddress": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72"
}
```
```bash cURL (wire format) theme={null}
curl -X POST "https://api.cngn.co/v1/api/bridge-quote" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "",
"iv": ""
}'
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Swap quote fetched successfully",
"data": {
"amountReceivable": "99750.00",
"networkFee": "150.00",
"bridgeFee": "100.00"
}
}
```
```json 403 - Permission denied theme={null}
{
"status": false,
"message": "Permission denied"
}
```
# Create Temporary Virtual Account
Source: https://docs.cngn.co/api-reference/create-temporary-virtual-account
POST https://api.cngn.co/v1/api/virtual-account/temporary
Create a one-time virtual account for a single customer deposit
Creates a short-lived virtual account tied to a specific expected amount, ideal for checkout flows where each customer payment needs its own account number. The account expires at `expiresAt`.
Requires the **Fiat Deposit** permission. The request body must be [encrypted](/guides/encryption).
### Body parameters (plain payload, pre-encryption)
Expected deposit amount in Naira. Minimum `100`.
The paying customer.
Customer email address.
Customer full name.
Custom display name for the account (3–50 characters).
Payment narration (max 100 characters).
### Response (decrypted `data`)
Your deposit reference.
Provider payment reference.
Amount the customer should transfer.
Total expected including fees.
Processing fee.
VAT on the fee.
`NGN`.
Deposit status (e.g. `pending`).
Narration attached to the account.
Temporary NUBAN account number.
Account display name.
Bank hosting the account.
CBN bank code.
ISO 8601 expiry timestamp. Deposits after this time are not credited.
```json Plain body (encrypt before sending) theme={null}
{
"amount": 50000,
"customer": {
"name": "Ada Obi",
"email": "ada@example.com"
},
"accountName": "Acme Checkout",
"narration": "Order #1042"
}
```
```bash cURL (wire format) theme={null}
curl -X POST "https://api.cngn.co/v1/api/virtual-account/temporary" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "",
"iv": ""
}'
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Temporary virtual account created successfully",
"data": {
"reference": "DEP-8c1f2a9b",
"paymentReference": "PAY-55aa66bb",
"amount": 50000,
"amountExpected": 50075,
"fee": 70,
"vat": 5,
"currency": "NGN",
"status": "pending",
"narration": "Order #1042",
"accountNumber": "8801234567",
"accountName": "Acme Checkout",
"bankName": "Wema Bank",
"bankCode": "035",
"expiresAt": "2026-07-22T18:45:00.000Z"
}
}
```
```json 400 - Validation error theme={null}
{
"status": 400,
"message": "amount: Minimum amount is 100 NGN"
}
```
# Get Balance
Source: https://docs.cngn.co/api-reference/get-balance
GET https://api.cngn.co/v1/api/balance
Retrieve your business's cNGN balances
Returns the cNGN balance(s) held by your business account.
### Response (decrypted `data`)
Array of asset balances.
Asset classification (e.g. `credit_alphanum4`).
Asset ticker (`CNGN`).
Current available balance as a decimal string.
```bash cURL theme={null}
curl -X GET "https://api.cngn.co/v1/api/balance" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
```
```typescript TypeScript theme={null}
const res = await fetch("https://api.cngn.co/v1/api/balance", {
headers: { Authorization: `Bearer ${API_KEY}` },
});
```
```python Python theme={null}
res = requests.get(
"https://api.cngn.co/v1/api/balance",
headers={"Authorization": f"Bearer {API_KEY}"},
)
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Balance fetched successfully",
"data": [
{
"asset_type": "credit_alphanum4",
"asset_code": "CNGN",
"balance": "150000.00"
}
]
}
```
```json 400 - Error theme={null}
{
"status": 400,
"message": "Merchant not found"
}
```
# Get Banks
Source: https://docs.cngn.co/api-reference/get-banks
GET https://api.cngn.co/v1/api/banks
List supported Nigerian banks and their codes
Returns the list of banks supported for redemptions and account verification, with their CBN bank codes.
The bank list changes rarely; cache it for hours rather than fetching on every request.
### Response (decrypted `data`)
Bank name.
CBN bank code. Use it as `bankCode` in other endpoints.
```bash cURL theme={null}
curl -X GET "https://api.cngn.co/v1/api/banks" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript TypeScript theme={null}
const res = await fetch("https://api.cngn.co/v1/api/banks", {
headers: { Authorization: `Bearer ${API_KEY}` },
});
```
```python Python theme={null}
res = requests.get(
"https://api.cngn.co/v1/api/banks",
headers={"Authorization": f"Bearer {API_KEY}"},
)
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Banks fetched successfully",
"data": [
{ "name": "Access Bank", "code": "044" },
{ "name": "Guaranty Trust Bank", "code": "058" },
{ "name": "United Bank for Africa", "code": "033" },
{ "name": "Zenith Bank", "code": "057" }
]
}
```
# Get Networks
Source: https://docs.cngn.co/api-reference/get-networks
GET https://api.cngn.co/v1/api/networks
List the blockchain networks cNGN currently supports
Returns the networks available for withdrawals, bridging, and address whitelisting. Use the returned `id` as `networkId` in other endpoints.
Network IDs differ between test and live environments. Resolve them per environment; never hard-code them.
### Query parameters
When `true`, includes underlying blockchain metadata for each network.
### Response (decrypted `data`)
Network ID. Pass this as `networkId` elsewhere.
Full network name.
Short code (e.g. `BASE`, `BSC`).
Disabled networks cannot be used for transfers.
Blockchain metadata when `includeBlockchain=true`.
```bash cURL theme={null}
curl -X GET "https://api.cngn.co/v1/api/networks" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript TypeScript theme={null}
const res = await fetch("https://api.cngn.co/v1/api/networks", {
headers: { Authorization: `Bearer ${API_KEY}` },
});
```
```python Python theme={null}
res = requests.get(
"https://api.cngn.co/v1/api/networks",
headers={"Authorization": f"Bearer {API_KEY}"},
)
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Supported networks fetched successfully",
"data": [
{
"id": "9b2e6a1f-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"name": "Base",
"short_name": "BASE",
"isDisabled": false,
"blockchain": null
},
{
"id": "1f2e3d4c-5b6a-7988-9a0b-c1d2e3f4a5b6",
"name": "Polygon",
"short_name": "POLYGON",
"isDisabled": false,
"blockchain": null
}
]
}
```
# Get Transactions
Source: https://docs.cngn.co/api-reference/get-transactions
GET https://api.cngn.co/v1/api/transactions
Retrieve your paginated transaction history
Returns all transactions on your business account (deposits, withdrawals, redemptions, and bridges), newest first.
### Query parameters
Page number to fetch.
Number of records per page.
### Response (decrypted `data`)
Array of transaction records.
Unique transaction ID.
Sender identifier or address.
Either `{ "address": "0x..." }` for on-chain transfers or `{ "bank": "...", "accountNumber": "..." }` for fiat settlement.
Transaction amount.
Human-readable description.
ISO 8601 creation timestamp.
Transaction reference. Use it with [Verify Withdrawal](/api-reference/verify-withdrawal).
Transaction type (e.g. `deposit`, `withdrawal`, `redeem`, `swap`).
Network the transaction executed on.
Asset classification.
Asset ticker (`CNGN`).
On-chain hash on the origin network, if applicable.
On-chain hash on the external/destination network, if applicable.
Block explorer URL for the transaction.
Transaction status (e.g. `pending`, `success`, `failed`).
Standard pagination metadata. See [Response Format](/guides/response-format#pagination).
```bash cURL theme={null}
curl -X GET "https://api.cngn.co/v1/api/transactions?page=1&limit=10" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript TypeScript theme={null}
const res = await fetch(
"https://api.cngn.co/v1/api/transactions?page=1&limit=10",
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
```
```python Python theme={null}
res = requests.get(
"https://api.cngn.co/v1/api/transactions",
params={"page": 1, "limit": 10},
headers={"Authorization": f"Bearer {API_KEY}"},
)
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Transactions fetched successfully",
"data": {
"data": [
{
"id": "9f8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"from": "Acme Ltd",
"receiver": { "address": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72" },
"amount": "25000.00",
"description": "Withdrawal to external wallet",
"createdAt": "2026-07-20T14:32:11.000Z",
"trx_ref": "WD-7f3a2b1c",
"trx_type": "withdrawal",
"network": "Base",
"asset_type": "credit_alphanum4",
"asset_symbol": "CNGN",
"base_trx_hash": "0x4a5b...e9f0",
"extl_trx_hash": null,
"explorer_link": "https://basescan.org/tx/0x4a5b...e9f0",
"status": "success"
}
],
"pagination": {
"count": 42,
"pages": 5,
"isLastPage": false,
"nextPage": 2,
"previousPage": null
}
}
}
```
# Get Virtual Account
Source: https://docs.cngn.co/api-reference/get-virtual-account
GET https://api.cngn.co/v1/api/virtual-account
Retrieve your dedicated virtual account(s) for Naira deposits
Returns the dedicated virtual bank account(s) assigned to your business. Naira transferred into these accounts is automatically converted and credited as cNGN.
Requires the **Fiat Deposit** permission. See [Permissions](/guides/permissions).
### Response (decrypted `data`)
10-digit NUBAN account number.
Account name (your registered business name).
Bank hosting the virtual account.
CBN bank code.
```bash cURL theme={null}
curl -X GET "https://api.cngn.co/v1/api/virtual-account" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript TypeScript theme={null}
const res = await fetch("https://api.cngn.co/v1/api/virtual-account", {
headers: { Authorization: `Bearer ${API_KEY}` },
});
```
```python Python theme={null}
res = requests.get(
"https://api.cngn.co/v1/api/virtual-account",
headers={"Authorization": f"Bearer {API_KEY}"},
)
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Virtual account created successfully",
"data": [
{
"accountNumber": "9977581222",
"accountName": "ACME LTD / CNGN",
"bankName": "Providus Bank",
"bankCode": "101"
}
]
}
```
```json 403 - Permission denied theme={null}
{
"status": false,
"message": "Permission denied"
}
```
# Get Whitelisted Addresses
Source: https://docs.cngn.co/api-reference/get-whitelisted-addresses
GET https://api.cngn.co/v1/api/whitelisted
List all wallet addresses whitelisted on your account
Returns every external address you have whitelisted, across all networks.
### Query parameters
When `true`, embeds a network summary (`id`, `name`, `short_name`) in each entry.
### Response (decrypted `data`)
Array of whitelisted address objects, with the same shape as [Whitelist Address](/api-reference/whitelist-address).
```bash cURL theme={null}
curl -X GET "https://api.cngn.co/v1/api/whitelisted?includeNetwork=true" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript TypeScript theme={null}
const res = await fetch(
"https://api.cngn.co/v1/api/whitelisted?includeNetwork=true",
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
```
```python Python theme={null}
res = requests.get(
"https://api.cngn.co/v1/api/whitelisted",
params={"includeNetwork": "true"},
headers={"Authorization": f"Bearer {API_KEY}"},
)
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Whitelisted addresses fetched successfully",
"data": [
{
"id": "7a8b9c0d-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
"networkId": "9b2e6a1f-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"publicKey": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
"internalPublicKey": "0x3cD4e5F6a7B8c9D0e1F2a3B4c5D6e7F8a9B0c1D2",
"network": {
"id": "9b2e6a1f-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"name": "Base",
"short_name": "BASE"
},
"created_at": "2026-07-22T10:15:00.000Z",
"updated_at": "2026-07-22T10:15:00.000Z"
}
]
}
```
# API Reference
Source: https://docs.cngn.co/api-reference/introduction
Conventions used across all cNGN API endpoints
## Base URL
```text theme={null}
https://api.cngn.co/v1/api
```
## Authentication
All endpoints require a Bearer API key and a whitelisted source IP:
```bash theme={null}
Authorization: Bearer cngn_live_xxxxxxxxxxxxx
```
See [Authentication](/guides/authentication).
## Encrypted payloads
**Request bodies** on `POST`/`PUT` endpoints must be AES-256-CBC encrypted and sent as `{ "content": "...", "iv": "..." }`. **Response `data` fields** are encrypted to your Ed25519 public key.
For readability, every example in this reference shows the **plain (decrypted) payloads**. On the wire, both are encrypted. See the [Encryption guide](/guides/encryption), or use an [official SDK](/sdks) which handles it transparently.
## Endpoints at a glance
| Method | Path | Description | Permission |
| ------ | ---------------------------- | ------------------------------------------- | ------------ |
| GET | `/balance` | Get cNGN balances | None |
| GET | `/transactions` | Paginated transaction history | None |
| GET | `/networks` | List supported blockchain networks | None |
| GET | `/virtual-account` | Get your dedicated deposit account(s) | Fiat Deposit |
| POST | `/virtual-account/temporary` | Create a one-time deposit account | Fiat Deposit |
| POST | `/redeemAsset` | Redeem cNGN to a bank account | Redeem |
| POST | `/account/verify` | Resolve a bank account name | None |
| GET | `/banks` | List supported banks with codes | None |
| PUT | `/bank-account` | Update your settlement bank account | None |
| POST | `/withdraw` | Withdraw cNGN to an external wallet | Send Crypto |
| GET | `/withdraw/verify/{tnxRef}` | Check a withdrawal's status | None |
| POST | `/bridge-quote` | Get fees and receivable amount for a bridge | Swap |
| POST | `/bridge` | Bridge cNGN between networks | Swap |
| POST | `/whitelist` | Whitelist an external wallet address | None |
| GET | `/whitelisted` | List whitelisted addresses | None |
## Rate limits
20 requests per 60 seconds per API key. Exceeding it blocks the key for 60 seconds with a `429`. See [Rate Limiting](/guides/rate-limits).
# Redeem Asset
Source: https://docs.cngn.co/api-reference/redeem-asset
POST https://api.cngn.co/v1/api/redeemAsset
Redeem cNGN for Naira, settled to a Nigerian bank account
Initiates a redemption: cNGN is burned and the equivalent Naira is paid out to the specified bank account.
Requires the **Redeem** permission. The request body must be [encrypted](/guides/encryption). Verify the destination account first with [Verify Account Details](/api-reference/verify-account-details).
### Body parameters (plain payload, pre-encryption)
Amount of cNGN to redeem. Minimum `1`.
CBN bank code of the destination bank, from [Get Banks](/api-reference/get-banks).
10-digit destination account number.
Save these bank details for future redemptions.
### Response (decrypted `data`)
Redemption transaction reference. Track it via [Get Transactions](/api-reference/get-transactions).
The deposit address associated with the redemption.
```json Plain body (encrypt before sending) theme={null}
{
"amount": 100000,
"bankCode": "058",
"accountNumber": "0123456789",
"saveDetails": true
}
```
```bash cURL (wire format) theme={null}
curl -X POST "https://api.cngn.co/v1/api/redeemAsset" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "",
"iv": ""
}'
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Deposit for redeem was successfully",
"data": {
"trxRef": "RD-3e7a91cf",
"address": "0x1fA2b3C4d5E6f7A8b9C0d1E2f3A4b5C6d7E8f9A0"
}
}
```
```json 400 - Service disabled theme={null}
{
"status": 400,
"message": "Service is currently unavailable. Please try again later."
}
```
# Update Bank Account
Source: https://docs.cngn.co/api-reference/update-bank-account
PUT https://api.cngn.co/v1/api/bank-account
Update your business's settlement bank account
Updates the bank account associated with your business for fiat settlement.
The request body must be [encrypted](/guides/encryption). Verify the account first with [Verify Account Details](/api-reference/verify-account-details).
### Body parameters (plain payload, pre-encryption)
Name of the bank.
Registered account name.
10-digit account number.
```json Plain body (encrypt before sending) theme={null}
{
"bankName": "Guaranty Trust Bank",
"bankAccountName": "ACME LTD",
"bankAccountNumber": "0123456789"
}
```
```bash cURL (wire format) theme={null}
curl -X PUT "https://api.cngn.co/v1/api/bank-account" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "",
"iv": ""
}'
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Bank account updated successfully",
"data": {
"bankName": "Guaranty Trust Bank",
"bankAccountName": "ACME LTD",
"bankAccountNumber": "0123456789"
}
}
```
# Verify Account Details
Source: https://docs.cngn.co/api-reference/verify-account-details
POST https://api.cngn.co/v1/api/account/verify
Resolve a bank account number to its registered account name
Resolves a Nigerian bank account and returns the registered account name. Use this before [Redeem Asset](/api-reference/redeem-asset) or [Update Bank Account](/api-reference/update-bank-account) to confirm the destination is correct.
The request body must be [encrypted](/guides/encryption).
### Body parameters (plain payload, pre-encryption)
CBN bank code, from [Get Banks](/api-reference/get-banks).
10-digit account number to resolve.
```json Plain body (encrypt before sending) theme={null}
{
"bankCode": "058",
"accountNumber": "0123456789"
}
```
```bash cURL (wire format) theme={null}
curl -X POST "https://api.cngn.co/v1/api/account/verify" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "",
"iv": ""
}'
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Account details verified successfully",
"data": {
"accountNumber": "0123456789",
"accountName": "ADA OBI",
"bankCode": "058"
}
}
```
```json 400 - Invalid account theme={null}
{
"status": 400,
"message": "Could not resolve account details"
}
```
# Verify Withdrawal
Source: https://docs.cngn.co/api-reference/verify-withdrawal
GET https://api.cngn.co/v1/api/withdraw/verify/{tnxRef}
Check the status of a withdrawal by its transaction reference
Fetches the full transaction record for a withdrawal, including its on-chain hash and current status. Use this to confirm settlement after calling [Withdraw](/api-reference/withdraw).
### Path parameters
The `trxRef` returned by [Withdraw](/api-reference/withdraw).
### Response (decrypted `data`)
Returns a full transaction object, the same shape as items in [Get Transactions](/api-reference/get-transactions).
```bash cURL theme={null}
curl -X GET "https://api.cngn.co/v1/api/withdraw/verify/WD-7f3a2b1c" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.cngn.co/v1/api/withdraw/verify/${trxRef}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
```
```python Python theme={null}
res = requests.get(
f"https://api.cngn.co/v1/api/withdraw/verify/{trx_ref}",
headers={"Authorization": f"Bearer {API_KEY}"},
)
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Transaction fetched successfully",
"data": {
"id": "9f8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"from": "Acme Ltd",
"receiver": { "address": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72" },
"amount": "25000.00",
"description": "Withdrawal to external wallet",
"createdAt": "2026-07-20T14:32:11.000Z",
"trx_ref": "WD-7f3a2b1c",
"trx_type": "withdrawal",
"network": "Base",
"asset_type": "credit_alphanum4",
"asset_symbol": "CNGN",
"base_trx_hash": "0x4a5b...e9f0",
"extl_trx_hash": null,
"explorer_link": "https://basescan.org/tx/0x4a5b...e9f0",
"status": "success"
}
}
```
```json 400 - Not found theme={null}
{
"status": 400,
"message": "Transaction not found"
}
```
# Whitelist Address
Source: https://docs.cngn.co/api-reference/whitelist-address
POST https://api.cngn.co/v1/api/whitelist
Whitelist an external wallet address for withdrawals
Registers an external wallet address against a network so it can receive withdrawals from your account.
The request body must be [encrypted](/guides/encryption).
### Body parameters (plain payload, pre-encryption)
Network the address belongs to. See [Get Networks](/api-reference/get-networks).
The wallet address to whitelist.
### Response (decrypted `data`)
Returns your updated list of whitelisted addresses.
Whitelist entry ID.
Network the address is whitelisted on.
The whitelisted wallet address.
Internal deposit address paired to this entry.
Network summary (`id`, `name`, `short_name`) when included.
ISO 8601 creation timestamp.
ISO 8601 last-update timestamp.
```json Plain body (encrypt before sending) theme={null}
{
"networkId": "9b2e6a1f-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"address": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72"
}
```
```bash cURL (wire format) theme={null}
curl -X POST "https://api.cngn.co/v1/api/whitelist" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "",
"iv": ""
}'
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Address whitelisted successfully",
"data": [
{
"id": "7a8b9c0d-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
"networkId": "9b2e6a1f-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"publicKey": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
"internalPublicKey": "0x3cD4e5F6a7B8c9D0e1F2a3B4c5D6e7F8a9B0c1D2",
"created_at": "2026-07-22T10:15:00.000Z",
"updated_at": "2026-07-22T10:15:00.000Z"
}
]
}
```
# Withdraw
Source: https://docs.cngn.co/api-reference/withdraw
POST https://api.cngn.co/v1/api/withdraw
Withdraw cNGN to an external wallet address
Sends cNGN from your business balance to an external wallet on a supported network.
Requires the **Send Crypto** permission. The request body must be [encrypted](/guides/encryption). The destination address may need to be [whitelisted](/api-reference/whitelist-address) first, depending on your account configuration.
### Body parameters (plain payload, pre-encryption)
Amount of cNGN to withdraw.
Destination wallet address on the target network.
Target network ID, from [Get Networks](/api-reference/get-networks).
Save the destination address for future withdrawals.
### Response (decrypted `data`)
Withdrawal transaction reference. Poll [Verify Withdrawal](/api-reference/verify-withdrawal) with this value to track status.
Destination address the withdrawal was sent to.
```json Plain body (encrypt before sending) theme={null}
{
"amount": 25000,
"address": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
"networkId": "9b2e6a1f-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"shouldSaveAddress": true
}
```
```bash cURL (wire format) theme={null}
curl -X POST "https://api.cngn.co/v1/api/withdraw" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "",
"iv": ""
}'
```
```json 200 - Success (data shown decrypted) theme={null}
{
"status": 200,
"message": "Withdrawal was successfully",
"data": {
"trxRef": "WD-7f3a2b1c",
"address": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72"
}
}
```
```json 403 - Permission denied theme={null}
{
"status": false,
"message": "Permission denied"
}
```
# Changelog
Source: https://docs.cngn.co/changelog
Notable changes and additions to the cNGN API and its documentation
## Celo support
cNGN is now deployed on Celo. The [Contract Addresses](/guides/contract-addresses) page
lists both the mainnet contract (`0xF6829D7393dAe24509eb1E52eE8e572e2E271a4f`) and the
testnet contract (`0xa188439ccCEe9A6aa0E842f9c17C1b00C7B4dd4D`).
Celo is an EVM chain, so withdrawals, bridging, and address whitelisting work exactly as
they do on the other EVM networks. Resolve its `networkId` per environment from
[Get Networks](/api-reference/get-networks); see the [Networks guide](/guides/networks).
## Full webhook event coverage
Every documented [webhook event](/guides/webhooks) is now delivered:
* **On-chain deposits** now fire `deposit.completed` with `trx_type: crypto_deposit`,
carrying both the source-network hash (`extl_trx_hash`) and the mint hash
(`base_trx_hash`). There is no preceding `deposit.received` for on-chain deposits.
* **Redemptions** now fire `redemption.completed` when the bank payout settles, and
`transaction.failed` (with `reason: "Provider failed to process payout"`) when it
does not. Redeemed cNGN is automatically reverted to your balance on payout failure.
* The [Webhooks guide](/guides/webhooks#sample-payloads-per-event) now documents exact
sample payloads for all five events, including the three `transaction.failed`
variants, plus a field-presence matrix.
## New endpoint: Bridge Quote
[`POST /bridge-quote`](/api-reference/bridge-quote) returns the receivable amount,
network fee, and bridge fee for a cross-network bridge before you commit to it with
[`POST /bridge`](/api-reference/bridge). Requires the **Swap** permission; the request
body follows the standard [encrypted wire format](/guides/encryption).
## New documentation site
Launch of this documentation site, covering the full third-party API surface:
* [Quickstart](/quickstart) and a [Going Live Checklist](/going-live)
* A dedicated [Security](/guides/security) tab: API keys, SSH keys, and the official
encryption/decryption implementations in TypeScript, Python, and PHP
* Reference pages for all endpoints with decrypted request and response examples
* [Webhooks](/guides/webhooks), [Contract Addresses](/guides/contract-addresses),
[Rate Limiting](/guides/rate-limits), and a full [Error Reference](/guides/errors)
# Going Live Checklist
Source: https://docs.cngn.co/going-live
Everything to verify before switching from test to production
Work through each phase in order before moving real funds. Most launch-day incidents trace back to a skipped item on this list.
Complete your business verification with the cNGN team:
* [ ] Complete the Know-Your-Business (KYB) process
* [ ] Submit all required business documents
* [ ] Complete identity verification for account owners
* [ ] Pay the associated onboarding fees
Onboarding must be fully approved before live API keys can be issued. Start this early; document review takes time.
Configure your merchant dashboard for day-to-day operations:
* [ ] Set up admin users with role-based access (see [Permissions](/guides/permissions))
* [ ] Whitelist every withdrawal address for authorized crypto transfers ([Whitelist Address](/api-reference/whitelist-address))
* [ ] Add bank accounts that match the registered KYB entity ([Update Bank Account](/api-reference/update-bank-account))
* [ ] Enable multi-factor authentication for all dashboard users
Verify your integration end-to-end in the test environment:
* [ ] Add all server IP addresses to the IP whitelist ([Authentication](/guides/authentication#ip-whitelisting))
* [ ] Generate an Ed25519 SSH key pair ([Quickstart](/quickstart#3-upload-your-ed25519-public-key))
* [ ] Upload the SSH public key to the merchant dashboard for the live environment
* [ ] Generate live API and encryption keys, and store them in a secrets manager
* [ ] Integrate request encryption and response decryption ([Encryption](/guides/encryption)) or an [official SDK](/sdks)
* [ ] Configure webhook URLs, set a signing secret, and verify signature validation ([Webhooks](/guides/webhooks))
* [ ] Test deposits, withdrawals, redemptions, and bridges with your `cngn_test` key
* [ ] Switch the deployment from the test API key to the live API key
The live environment has its own credentials: API key, encryption key, and SSH key slot. Repeat the key setup for live; test credentials will not work.
Confirm your regulatory controls are active:
* [ ] Ensure alignment with anti-money laundering (AML) policies
* [ ] Ensure alignment with counter-terrorism financing (CFT) policies
* [ ] Enable real-time monitoring dashboards
* [ ] Set up alerts for suspicious activity
Prepare your team and your customers:
* [ ] Set up customer support channels
* [ ] Notify internal teams about launch procedures
* [ ] Update your website and prepare FAQ documentation
Stay close to the integration during the first weeks:
* [ ] Monitor first-day transactions for issues ([Get Transactions](/api-reference/get-transactions))
* [ ] Conduct weekly check-ins with the onboarding team
* [ ] Establish sub-admin roles for transaction approvals as needed
All boxes ticked? You're ready for production. If anything behaves unexpectedly after switching to live keys, start with the [Error Reference](/guides/errors).
# Authentication
Source: https://docs.cngn.co/guides/authentication
API keys, environments, and IP whitelisting
Every request to the cNGN API must be authenticated with an API key sent as a Bearer token:
```bash theme={null}
Authorization: Bearer cngn_live_xxxxxxxxxxxxx
```
## Environments
The API key prefix determines the environment. Both environments use the same base URL (`https://api.cngn.co/v1/api`):
| Environment | Key prefix | Behaviour |
| ----------- | ----------- | ----------------------------------------------- |
| **Test** | `cngn_test` | Sandbox data; safe for integration testing |
| **Live** | `cngn_live` | Production; real funds and real bank settlement |
Each environment has its own independent set of credentials:
* **API key**: identifies your business
* **Encryption key**: AES key used to encrypt request bodies
* **Ed25519 SSH public key**: used by the API to encrypt response payloads to you
A key from one environment never works against the other's data. Using a token without a recognised prefix fails with `Invalid token prefix`.
## IP whitelisting
After the API key check, the request's source IP is validated against your whitelist. Requests from non-whitelisted addresses are rejected:
```json theme={null}
{
"status": 403,
"message": "IP address not whitelisted"
}
```
Manage the whitelist from your merchant dashboard. Remember to add every egress IP your infrastructure uses (load balancers, NAT gateways, serverless egress ranges).
## Authentication errors
| Message | Cause |
| ------------------------------------------------- | --------------------------------------------------- |
| `No token provided` | Missing `Authorization` header |
| `Invalid token prefix` | Key doesn't start with `cngn_test` or `cngn_live` |
| `Merchant not found` | Key not recognised (revoked or mistyped) |
| `No Test SSH Key found` / `No Live SSH Key found` | No Ed25519 public key uploaded for that environment |
| `IP address not whitelisted` | Request came from a non-whitelisted IP |
## Key rotation and safety
* Store keys in a secrets manager, never in code or client-side bundles.
* Rotate keys immediately if you suspect exposure: generate a new key in the dashboard and update your deployment.
* Use test keys everywhere except production infrastructure.
# Contract Addresses
Source: https://docs.cngn.co/guides/contract-addresses
Official cNGN token contracts on every supported network
cNGN is deployed as a token contract on each supported blockchain. Use the addresses below when adding cNGN to wallets, indexing transfers, or interacting with the token on-chain.
Only trust contract addresses published here or on the official [cNGN website](https://cngn.co). Scammers deploy look-alike tokens; always verify the full address, never just the ticker.
## Token contracts
| Network | Type | Contract address |
| ----------- | ------------- | ---------------------------------------------------------- |
| Bantu | Stellar-based | `GD6G2NT7CQHPIYHA52KZHWB6ONNWTSGZOOLTRLRASENM2VWSF6CHYFRX` |
| Asset Chain | EVM | `0x7923C0f6FA3d1BA6EAFCAedAaD93e737Fd22FC4F` |
| BNB Chain | EVM | `0xa8AEA66B361a8d53e8865c62D142167Af28Af058` |
| Ethereum | EVM | `0x17CDB2a01e7a34CbB3DD4b83260B05d0274C8dab` |
| Polygon | EVM | `0x52828daa48C1a9A06F37500882b42daf0bE04C3B` |
| Base | EVM | `0x46C85152bFe9f96829aA94755D9f915F9B10EF5F` |
| Solana | SPL | `3jiqwBQVRC5zRwHyqvnkQurebJ5RNxg3F5fXMwaxgkv8` |
| Lisk | EVM | `0xC7aB2C35Ea37236e644C24A4E4a1911c082887c0` |
| Celo | EVM | `0xF6829D7393dAe24509eb1E52eE8e572e2E271a4f` |
| Monad | EVM | Not yet deployed |
| Arc | EVM | Not yet deployed |
| Network | Type | Contract address |
| ----------- | ------------- | ---------------------------------------------------------- |
| Bantu | Stellar-based | `GAE7E56N3XIC6JGJI54SD3VN4EDY3OZVFA7CLHXAMMTHLU4LIFYJMFSI` |
| Asset Chain | EVM | `0x00F0a33d9AFaC108A4963D4Cb4Ef6A9C6B8D8859` |
| BNB Chain | EVM | `0x8a078b182bA9649c03982c2a80CDcc81cdc99dA8` |
| Ethereum | EVM | `0xF55E56423e6b50808fD07cB62b6A32B91903f50E` |
| Polygon | EVM | `0xf24B1Cee8cA70341FcefBCa10e7e4Db9A4896486` |
| Base | EVM | `0xEFdF04BAfE0ebabb5F5cD9e3f36564f51CFe1530` |
| Solana | SPL | `HfJWS8vJHvxKn5xW3uLXkTmEy4jny3G45QnS1Eab5sg` |
| Lisk | EVM | `0x999E3A32eF3F9EAbF133186512b5F29fADB8a816` |
| Celo | EVM | `0xa188439ccCEe9A6aa0E842f9c17C1b00C7B4dd4D` |
| Monad | EVM | `0x4F90098BA5b08ABAf039b95A851F8e764EB84b49` |
| Arc | EVM | `0x1716Df6A18DcFF031BFD209aDB8035174AdC0D31` |
## Good to know
A testnet address holds no real value and will not work on mainnet (and vice versa). Match the contract to the environment of your API key: `cngn_test` integrations should interact with testnet contracts only.
A contract existing on a chain does not automatically mean withdrawals and bridging are enabled for it. The authoritative list for API operations is always [Get Networks](/api-reference/get-networks); see the [Networks guide](/guides/networks).
# Encryption
Source: https://docs.cngn.co/guides/encryption
Encrypt request bodies and decrypt response payloads
The cNGN API encrypts payloads in both directions:
* **Requests** (`POST`/`PUT` bodies): you encrypt the JSON payload with **AES-256-CBC** using the encryption key from your dashboard.
* **Responses**: the API encrypts the `data` field of every successful response to your **Ed25519 public key**. You decrypt it locally with your private key.
The official [SDKs](/sdks) implement both sides of this automatically. Read on only if you're integrating directly over HTTP.
## Encrypting requests (AES-256-CBC)
Every endpoint that accepts a body expects this wire format instead of the plain JSON:
```json theme={null}
{
"content": "",
"iv": ""
}
```
Steps:
1. SHA-256 hash your encryption key to derive a 32-byte AES key.
2. Generate a random 16-byte initialization vector (IV); use a fresh one per request.
3. Encrypt the JSON string with AES-256-CBC.
4. Base64-encode the ciphertext and the IV, and send them as `content` and `iv`.
The official `AESCrypto` implementations (the `decrypt` method is included for local round-trip testing):
```typescript TypeScript theme={null}
import {Buffer} from 'buffer';
import * as crypto from 'crypto';
export type AESEncryptionResponse = {
iv: string,
content: string
}
export class AESCrypto {
private static readonly ALGORITHM = 'aes-256-cbc';
private static readonly IV_LENGTH = 16;
private static readonly KEY_LENGTH = 32; // 256 bits
private static prepareKey(key: string): Buffer {
// Hash the key to ensure it's always the correct length
const hash = crypto.createHash('sha256');
hash.update(key);
return hash.digest();
}
public static encrypt(data: string, key: string): AESEncryptionResponse {
const iv = crypto.randomBytes(this.IV_LENGTH);
const keyBuffer = this.prepareKey(key);
const cipher = crypto.createCipheriv(this.ALGORITHM, keyBuffer, iv);
let encrypted = cipher.update(data, 'utf8', 'base64');
encrypted += cipher.final('base64');
return {
content: encrypted,
iv: iv.toString('base64')
};
}
public static decrypt(encryptedData: AESEncryptionResponse, key: string): string {
const iv = Buffer.from(encryptedData.iv, 'base64');
const keyBuffer = this.prepareKey(key);
const decipher = crypto.createDecipheriv(this.ALGORITHM, keyBuffer, iv);
let decrypted = decipher.update(encryptedData.content, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
```
```python Python theme={null}
# You have to install cryptography
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
import os
import base64
class AESCrypto:
ALGORITHM = algorithms.AES
IV_LENGTH = 16
KEY_LENGTH = 32 # 256 bits
@staticmethod
def prepare_key(key: str) -> bytes:
# Hash the key using SHA-256 to ensure it's always the correct length (32 bytes)
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(key.encode('utf-8'))
return digest.finalize()
@staticmethod
def encrypt(data: str, key: str) -> dict:
# Generate a random Initialization Vector (IV)
iv = os.urandom(AESCrypto.IV_LENGTH)
key_buffer = AESCrypto.prepare_key(key)
# Create cipher and encrypt the data
cipher = Cipher(AESCrypto.ALGORITHM(key_buffer), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
# Pad data to be multiple of 16 bytes (block size for AES)
padding_length = 16 - (len(data) % 16)
padded_data = data + chr(padding_length) * padding_length
encrypted = encryptor.update(padded_data.encode('utf-8')) + encryptor.finalize()
# Return the encrypted content and the IV (both base64 encoded)
return {
'content': base64.b64encode(encrypted).decode('utf-8'),
'iv': base64.b64encode(iv).decode('utf-8')
}
@staticmethod
def decrypt(encrypted_data: dict, key: str) -> str:
# Decode the base64 encoded IV and content
iv = base64.b64decode(encrypted_data['iv'])
encrypted_content = base64.b64decode(encrypted_data['content'])
key_buffer = AESCrypto.prepare_key(key)
# Create cipher and decrypt the data
cipher = Cipher(AESCrypto.ALGORITHM(key_buffer), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
decrypted = decryptor.update(encrypted_content) + decryptor.finalize()
# Remove padding
padding_length = decrypted[-1]
decrypted = decrypted[:-padding_length]
return decrypted.decode('utf-8')
```
```php PHP theme={null}
base64_encode($encrypted),
'iv' => base64_encode($iv)
];
}
public static function decrypt(array $encryptedData, string $key): string
{
// Decode the base64 encoded IV and content
$iv = base64_decode($encryptedData['iv']);
$encryptedContent = base64_decode($encryptedData['content']);
$keyBuffer = self::prepareKey($key);
// Decrypt the data
$decrypted = openssl_decrypt($encryptedContent, self::ALGORITHM, $keyBuffer, OPENSSL_RAW_DATA, $iv);
return $decrypted;
}
}
```
Usage: `AESCrypto.encrypt(JSON.stringify(payload), encryptionKey)` returns the `{content, iv}` object to send as the request body.
Sending a plain JSON body to a `POST`/`PUT` endpoint fails with `400 Missing encryption data, key, or IV`. A body that can't be decrypted fails with `400 Decryption failed`.
## Decrypting responses (Ed25519 / Curve25519)
Successful responses return an encrypted base64 string in `data`:
```json theme={null}
{
"status": 200,
"message": "Balance fetched successfully",
"data": "kJ8vX2mN...=="
}
```
The payload is encrypted with libsodium's `crypto_box` to your Ed25519 public key (converted to Curve25519). The base64-decoded blob is laid out as:
| Bytes | Content |
| ------------ | -------------------- |
| 0 to 23 | 24-byte nonce |
| 24 to (n-32) | ciphertext |
| last 32 | ephemeral public key |
The official `Ed25519Crypto` implementations take your OpenSSH-format private key (the `cngn_api_key` file) directly; they locate the 64-byte key data after the `0x00 0x00 0x00 0x40` length marker, convert it to Curve25519, and open the box:
```typescript TypeScript theme={null}
import sodium from 'libsodium-wrappers';
import {Buffer} from 'buffer';
export class Ed25519Crypto {
private static isInitialized = false;
private static async initialize() {
if (!this.isInitialized) {
await sodium.ready;
this.isInitialized = true;
}
}
private static parseOpenSSHPrivateKey(privateKey: string): Uint8Array {
const lines = privateKey.split('\n');
const base64PrivateKey = lines.slice(1, -1).join('');
const privateKeyBuffer = Buffer.from(base64PrivateKey, 'base64');
const keyDataStart = privateKeyBuffer.indexOf(Buffer.from([0x00, 0x00, 0x00, 0x40]));
if (keyDataStart === -1) {
throw new Error('Unable to find Ed25519 key data');
}
return new Uint8Array(privateKeyBuffer.subarray(keyDataStart + 4, keyDataStart + 68));
}
public static async decryptWithPrivateKey(ed25519PrivateKey: string, encryptedData: string): Promise {
await this.initialize();
try {
const fullPrivateKey = this.parseOpenSSHPrivateKey(ed25519PrivateKey);
const curve25519PrivateKey = sodium.crypto_sign_ed25519_sk_to_curve25519(fullPrivateKey);
const encryptedBuffer = Buffer.from(encryptedData, 'base64');
const nonce = encryptedBuffer.subarray(0, sodium.crypto_box_NONCEBYTES);
const ephemeralPublicKey = encryptedBuffer.subarray(-sodium.crypto_box_PUBLICKEYBYTES);
const ciphertext = encryptedBuffer.subarray(sodium.crypto_box_NONCEBYTES, -sodium.crypto_box_PUBLICKEYBYTES);
const decrypted = sodium.crypto_box_open_easy(
ciphertext,
nonce,
ephemeralPublicKey,
curve25519PrivateKey
);
return sodium.to_string(decrypted)
} catch (error) {
throw new Error(`Failed to decrypt with the provided Ed25519 private key: ${error}`);
}
}
}
```
```python Python theme={null}
# You have to install pynacl, cryptography
from nacl.public import PrivateKey, PublicKey, Box
from nacl.encoding import Base64Encoder
from nacl.bindings import crypto_sign_ed25519_sk_to_curve25519
import base64
import re
class Ed25519Crypto:
is_initialized = False
@staticmethod
def initialize():
if not Ed25519Crypto.is_initialized:
# No explicit initialization is required for PyNaCl, but we keep this as a safeguard.
Ed25519Crypto.is_initialized = True
@staticmethod
def parse_openssh_private_key(private_key: str) -> bytes:
"""
Parses an OpenSSH Ed25519 private key to extract the raw private key.
:param private_key: The OpenSSH private key string.
:return: The 64-byte Ed25519 private key as bytes.
"""
# Remove the key header/footer and decode the base64 content
private_key_stripped = re.sub(r'-----.* PRIVATE KEY-----', '', private_key).strip()
private_key_stripped = re.sub(r"\s+", '', private_key_stripped)
private_key_buffer = base64.b64decode(private_key_stripped)
# Look for Ed25519 key data (00 00 00 40)
key_data_start = private_key_buffer.find(b'\x00\x00\x00\x40')
if key_data_start == -1:
raise Exception('Unable to find Ed25519 key data')
# The key starts after 0x00 0x00 0x00 0x40 (64-byte key length marker)
return private_key_buffer[key_data_start + 4: key_data_start + 68]
@staticmethod
def decrypt_with_private_key(ed25519_private_key: str, encrypted_data: str) -> str:
"""
Decrypts data using an Ed25519 private key (converted to Curve25519).
:param ed25519_private_key: The OpenSSH Ed25519 private key string.
:param encrypted_data: The encrypted data in base64 format.
:return: The decrypted plaintext as a string.
"""
Ed25519Crypto.initialize()
try:
# Parse the OpenSSH private key format and extract the Ed25519 private key
ed25519_private_key_bytes = Ed25519Crypto.parse_openssh_private_key(ed25519_private_key)
# Convert Ed25519 private key to Curve25519 private key for use with Box
curve25519_private_key_bytes = crypto_sign_ed25519_sk_to_curve25519(ed25519_private_key_bytes)
private_key = PrivateKey(curve25519_private_key_bytes)
# Decode the base64-encoded encrypted data
encrypted_buffer = base64.b64decode(encrypted_data)
# Extract nonce (24 bytes), ephemeral public key (32 bytes), and ciphertext
nonce = encrypted_buffer[:24]
ephemeral_public_key = PublicKey(encrypted_buffer[-32:])
ciphertext = encrypted_buffer[24:-32]
# Create a Box with the recipient's Curve25519 private key and the sender's ephemeral public key
box = Box(private_key, ephemeral_public_key)
# Decrypt the ciphertext
decrypted = box.decrypt(ciphertext, nonce)
return decrypted.decode('utf-8')
except Exception as e:
raise Exception("Failed to decrypt with the provided Ed25519 private key: " + str(e))
```
```php PHP theme={null}
getMessage());
}
}
}
```
Usage: `Ed25519Crypto.decryptWithPrivateKey(privateKeyFileContents, response.data)` returns the decrypted JSON string; parse it to get the payload.
## Key management summary
| Key | Direction | Where it lives |
| ----------------------------------------- | ------------------------------- | ----------------------------- |
| API key (`cngn_test...` / `cngn_live...`) | Request auth | Your server + dashboard |
| Encryption key (AES) | Request body encryption | Your server + dashboard |
| Ed25519 public key | Response encryption (API-side) | Uploaded to dashboard |
| Ed25519 private key | Response decryption (your side) | Your server only; never share |
# Error Reference
Source: https://docs.cngn.co/guides/errors
Every error the API returns and how to resolve it
Errors are returned with an HTTP status code and a message:
```json theme={null}
{
"status": 400,
"message": "Decryption failed"
}
```
## Authentication & access errors
| HTTP | Message | Cause | Fix |
| ---- | ------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------- |
| 400 | `No token provided` | Missing `Authorization` header | Send `Authorization: Bearer ` |
| 400 | `Invalid token prefix` | Key doesn't start with `cngn_test`/`cngn_live` | Check you copied the full key |
| 400 | `Merchant not found` | API key not recognised | Regenerate the key in the dashboard |
| 400 | `No Test SSH Key found` / `No Live SSH Key found` | No Ed25519 public key uploaded for this environment | Upload your public key in the dashboard |
| 403 | `IP address not whitelisted` | Request came from a non-whitelisted IP | Add your server IP in dashboard security settings |
| 403 | `Could not determine client IP address` | Source IP could not be resolved | Check proxy configuration (`X-Forwarded-For`) |
| 403 | `Permission denied` | Your role lacks the required permission | See [Permissions](/guides/permissions) |
| 404 | `Merchant not found` | Business role could not be resolved | Contact support |
## Encryption errors
| HTTP | Message | Cause | Fix |
| ---- | ------------------------------------- | -------------------------------------------------- | ------------------------------------------------------ |
| 400 | `Missing encryption data, key, or IV` | Body sent as plain JSON instead of `{content, iv}` | Encrypt the body; see [Encryption](/guides/encryption) |
| 400 | `Decryption failed` | Wrong AES key, malformed base64, or corrupted IV | Verify the encryption key and IV generation |
## Validation errors
Request bodies are validated after decryption. Validation failures return `400` with a `field: message` string, for example:
```json theme={null}
{
"status": 400,
"message": "amount: Number must be greater than or equal to 1"
}
```
## Rate limiting
| HTTP | Message | Fix |
| ---- | -------------------------------------------- | --------------------------------------------------------- |
| 429 | `Too many requests. Please try again later.` | Wait 60 seconds; see [Rate Limiting](/guides/rate-limits) |
## Service availability
| HTTP | Message | Cause |
| ---- | ----------------------------------------------------------- | --------------------------------------------------------------- |
| 400 | `Service is currently unavailable. Please try again later.` | Redeem or withdrawal service temporarily disabled platform-wide |
## Business-logic errors
Endpoint-specific failures (insufficient balance, invalid bank account, unsupported network, etc.) also return `400` with a descriptive message. Treat any unrecognised `400` message as non-retryable without changing the request.
**Retry guidance:** retry `429` (after the block window) and `5xx` with exponential backoff. Do not blindly retry `400` responses on money-moving endpoints. Verify state first with [Verify Withdrawal](/api-reference/verify-withdrawal) or [Get Transactions](/api-reference/get-transactions) to avoid duplicate transfers.
# Networks
Source: https://docs.cngn.co/guides/networks
Working with supported blockchain networks
cNGN is issued on multiple blockchain networks. Endpoints that move value on-chain ([Withdraw](/api-reference/withdraw), [Bridge](/api-reference/bridge), and [Whitelist Address](/api-reference/whitelist-address)) identify the target chain with a **`networkId`**.
## Getting network IDs
Network IDs are environment-specific database identifiers, not fixed enums. Always fetch the current list at runtime or integration time from [Get Networks](/api-reference/get-networks):
```bash theme={null}
curl -X GET "https://api.cngn.co/v1/api/networks" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Decrypted response payload:
```json theme={null}
[
{
"id": "9b2e6a1f-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"name": "Base",
"short_name": "BASE",
"isDisabled": false,
"blockchain": null
},
{
"id": "1f2e3d4c-5b6a-7988-9a0b-c1d2e3f4a5b6",
"name": "Polygon",
"short_name": "POLYGON",
"isDisabled": false,
"blockchain": null
}
]
```
Use the `id` value as `networkId` in subsequent requests. Pass `?includeBlockchain=true` to include underlying blockchain metadata in the response.
## Supported chains
cNGN supports EVM chains (Ethereum, BNB Smart Chain, Polygon, Base, Asset Chain, Lisk, Celo), Solana, Tron, and Bantu (XBN). The authoritative list for your environment is always the [Get Networks](/api-reference/get-networks) response. Networks with `isDisabled: true` cannot be used for withdrawals or bridging.
Never hard-code network IDs across environments; test and live environments have different IDs for the same chain. Resolve IDs per environment.
## Contract addresses
Verified cNGN token contract addresses for every chain, mainnet and testnet, are listed on the [Contract Addresses](/guides/contract-addresses) page. Always verify contract addresses from the official source before interacting on-chain.
# Permissions
Source: https://docs.cngn.co/guides/permissions
Role permissions required by certain endpoints
Some endpoints require your merchant account's role to hold a specific permission. Permissions are managed by your organisation's administrator in the merchant dashboard.
If your role lacks the required permission, the API responds with:
```json theme={null}
{
"status": false,
"message": "Permission denied"
}
```
## Permission map
| Permission | Required by |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Fiat Deposit** | [Get Virtual Account](/api-reference/get-virtual-account), [Create Temporary Virtual Account](/api-reference/create-temporary-virtual-account) |
| **Redeem** | [Redeem Asset](/api-reference/redeem-asset) |
| **Send Crypto** | [Withdraw](/api-reference/withdraw) |
| **Swap** | [Bridge Quote](/api-reference/bridge-quote), [Bridge](/api-reference/bridge) |
All other endpoints only require a valid API key and whitelisted IP.
## Service availability
Redemption and withdrawal endpoints are additionally subject to platform-level service controls. If a service has been temporarily disabled (for example during an incident), requests fail with:
```json theme={null}
{
"status": 400,
"message": "Service is currently unavailable. Please try again later."
}
```
This is transient; retry later or check the status page.
# Rate Limiting
Source: https://docs.cngn.co/guides/rate-limits
Request quotas and how to handle 429 responses
The cNGN API rate-limits requests **per API key**, so each merchant has an independent budget regardless of source IP.
| Limit | Value |
| ---------------------------- | --------------------------------- |
| Requests | **20 per 60 seconds** per API key |
| Block duration when exceeded | 60 seconds |
## When you exceed the limit
The API responds with HTTP `429`:
```json theme={null}
{
"status": 429,
"message": "Too many requests. Please try again later."
}
```
Once blocked, all requests with that key are rejected for 60 seconds.
## Best practices
* **Back off on 429**: wait at least 60 seconds before retrying; retrying earlier extends nothing and wastes quota.
* **Cache stable data**: bank lists ([Get Banks](/api-reference/get-banks)) and supported networks ([Get Networks](/api-reference/get-networks)) change rarely; cache them for hours, not seconds.
* **Batch reads**: use pagination with a higher `limit` on [Get Transactions](/api-reference/get-transactions) instead of many small pages.
* **Poll responsibly**: for withdrawal status, poll [Verify Withdrawal](/api-reference/verify-withdrawal) with exponential backoff rather than a tight loop.
# Response Format
Source: https://docs.cngn.co/guides/response-format
The standard response envelope returned by every endpoint
## Success envelope
Every successful call returns HTTP `200` with this structure:
```json theme={null}
{
"status": 200,
"message": "Balance fetched successfully",
"data": ""
}
```
| Field | Type | Description |
| --------- | ------ | ------------------------------------------------------------------------------------------------ |
| `status` | number | HTTP status code |
| `message` | string | Human-readable summary of the result |
| `data` | string | The response payload, encrypted to your Ed25519 public key; see [Encryption](/guides/encryption) |
Throughout the API Reference, response examples show the **decrypted** contents of `data` so you can see the actual payload shape. On the wire, `data` is always an encrypted base64 string.
## Error envelope
Errors return the status and message with no `data` field:
```json theme={null}
{
"status": 400,
"message": "Invalid api key"
}
```
Permission failures use a slightly different shape:
```json theme={null}
{
"status": false,
"message": "Permission denied"
}
```
See the [Error Reference](/guides/errors) for the full list.
## Pagination
List endpoints such as [Get Transactions](/api-reference/get-transactions) return paginated data:
```json theme={null}
{
"data": [ ... ],
"pagination": {
"count": 42,
"pages": 5,
"isLastPage": false,
"nextPage": 2,
"previousPage": null
}
}
```
| Field | Type | Description |
| -------------- | -------------- | ------------------------------------------------- |
| `count` | number | Total number of records |
| `pages` | number | Total number of pages |
| `isLastPage` | boolean | Whether the current page is the last |
| `nextPage` | number \| null | Next page number, or `null` on the last page |
| `previousPage` | number \| null | Previous page number, or `null` on the first page |
# Security
Source: https://docs.cngn.co/guides/security
How the cNGN API protects every request, and how to handle your credentials
All communication between your application and the cNGN platform is protected end-to-end: AES-256-CBC encryption for request payloads and Ed25519 public-key encryption for response data, on top of API-key authentication and IP whitelisting.
Safeguard your **encryption key** and **Ed25519 private key** at all times. Never expose them in client-side code, mobile apps, or public repositories. Anyone holding them can read your API traffic.
## The three security layers
A Bearer token identifies your business and selects the test or live environment.
Requests are only accepted from server IPs you have whitelisted.
Request bodies and response data are encrypted in both directions.
## Setting up secure communication
Retrieve the auto-generated keys from your merchant dashboard:
* **API key** (`cngn_test...` or `cngn_live...`): sent as your Bearer token
* **Encryption key**: used to AES-encrypt request bodies
Store both in a secrets manager immediately; treat them like passwords.
Step-by-step dashboard walkthrough.
```bash theme={null}
ssh-keygen -t ed25519 -C "api@yourcompany.com" -f cngn_api_key
```
This creates two files:
| File | Role | Handling |
| ------------------ | ------------------------------------- | ------------------------ |
| `cngn_api_key` | Private key, decrypts API responses | Keep on your server only |
| `cngn_api_key.pub` | Public key, encrypts responses to you | Upload to the dashboard |
Key generation and clipboard commands for macOS, Linux, and Windows.
Upload `cngn_api_key.pub` to the SSH key slot for the matching environment. Each environment (test and live) has its own slot; requests fail with `No Test SSH Key found` or `No Live SSH Key found` if the slot is empty.
Wire the crypto into your integration, or let an [official SDK](/sdks) do it for you:
* Encrypt every `POST`/`PUT` body into the `{content, iv}` format
* Decrypt the `data` field of every response with your private key
Full code for both directions is in the [Encryption guide](/guides/encryption).
## Credential handling rules
| Credential | Share with cNGN? | Commit to git? | Client-side? |
| ------------------- | --------------------------- | -------------- | ------------ |
| API key | Never (dashboard issues it) | Never | Never |
| Encryption key | Never (dashboard issues it) | Never | Never |
| Ed25519 private key | Never | Never | Never |
| Ed25519 public key | Yes, via dashboard upload | Acceptable | Acceptable |
## Best practices
Use a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) rather than environment files checked into repositories. Scope read access to the services that call the API.
If a key may have leaked, or someone with access leaves the team, regenerate the API and encryption keys in the dashboard and replace the SSH key pair. Old keys stop working the moment new ones are issued.
Whitelist only the egress IPs that actually call the API, and remove entries when infrastructure is decommissioned. A short list limits the blast radius of a leaked key, since requests from other IPs are rejected.
Never point production services at `cngn_test` keys or vice versa. Keep the two credential sets in separate secret scopes so a misconfiguration cannot cross environments.
Watch your [transaction history](/api-reference/get-transactions) for transfers you did not initiate, and alert on repeated `403` or `Decryption failed` responses, which can indicate probing.
Keys generated, public key uploaded, secrets locked away? Continue to the [Encryption guide](/guides/encryption) to wire up the payload crypto, then verify everything with the [Going Live Checklist](/going-live).
# Generating API Keys
Source: https://docs.cngn.co/guides/security/generating-api-keys
Retrieve your API and encryption keys from the merchant dashboard
Your API key and encryption key are auto-generated by the platform. This guide shows where to find them and how to complete the key setup.
Log in to your merchant dashboard and navigate to **Settings** from the sidebar menu.
Select the **API Key** tab from the tab menu at the top of the settings page. Both your **API Key** and **Encryption Key** are already generated and displayed there:
* Click the **eye icon** to reveal a key
* Click the **clipboard icon** to copy it
Each environment has its own key set. Test keys start with `cngn_test` and live keys with `cngn_live`; make sure you are viewing the environment you intend to integrate. See [Authentication](/guides/authentication#environments).
On the same API Key tab, paste your **SSH public key** into the designated field. The platform uses it to encrypt response data to you; without it, requests fail with `No Test SSH Key found` or `No Live SSH Key found`.
Don't have a key pair yet? Create one first: [Generating SSH Keys](/guides/security/generating-ssh-keys).
Move both keys into a secrets manager immediately.
Never expose the API key or encryption key in client-side code, mobile apps, or public repositories. See the [credential handling rules](/guides/security#credential-handling-rules).
## What each key does
| Key | Purpose | Where it's used |
| -------------- | ------------------------------------------ | ----------------------------------------------- |
| API Key | Authenticates every request | `Authorization: Bearer ` header |
| Encryption Key | Encrypts request bodies (AES-256-CBC) | Your server, before sending `POST`/`PUT` bodies |
| SSH Public Key | Lets the platform encrypt responses to you | Uploaded to the dashboard |
Next: wire up the payload crypto in [Encrypting & Decrypting Requests](/guides/encryption).
# Generating SSH Keys
Source: https://docs.cngn.co/guides/security/generating-ssh-keys
Create the Ed25519 key pair used to encrypt API responses to you
The cNGN API encrypts every response payload to an **Ed25519 public key** you upload to your dashboard. This guide creates the key pair and copies the public key, on any operating system.
```bash theme={null}
ssh-keygen -t ed25519 -C "your_email@example.com"
```
The command prompts you for:
* **Save location**: press Enter to accept the default (`~/.ssh/id_ed25519`), or provide a dedicated path such as `~/.ssh/cngn_api_key`
* **Passphrase**: optional; press Enter to skip
This produces two files: the private key (`id_ed25519`) and the public key (`id_ed25519.pub`).
If you set a passphrase, your integration needs it to load the private key when decrypting responses. For server-side automation, most teams skip the passphrase and rely on filesystem permissions and secret management instead.
```bash theme={null}
cat ~/.ssh/id_ed25519.pub | pbcopy
```
```bash theme={null}
cat ~/.ssh/id_ed25519.pub | xclip -selection clipboard
```
Or simply print it and copy manually:
```bash theme={null}
cat ~/.ssh/id_ed25519.pub
```
```powershell theme={null}
Get-Content ~\.ssh\id_ed25519.pub | Set-Clipboard
```
Paste the copied public key into the SSH public key field on the API Key tab of your dashboard settings. Full walkthrough: [Generating API Keys](/guides/security/generating-api-keys).
## Public vs. private key
| File | Can be shared? | Role |
| ------------------------- | ------------------------------- | ------------------------------------------- |
| `id_ed25519.pub` (public) | Yes, upload to the dashboard | The platform encrypts responses with it |
| `id_ed25519` (private) | Never, keep on your server only | Your integration decrypts responses with it |
Your private key must remain protected at all times and never be disclosed to anyone. If it's ever exposed, generate a new pair and update the dashboard immediately.
Next: use the private key to decrypt responses in [Encrypting & Decrypting Requests](/guides/encryption).
# Webhooks
Source: https://docs.cngn.co/guides/webhooks
Receive real-time notifications when deposits, redemptions, withdrawals, and bridges settle
Instead of polling [Get Transactions](/api-reference/get-transactions), let cNGN notify your server the moment a transaction changes state. Webhooks are how your systems learn that a customer's deposit landed, a redemption paid out, or a withdrawal failed.
Webhook URLs, event subscriptions, and the signing secret are managed in your **merchant dashboard**, not through the third-party API. Each environment has its own webhook URL: one for test and one for live.
## Setting up
In the dashboard security settings, set the HTTPS endpoint that should receive events. Configure the test URL first and verify your handler against test transactions before setting the live URL.
Add a signing secret in the dashboard. cNGN uses it to sign every delivery so your server can verify the event genuinely came from cNGN. Without a secret configured, deliveries arrive unsigned.
Choose which of the five event types (below) your endpoint should receive. Subscribe only to what you act on; you can change subscriptions at any time.
Validate the signature, queue the event for processing, and return a `2xx` immediately. Do the heavy work asynchronously.
## Event types
| Event | Fires when |
| ---------------------- | ---------------------------------------------------------------------------------------------------------- |
| `deposit.received` | A fiat payment has landed and is awaiting mint approval. Funds are confirmed but cNGN is not yet credited. |
| `deposit.completed` | A deposit (fiat or on-chain) has been fully processed and cNGN credited to your balance. |
| `redemption.completed` | A redemption has settled; Naira has been paid out to the bank account. |
| `withdrawal.completed` | An on-chain withdrawal has been confirmed on the network. |
| `transaction.failed` | Any transaction has failed or been rejected. The `reason` field explains why. |
`deposit.received` followed by `deposit.completed` is the normal lifecycle of a fiat deposit. Credit your customer only on `deposit.completed`.
## Delivery payload
Every delivery is an HTTPS `POST` with the same JSON envelope:
```json theme={null}
{
"event": "",
"data": { "...": "transaction snapshot, fields vary by event" },
"timestamp": "2026-07-22T14:32:12.104Z"
}
```
One of the five event types above.
The transaction snapshot at the moment of the state change. Common fields:
Unique transaction ID. Use it for idempotency.
Transaction reference, the same value shown in [Get Transactions](/api-reference/get-transactions).
Your business ID.
User that initiated the transaction.
`pending`, `completed`, `failed`, or `rejected`.
Underlying type: `fiat_buy`, `crypto_deposit`, `fiat_redeem`, `enaira_redeem`, or `withdraw`.
Network the transaction executed on. Present when the event involves an on-chain step.
On-chain hash on the origin network, when applicable.
On-chain hash on the destination network, when applicable.
Block explorer URL, when a hash exists.
Transaction amount as a decimal string.
`NGN` for fiat-side events, `CNGN` for on-chain events.
Recipient. Format varies by event; see the samples below.
Failure reason. Present on `transaction.failed` when a cause is known.
ISO 8601 timestamp of the state change.
ISO 8601 time the webhook was dispatched.
## Sample payloads per event
Fires when a fiat payment lands in your virtual account and is awaiting mint approval. No on-chain step has happened yet, so there is no `network` or hash, the `asset_symbol` is `NGN`, and `receiver` is your business ID:
```json theme={null}
{
"event": "deposit.received",
"data": {
"transactionId": "9f8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"trx_ref": "b3c1a2d4-8e9f-4a5b-9c0d-1e2f3a4b5c6d",
"businessId": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"initiatorId": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
"status": "pending",
"trx_type": "fiat_buy",
"amount": "50000",
"asset_symbol": "NGN",
"receiver": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"occurredAt": "2026-07-22T14:32:11.000Z"
},
"timestamp": "2026-07-22T14:32:11.104Z"
}
```
Funds are confirmed received but cNGN has **not** been credited yet. Do not credit your customer on this event; wait for `deposit.completed`.
Fires when the deposit is approved and cNGN is minted to your balance. The mint settles on the issuing network, so the payload now carries `network`, the mint hash, and an explorer link:
```json theme={null}
{
"event": "deposit.completed",
"data": {
"transactionId": "9f8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"trx_ref": "b3c1a2d4-8e9f-4a5b-9c0d-1e2f3a4b5c6d",
"businessId": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"initiatorId": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
"status": "completed",
"trx_type": "fiat_buy",
"network": "XBN",
"base_trx_hash": "e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6",
"explorer_link": "https://explorer.bantu.network/tx/e5f6a7b8...e5f6",
"amount": "50000",
"asset_symbol": "NGN",
"receiver": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"occurredAt": "2026-07-22T15:05:42.000Z"
},
"timestamp": "2026-07-22T15:05:42.310Z"
}
```
This is the event to credit your customer on. For fiat deposits, the pair (`deposit.received` then `deposit.completed`) shares the same `transactionId` and `trx_ref`.
For **on-chain deposits** (cNGN sent to your wallet from an external address), the same event fires with `trx_type: crypto_deposit` and `asset_symbol: CNGN`. There is no preceding `deposit.received`. Deposits arriving via an EVM or Solana network also carry `extl_trx_hash`, the transaction hash on the source network:
```json theme={null}
{
"event": "deposit.completed",
"data": {
"transactionId": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
"trx_ref": "d4c3b2a1-0f9e-4d8c-b7a6-5f4e3d2c1b0a",
"businessId": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"initiatorId": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
"status": "completed",
"trx_type": "crypto_deposit",
"network": "base",
"base_trx_hash": "f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5",
"extl_trx_hash": "0x1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"explorer_link": "https://basescan.org/tx/0x1b2c...a1b2",
"amount": "75000",
"asset_symbol": "CNGN",
"receiver": "Wrap CBDC",
"occurredAt": "2026-07-22T18:03:27.000Z"
},
"timestamp": "2026-07-22T18:03:27.245Z"
}
```
Fires when a redemption has settled and the Naira payout to the bank account has been confirmed. The `receiver` encodes the destination bank account as `bankCode:accountNumber:accountName`:
```json theme={null}
{
"event": "redemption.completed",
"data": {
"transactionId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"trx_ref": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"businessId": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"initiatorId": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
"status": "completed",
"trx_type": "fiat_redeem",
"network": "xbn",
"amount": "100000",
"asset_symbol": "NGN",
"receiver": "058:0123456789:ADA OBI",
"occurredAt": "2026-07-22T16:12:03.000Z"
},
"timestamp": "2026-07-22T16:12:03.221Z"
}
```
`trx_type` is `fiat_redeem` for bank redemptions and `enaira_redeem` for eNaira redemptions.
Fires when an on-chain withdrawal is confirmed on the destination network. This is the richest payload: `base_trx_hash` is the burn on the issuing network, `extl_trx_hash` is the transfer on the destination network, and `receiver` is the destination wallet address:
```json theme={null}
{
"event": "withdrawal.completed",
"data": {
"transactionId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"trx_ref": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f",
"businessId": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"initiatorId": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
"status": "completed",
"trx_type": "withdraw",
"network": "BASE",
"base_trx_hash": "d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1",
"extl_trx_hash": "0x4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b",
"explorer_link": "https://basescan.org/tx/0x4a5b...4a5b",
"amount": "25000",
"asset_symbol": "CNGN",
"receiver": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
"occurredAt": "2026-07-22T17:44:19.000Z"
},
"timestamp": "2026-07-22T17:44:19.402Z"
}
```
Fires when any transaction fails or is rejected. The shape depends on where in the pipeline it stopped.
A withdrawal that failed **before** the on-chain burn includes a `reason` and no hashes:
```json theme={null}
{
"event": "transaction.failed",
"data": {
"transactionId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"trx_ref": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f",
"businessId": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"initiatorId": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
"status": "failed",
"trx_type": "withdraw",
"network": "base",
"amount": "25000",
"asset_symbol": "CNGN",
"receiver": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
"reason": "Insufficient balance",
"occurredAt": "2026-07-22T17:40:02.000Z"
},
"timestamp": "2026-07-22T17:40:02.118Z"
}
```
A withdrawal that failed **after** the burn carries the burn hash (funds are reverted automatically); `reason` may be absent:
```json theme={null}
{
"event": "transaction.failed",
"data": {
"transactionId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"trx_ref": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f",
"businessId": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"initiatorId": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
"status": "failed",
"trx_type": "withdraw",
"network": "BASE",
"base_trx_hash": "d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1",
"explorer_link": "https://basescan.org/tx/d0c1...d0c1",
"amount": "25000",
"asset_symbol": "CNGN",
"receiver": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
"occurredAt": "2026-07-22T17:52:47.000Z"
},
"timestamp": "2026-07-22T17:52:47.093Z"
}
```
A redemption whose bank payout could not be processed arrives with the redemption's details and a provider failure reason:
```json theme={null}
{
"event": "transaction.failed",
"data": {
"transactionId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"trx_ref": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"businessId": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"initiatorId": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
"status": "failed",
"trx_type": "fiat_redeem",
"network": "xbn",
"amount": "100000",
"asset_symbol": "NGN",
"receiver": "058:0123456789:ADA OBI",
"reason": "Provider failed to process payout",
"occurredAt": "2026-07-22T16:20:44.000Z"
},
"timestamp": "2026-07-22T16:20:44.156Z"
}
```
The redeemed cNGN is automatically reverted to your balance when a payout fails.
A deposit declined during review arrives with `status: "rejected"` and the decline reason:
```json theme={null}
{
"event": "transaction.failed",
"data": {
"transactionId": "9f8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"trx_ref": "b3c1a2d4-8e9f-4a5b-9c0d-1e2f3a4b5c6d",
"businessId": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"initiatorId": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
"status": "rejected",
"trx_type": "fiat_buy",
"amount": "50000",
"asset_symbol": "NGN",
"receiver": "4f1d0c2a-7b3e-4d5f-8a9b-0c1d2e3f4a5b",
"reason": "Deposit could not be matched to a payment",
"occurredAt": "2026-07-22T15:01:20.000Z"
},
"timestamp": "2026-07-22T15:01:20.077Z"
}
```
Branch on `data.trx_type` to know what failed, and on `data.status` (`failed` vs `rejected`) to distinguish processing errors from review declines. Always check for `reason` defensively; it is present only when a cause is known.
### Field presence by event
| Field | deposit.received | deposit.completed | redemption.completed | withdrawal.completed | transaction.failed |
| --------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ----------------- | -------------------- | -------------------- | ------------------ |
| `transactionId`, `trx_ref`, `businessId`, `initiatorId`, `status`, `trx_type`, `amount`, `asset_symbol`, `receiver`, `occurredAt` | Yes | Yes | Yes | Yes | Yes |
| `network` | No | Yes | Yes | Yes | Sometimes |
| `base_trx_hash` | No | Yes | No | Yes | Sometimes |
| `extl_trx_hash` | No | On-chain deposits | No | Yes | No |
| `explorer_link` | No | Yes | No | Yes | Sometimes |
| `reason` | No | No | No | No | When known |
## Verifying the signature
When a signing secret is configured, every delivery carries this header:
```text theme={null}
X-cNGN-Signature: sha256=
```
The digest is an **HMAC-SHA256 of the raw JSON request body**, keyed with your signing secret. Recompute it and compare before trusting the event:
```typescript TypeScript (Express) theme={null}
import crypto from "crypto";
import express from "express";
const app = express();
// Capture the raw body; the HMAC is computed over the exact bytes sent
app.use(express.json({
verify: (req: any, _res, buf) => { req.rawBody = buf.toString("utf8"); },
}));
app.post("/webhooks/cngn", (req: any, res) => {
const received = req.header("X-cNGN-Signature") ?? "";
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.CNGN_SIGNING_SECRET!)
.update(req.rawBody)
.digest("hex");
const valid = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.status(401).send("invalid signature");
// Acknowledge immediately, process asynchronously
res.status(200).send("ok");
queue.add(req.body); // your job queue
});
```
```python Python (Flask) theme={null}
import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhooks/cngn")
def cngn_webhook():
received = request.headers.get("X-cNGN-Signature", "")
expected = "sha256=" + hmac.new(
os.environ["CNGN_SIGNING_SECRET"].encode(),
request.get_data(), # raw body bytes
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(received, expected):
abort(401)
event = request.get_json()
enqueue(event) # your job queue; acknowledge fast
return "ok", 200
```
```php PHP theme={null}
## Delivery behaviour and best practices
Deliveries are sent once, with a 10-second timeout, and are **not automatically retried**. Design your handler accordingly:
If your endpoint is down when an event fires, that delivery is missed. Reconcile periodically against [Get Transactions](/api-reference/get-transactions) or [Verify Withdrawal](/api-reference/verify-withdrawal) so a missed webhook never means missed money.
The dispatcher times out after 10 seconds. Persist the event to a queue and return `200` immediately; never do bank calls or blockchain lookups inline.
Key your processing on `data.transactionId` plus `event`. If you've already processed that pair, acknowledge and skip.
Anyone who discovers your endpoint URL can POST fake events to it. Reject anything whose `X-cNGN-Signature` doesn't validate, and keep the signing secret out of source control.
## Bridge completion callback
Separately from event webhooks, [Bridge](/api-reference/bridge) accepts an optional per-request `callbackUrl`. When the bridged cNGN is minted on the destination network, cNGN POSTs a one-time notification to that URL:
```json theme={null}
{
"explorerLink": "https://basescan.org/tx/0x4a5b...e9f0",
"hash": "0x4a5b...e9f0",
"transactionId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"status": "completed",
"amount": "100000.00"
}
```
This callback is **not signed** and times out after 4.5 seconds, so treat it purely as a hint to check state; confirm via [Get Transactions](/api-reference/get-transactions) before crediting anything.
# Introduction
Source: https://docs.cngn.co/index
Welcome to the cNGN API: programmatic access to Africa's first regulated stablecoin
## What is cNGN?
cNGN is a regulated Naira-backed stablecoin. The cNGN API gives businesses and third-party integrators programmatic access to mint, redeem, transfer, and bridge cNGN across multiple blockchain networks, with fiat on-ramps and off-ramps through Nigerian bank accounts.
## What you can do with the API
Retrieve your cNGN balance across supported networks.
Collect Naira through dedicated or temporary virtual bank accounts and receive cNGN.
Convert cNGN back to Naira, settled directly to a Nigerian bank account.
Send cNGN to any whitelisted external wallet address.
Move cNGN between supported blockchain networks.
Query your full transaction history with pagination.
## Base URL
All API requests are made to:
```text theme={null}
https://api.cngn.co/v1/api
```
The same base URL serves both environments. Your API key prefix (`cngn_test` or `cngn_live`) determines whether a request runs against the sandbox or production environment. See [Authentication](/guides/authentication).
## Security model
The cNGN API is designed for institutional-grade security. Every integration uses three layers:
1. **API key authentication**: a Bearer token identifies your business and environment.
2. **IP whitelisting**: requests are only accepted from IP addresses you have whitelisted in your dashboard.
3. **Payload encryption**: request bodies are encrypted with AES-256-CBC, and response payloads are encrypted to your Ed25519 public key. See the [Encryption guide](/guides/encryption).
## SDKs
Official libraries for TypeScript, Python, PHP, and Java handle authentication and payload encryption for you.
Browse the supported libraries and their repositories.
## Next steps
Generate test and live API keys from the cNGN merchant dashboard.
Make your first authenticated request in the [Quickstart](/quickstart).
Configure request encryption and response decryption in the [Encryption guide](/guides/encryption).
Run through the [Going Live Checklist](/going-live) before switching to production keys.
# Quickstart
Source: https://docs.cngn.co/quickstart
Make your first cNGN API request in minutes
This guide walks you through everything needed to make your first successful API call: keys, IP whitelisting, encryption setup, and a balance request.
## Prerequisites
* A verified cNGN merchant account with dashboard access
* Your server's public IP address
* An Ed25519 SSH key pair (used to encrypt API responses to you)
## 1. Generate your API key
From the merchant dashboard, generate an API key for the environment you want to use:
| Environment | Key prefix | Purpose |
| ----------------- | -------------- | ------------------------------------------- |
| Test (sandbox) | `cngn_test...` | Integration and testing; no real funds move |
| Live (production) | `cngn_live...` | Real transactions |
Treat API keys like passwords. Never commit them to source control or expose them in client-side code.
## 2. Whitelist your IP address
The API rejects requests from IP addresses that are not whitelisted with a `403 IP address not whitelisted` error. Add every server IP that will call the API in your dashboard's security settings.
## 3. Upload your Ed25519 public key
Generate an Ed25519 key pair and upload the **public key** to your dashboard for the matching environment:
```bash theme={null}
ssh-keygen -t ed25519 -C "api@yourcompany.com" -f cngn_api_key
```
This produces `cngn_api_key` (private; keep it secret) and `cngn_api_key.pub` (public; upload it). The API encrypts every response payload to this public key; only your private key can decrypt it. Each environment (test/live) has its own SSH key slot; a request fails with `No Test SSH Key found` / `No Live SSH Key found` if the key for that environment is missing.
## 4. Make your first request
`GET /balance` requires no request body, so no request encryption is needed, which makes it a good first call:
```bash cURL theme={null}
curl -X GET "https://api.cngn.co/v1/api/balance" \
-H "Authorization: Bearer cngn_test_xxxxxxxxxxxxx" \
-H "Content-Type: application/json"
```
```typescript TypeScript theme={null}
const res = await fetch("https://api.cngn.co/v1/api/balance", {
headers: {
Authorization: `Bearer ${process.env.CNGN_API_KEY}`,
"Content-Type": "application/json",
},
});
const body = await res.json();
```
```python Python theme={null}
import os, requests
res = requests.get(
"https://api.cngn.co/v1/api/balance",
headers={
"Authorization": f"Bearer {os.environ['CNGN_API_KEY']}",
"Content-Type": "application/json",
},
)
body = res.json()
```
You'll receive the standard response envelope. The `data` field is an encrypted base64 string:
```json theme={null}
{
"status": 200,
"message": "Balance fetched successfully",
"data": "kJ8vX2mN...base64-encrypted-payload...=="
}
```
## 5. Decrypt the response
Decrypt `data` with your Ed25519 private key (see the [Encryption guide](/guides/encryption) for full implementations). The decrypted payload:
```json theme={null}
[
{
"asset_type": "credit_alphanum4",
"asset_code": "CNGN",
"balance": "150000.00"
}
]
```
## 6. Encrypt request bodies
Every `POST`/`PUT` endpoint requires the JSON body to be AES-256-CBC encrypted and wrapped as:
```json theme={null}
{
"content": "",
"iv": ""
}
```
The encryption key is issued alongside your API key in the dashboard. Requests with plain JSON bodies fail with `Missing encryption data, key, or IV`. See [Encryption](/guides/encryption) for step-by-step code, or use an [official SDK](/sdks) which handles this automatically.
## Next steps
How API keys, environments, and IP whitelisting work.
Encrypt requests and decrypt responses.
Explore every endpoint.
Understand and handle error responses.
# Libraries & SDKs
Source: https://docs.cngn.co/sdks
Official cNGN libraries for TypeScript, Python, PHP, and Java
Official server-side libraries wrap the cNGN API and handle authentication, request encryption (AES-256-CBC), and response decryption (Ed25519) for you. Four languages are currently supported:
`cngn-typescript-library`: for Node.js and TypeScript backends.
`cngn-python-library`: for Python backends.
`cngn-php-library`: for PHP backends (Composer).
`cngn-java-library`: for Java and JVM backends.
SDKs are server-side only. Never embed your API key, encryption key, or Ed25519 private key in client-side or mobile code.
## What the SDKs handle for you
* Bearer authentication with your `cngn_test` / `cngn_live` API key
* AES-256-CBC encryption of request bodies into the `{content, iv}` wire format
* Ed25519/Curve25519 decryption of response payloads
* Typed methods for every endpoint in the [API Reference](/api-reference/introduction)
If your language isn't listed, integrate directly over HTTP using the [Encryption guide](/guides/encryption); all endpoints are plain REST.
## Related tools
On-chain price oracle for cNGN.
## Brand assets
Building a cNGN integration into your product? Use the official logos and brand marks rather than recreating them.
Download official cNGN logos and brand assets from the shared drive.