Skip to main content
This guide walks through the complete payout flow — from discovering corridor requirements to tracking delivery.

Prerequisites

  • A Cadana business account with an Org API key
  • Review Authentication for API key setup

Step 0: Fund Your Wallet

Payouts are sent from your Cadana wallet balance. Before you can send a payment, make sure your wallet is funded.

Check Your Balance

bash
curl -X GET 'https://api.cadanapay.com/v1/balances' \
  -H 'Authorization: Bearer YOUR_API_KEY'
Response:
{
  "data": [
    {
      "id": "517075a2-17db-469f-9481-eb8347cb920c",
      "currency": "USD",
      "balance": 703448,
      "available": 703448,
      "processing": 0
    }
  ]
}
Use the available field to determine how much you can send — processing reflects amounts currently in transit. All values are in the lowest denomination (e.g., cents).

Get Funding Instructions

To add funds, call GET /v1/funding-details to get the bank details for depositing into your wallet:
bash
curl -X GET 'https://api.cadanapay.com/v1/funding-details' \
  -H 'Authorization: Bearer YOUR_API_KEY'
This returns bank account details you can use to transfer funds into your Cadana wallet. The response varies by currency — for example, USD returns ACH, wire, and SWIFT details, while EUR returns IBAN details.
In sandbox, use POST /v1/sandbox/business-deposits to add test funds to your wallet.

Step 1: Discover Requirements

Before creating a beneficiary, query the corridor requirements and available providers.

Get Payment Requirements

Returns the required fields for a given country, currency, and payment method:
bash
curl -X GET 'https://api.cadanapay.com/v1/payment-requirements' \
  -H 'Authorization: Bearer YOUR_API_KEY'
The response is a JSON schema describing mandatory fields, validation patterns, and allowed values. See Payment Requirements for how to interpret the schema and build dynamic forms from it.

Get Supported Providers

Returns banks and mobile money providers you can send payments to:
bash
curl -X GET 'https://api.cadanapay.com/v1/providers' \
  -H 'Authorization: Bearer YOUR_API_KEY'
Response:
{
  "data": [
    {
      "code": "01",
      "name": "Example Bank",
      "countryCode": "KE",
      "currency": "KES",
      "type": "bank"
    }
  ]
}
Use the returned code as the bankCode when creating beneficiaries.

Step 2: Create a Beneficiary

A beneficiary stores the recipient’s payment details. Create it once, then reference it for every payout to that recipient.

Upload KYC Document

Beneficiary creation requires a KYC identity document. Upload it first: 2a. Get an upload URL:
bash
curl -X POST 'https://api.cadanapay.com/v1/files/upload-url' \
  -H 'Authorization: Bearer YOUR_API_KEY'
Response:
{
  "fileId": "517075a2-17db-469f-9481-eb8347cb920c",
  "putUrl": "https://cadana-kyc.s3.amazonaws.com/tmp/file_9c52fa2e?X-Amz-Algorithm=...",
  "expiresIn": 900
}
2b. Upload the file to the putUrl before it expires:
Bash
curl -X PUT 'RETURNED_PUT_URL' \
  -H 'Content-Type: image/jpeg' \
  --data-binary @passport-front.jpg

Create the Beneficiary

bash
curl -X POST 'https://api.cadanapay.com/v1/beneficiaries' \
  -H 'Authorization: Bearer YOUR_API_KEY'
Response:
{
  "id": "7a7f80a6-1665-4f64-9ef3-d5f90f8f309b",
}
Save the beneficiary id. You’ll use it every time you pay this recipient.

Managing Beneficiaries

Once created, you can list, retrieve, and delete beneficiaries:

Step 3: Get an FX Quote

Lock the exchange rate before creating the payout. The returned quote id is required when submitting the payout.
bash
curl -X POST 'https://api.cadanapay.com/v1/fx-quotes' \
  -H 'Authorization: Bearer YOUR_API_KEY'
Response:
{
  "id": "57c74c5d-38a0-41e7-a19e-161f16dc4898",
  "from": "USD",
  "to": "KES",
  "rate": "129.50",
  "expirationTimestamp": "2024-01-15T10:45:00Z"
}
Quotes expire in ~2 minutes. Create the payout before the quote expires or it will be rejected.

Step 4: Create the Payout

Check your balance first, then execute:

Check Balance

bash
curl -X GET 'https://api.cadanapay.com/v1/balances' \
  -H 'Authorization: Bearer YOUR_API_KEY'

Generate a Reference

Include a unique reference to identify the payout in your system. References are useful for:
  • Tracking — look up payouts by reference via GET /v1/payouts?reference=INV-2024-001
  • Preventing duplicates — submitting the same reference twice returns a 400 error, preventing accidental double payments
References are optional but recommended. Max 128 characters.

Execute Payout

bash
curl -X POST 'https://api.cadanapay.com/v1/payouts' \
  -H 'Authorization: Bearer YOUR_API_KEY'
The amount value is in the lowest denomination of the currency (e.g., cents for USD). 100000 = $1,000.00 USD.
Response:
{
  "id": "d35d2bd6-188b-4e82-9a16-71442dad7375"
}

Retrying a Failed Payout

If a payout fails, you can retry it by creating a new payout with the same beneficiary. You’ll need a fresh FX quote (the original may have expired) and a new reference, since the original reference is already recorded.
1. Get a new FX quote      →  POST /v1/fx-quotes
2. Create a new payout      →  POST /v1/payouts (same beneficiaryId, new quoteId, new reference)

Step 5: Track the Payment

