Transfer Examples
Complete examples for initiating a B2C transfer. They use thesignRequest / sign_request / SignRequest helper from the Authentication page, which builds the v1 canonical string and returns the required auth headers.
Initiate Transfer
- Node.js
- Python
- Go
const axios = require('axios');
const { signRequest } = require('./gopay-auth'); // see Authentication page
const BASE_URL = 'https://transfers.gopay.example.com'; // your onboarding base URL
async function initiateTransfer() {
const payload = {
mer_id: process.env.GOPAY_MERCHANT_ID,
amount: 500.0,
currency: 'ETB',
reason: 'Salary payment',
receiver_phone_number: '+251912345678',
client_reference: `payout-${Date.now()}`,
notify_url: 'https://your-business.com/api/gopay-b2c-webhook',
bank_id: '1', // Telebirr
};
// Serialize ONCE — the same bytes must be hashed, signed and sent.
const body = Buffer.from(JSON.stringify(payload), 'utf8');
const url = `${BASE_URL}/api/v1/initiate-transfer`;
const headers = signRequest('POST', url, body);
try {
const res = await axios.post(url, body, { headers });
console.log(`Transfer accepted: ${res.data.txn_id} (${res.data.status})`);
return res.data.txn_id;
} catch (err) {
const e = err.response?.data?.error;
if (e?.code === 'INSUFFICIENT_FUNDS') {
console.error('Wallet balance too low — top up and retry.');
}
console.error(`Transfer failed [${e?.code}]: ${e?.message} (requestId=${e?.requestId})`);
throw err;
}
}
import json
import uuid
import requests
from gopay_auth import sign_request # see Authentication page
BASE_URL = "https://transfers.gopay.example.com" # your onboarding base URL
def initiate_transfer(merchant_id: str) -> str:
payload = {
"mer_id": merchant_id,
"amount": 500.0,
"currency": "ETB",
"reason": "Salary payment",
"receiver_phone_number": "+251912345678",
"client_reference": f"payout-{uuid.uuid4()}",
"notify_url": "https://your-business.com/api/gopay-b2c-webhook",
"bank_id": "1", # Telebirr
}
# Serialize ONCE — the same bytes must be hashed, signed and sent.
body = json.dumps(payload).encode("utf-8")
url = f"{BASE_URL}/api/v1/initiate-transfer"
headers = sign_request("POST", url, body)
resp = requests.post(url, data=body, headers=headers, timeout=15)
if resp.status_code != 200:
err = resp.json()["error"]
if err["code"] == "INSUFFICIENT_FUNDS":
raise RuntimeError("wallet balance too low — top up and retry")
raise RuntimeError(
f"transfer failed [{err['code']}]: {err['message']} "
f"(requestId={err.get('requestId')})"
)
data = resp.json()
print(f"Transfer accepted: {data['txn_id']} ({data['status']})")
return data["txn_id"]
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/google/uuid"
)
const baseURL = "https://transfers.gopay.example.com" // your onboarding base URL
type initiateTransferResponse struct {
Message string `json:"message"`
TxnID string `json:"txn_id"`
Status string `json:"status"`
}
type apiError struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"requestId"`
} `json:"error"`
}
func initiateTransfer(merchantID string) (string, error) {
payload := map[string]any{
"mer_id": merchantID,
"amount": 500.0,
"currency": "ETB",
"reason": "Salary payment",
"receiver_phone_number": "+251912345678",
"client_reference": "payout-" + uuid.NewString(),
"notify_url": "https://your-business.com/api/gopay-b2c-webhook",
"bank_id": "1", // Telebirr
}
// Serialize ONCE — the same bytes must be hashed, signed and sent.
body, err := json.Marshal(payload)
if err != nil {
return "", err
}
url := baseURL + "/api/v1/initiate-transfer"
// SignRequest is on the Authentication page
headers, err := SignRequest(
os.Getenv("GOPAY_PRIVATE_KEY"),
os.Getenv("GOPAY_PUBLIC_KEY"),
"POST", url, body,
)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
return "", err
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
var apiErr apiError
_ = json.Unmarshal(respBody, &apiErr)
if apiErr.Error.Code == "INSUFFICIENT_FUNDS" {
return "", fmt.Errorf("wallet balance too low — top up and retry")
}
return "", fmt.Errorf("transfer failed [%s]: %s (requestId=%s)",
apiErr.Error.Code, apiErr.Error.Message, apiErr.Error.RequestID)
}
var out initiateTransferResponse
if err := json.Unmarshal(respBody, &out); err != nil {
return "", err
}
fmt.Printf("Transfer accepted: %s (%s)\n", out.TxnID, out.Status)
return out.TxnID, nil
}
A 200 response means the transfer was accepted and your wallet debited — not that the recipient has been paid. Wait for the settlement webhook on your
notify_url before treating a payout as final. See B2C Callbacks.