> ## Documentation Index
> Fetch the complete documentation index at: https://doc.gopay.et/llms.txt
> Use this file to discover all available pages before exploring further.

# Transfer Examples

> Code examples for the B2C Transfer API

# Transfer Examples

Complete examples for initiating a B2C transfer. They use the `signRequest` / `sign_request` / `SignRequest` helper from the [Authentication](/getting-started/authentication#signing-helpers) page, which builds the v1 canonical string and returns the required auth headers.

## Initiate Transfer

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    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;
        }
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    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"]
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    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
    }
    ```
  </Tab>
</Tabs>

<Warning>
  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](/webhooks/b2c-callbacks).
</Warning>