Subscribe to transaction events for real-time status updates. See Webhooks to configure your endpoint. Each status change emits a webhook with the following envelope:
{
  "id": "evt_abc123",
  "eventType": "transaction.initiated",
  "version": "1.0",
  "timestamp": 1681007225,
  "data": { ... }
}
The data payload varies by event:
Payout created, compliance checks in progress.
{
  "id": "d35d2bd6-188b-4e82-9a16-71442dad7375",
  "amount": {
    "currency": "USD",
    "amount": 100000
  },
  "type": "PAYOUT",
  "reference": "INV-2024-001",
  "recipientId": "7a7f80a6-1665-4f64-9ef3-d5f90f8f309b",
  "tenantKey": "your-tenant-key",
  "timestamp": 1681007225
}

Status Lifecycle

initiated → processing → succeeded
                ↓              ↓
              failed      returned (failed with isReturned: true)

Polling

Alternatively, poll the payout endpoint for status updates:
bash
curl -X GET 'https://api.cadanapay.com/v1/payouts/d35d2bd6-188b-4e82-9a16-71442dad7375' \
  -H 'Authorization: Bearer YOUR_API_KEY'
Response:
{
  "id": "d35d2bd6-188b-4e82-9a16-71442dad7375",
  "beneficiaryId": "7a7f80a6-1665-4f64-9ef3-d5f90f8f309b",
  "quoteId": "57c74c5d-38a0-41e7-a19e-161f16dc4898",
  "reference": "INV-2024-001",
  "amount": {
    "amount": 100000,
    "currency": "USD"
  },
  "sourceAmount": {
    "amount": 100000,
    "currency": "USD"
  },
  "feeAmount": {
    "amount": 500,
    "currency": "USD"
  },
  "fxRate": "129.50",
  "status": "succeeded",
  "createdTimestamp": 1748478276,
  "lastUpdatedTimestamp": 1748478276
}

List Payouts

Retrieve all payouts with optional filters:
bash
curl -X GET 'https://api.cadanapay.com/v1/payouts' \
  -H 'Authorization: Bearer YOUR_API_KEY'
The endpoint has three modes depending on which parameters you pass:
ModeWhenBehavior
Reference lookupreference is setReturns the single matching payout. All other params are ignored.
Paginated listDefaultReturns payouts in pages (default 20). Supports status, startDate, endDate, order (asc/desc, default desc), cursor, and limit.
CSV exportformat=csvReturns all matching payouts as a CSV file. Requires startDate and endDate. Order is always desc.

Complete Example

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

const baseURL = "https://dev-api.cadanapay.com"

func main() {
	apiKey := os.Getenv("CADANA_API_KEY")

	// 1. Upload KYC document
	fmt.Println("Uploading KYC document...")
	upload := post(apiKey, "/v1/files/upload-url", map[string]string{
		"purpose":     "kyc-id-front",
		"contentType": "image/jpeg",
	})
	fileID := upload["fileId"].(string)
	putURL := upload["putUrl"].(string)
	uploadFile(putURL, "id-front.jpg", "image/jpeg")

	// 2. Create beneficiary
	fmt.Println("Creating beneficiary...")
	ben := post(apiKey, "/v1/beneficiaries", map[string]any{
		"name":        "Jane Wanjiku",
		"email":       "jane@example.com",
		"countryCode": "KE",
		"currency":    "KES",
		"paymentDetails": map[string]any{
			"preferredMethod": "bank",
			"bank": map[string]string{
				"accountName":   "Jane Wanjiku",
				"accountNumber": "1234567890",
				"bankCode":      "01",
				"bankName":      "Example Bank",
			},
		},
		"kyc": map[string]string{
			"firstName":   "Jane",
			"lastName":    "Wanjiku",
			"dateOfBirth": "1990-08-15",
			"countryCode": "KE",
			"idType":      "NATIONAL_ID",
			"idNumber":    "12345678",
			"idFileFront": fileID,
		},
	})
	benID := ben["id"].(string)
	fmt.Printf("Beneficiary: %s\n", benID)

	// 3. Get FX quote
	fmt.Println("Getting FX quote...")
	quote := post(apiKey, "/v1/fx-quotes", map[string]string{
		"from": "USD",
		"to":   "KES",
	})
	quoteID := quote["id"].(string)
	fmt.Printf("Quote: %s (rate: %v)\n", quoteID, quote["rate"])

	// 4. Create payout
	fmt.Println("Creating payout...")
	payout := post(apiKey, "/v1/payouts", map[string]any{
		"beneficiaryId": benID,
		"quoteId":       quoteID,
		"amount":        map[string]any{"amount": 100000, "currency": "USD"},
		"reference":     fmt.Sprintf("test-%d", time.Now().Unix()),
	})
	payoutID := payout["id"].(string)
	fmt.Printf("Payout: %s\n", payoutID)

	// 5. Check status
	time.Sleep(3 * time.Second)
	fmt.Println("Checking status...")
	status := get(apiKey, "/v1/payouts/"+payoutID)
	fmt.Printf("Status: %s\n", status["status"])
}

func post(apiKey, path string, body any) map[string]any {
	data, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", baseURL+path, bytes.NewReader(data))
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")
	return doRequest(req)
}

func get(apiKey, path string) map[string]any {
	req, _ := http.NewRequest("GET", baseURL+path, nil)
	req.Header.Set("Authorization", "Bearer "+apiKey)
	return doRequest(req)
}

func doRequest(req *http.Request) map[string]any {
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Fprintf(os.Stderr, "request failed: %v\n", err)
		os.Exit(1)
	}
	defer resp.Body.Close()
	var result map[string]any
	json.NewDecoder(resp.Body).Decode(&result)
	return result
}

func uploadFile(putURL, filePath, contentType string) {
	file, _ := os.Open(filePath)
	defer file.Close()
	req, _ := http.NewRequest("PUT", putURL, file)
	req.Header.Set("Content-Type", contentType)
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
	io.Copy(io.Discard, resp.Body)
}

Next Steps