Quick Start

Set up your environment and make your first API call in minutes.

Quick Start

The ePay Business API lets you initialize payment sessions, track transaction status, and receive webhook notifications — all with a single API key.


Prerequisites

  • An ePay merchant account (open one here)
  • An API key from your dashboard under Developers → API Keys

Authentication

All requests must include your secret key in the Authorization header:

Authorization: Bearer sk_live_your_key_here

Your key prefix sets the mode automatically — sk_live_… targets live payments, sk_test_… targets the sandbox. You don't configure the mode separately.


Base URL

https://api.epayethiopia.com/v1

All endpoints below are relative to this base URL.


Install an SDK

Every example on this site is available as raw HTTP and as an official SDK — use the SDK / HTTP toggle above any code block to switch, and the choice follows you across the whole site. In SDK mode, languages with framework guides get a second row — Node.js / NestJS, Python / Django / Flask / FastAPI, PHP / Laravel, and Java / Spring Boot — and each language remembers its own pick.

The SDKs are thin: same endpoints, same field names, same response shapes. What they add is the tedious part — automatic retries with backoff on 429/5xx, one idempotency key reused across every retry of an initialize, local validation of amounts and phone numbers, cursor pagination as a loop, and constant-time webhook signature verification.

LanguagePackageInstall
Node.js, Bun, Deno@e-pay/nodenpm install @e-pay/node
NestJS@e-pay/nestjsnpm install @e-pay/nestjs
Python 3.9+epaypip install epay
PHP 8.1+, Laravelepay-et/php-sdkcomposer require epay-et/php-sdk
Java 17+com.epayethiopia:epay-javaimplementation("com.epayethiopia:epay-java:0.1.0")
Spring Boot 3.5+com.epayethiopia:epay-spring-boot-starterimplementation("com.epayethiopia:epay-spring-boot-starter:0.1.0")
Go 1.22+github.com/epay-et/go-sdkgo get github.com/epay-et/go-sdk

Each client reads EPAY_SECRET_KEY from the environment, so there is nothing to pass in the common case:

EPAY_SECRET_KEY=sk_test_your_key_here
EPAY_WEBHOOK_SECRET=your_webhook_secret

See SDKs & Plugins for the full surface of each one.


Your first payment

Initialize a session, redirect the customer, then check the result.

Create a payment session

raw HTTP
npm install @e-pay/node
npm install @e-pay/nestjs
pip install epay
pip install epay
pip install epay
pip install epay
composer require epay-et/php-sdk
composer require epay-et/php-sdk
implementation("com.epayethiopia:epay-java:0.1.0")
implementation("com.epayethiopia:epay-spring-boot-starter:0.1.0")
go get github.com/epay-et/go-sdk

Nothing NestJS-specific here — this is the same Node.js client call.

Nothing Django-specific here — this is the same Python client call.

Nothing Flask-specific here — this is the same Python client call.

Nothing FastAPI-specific here — this is the same Python client call.

Nothing Laravel-specific here — this is the same PHP client call.

Nothing Spring Boot-specific here — this is the same Java client call.

curl https://api.epayethiopia.com/v1/transactions/initialize \
  -X POST \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -H "x-idempotency-key: order_001" \
  -d '{
    "amount": "250.00",
    "currencyCode": "ETB",
    "customerPhone": "+251911234567",
    "merchantReference": "order_001"
  }'
import { Epay } from '@e-pay/node';

const epay = new Epay(); // reads EPAY_SECRET_KEY

const session = await epay.payments.initialize(
  {
    amount: '250.00',
    currencyCode: 'ETB',
    customerPhone: '+251911234567',
    merchantReference: 'order_001',
  },
  { idempotencyKey: 'order_001' },
);
const res = await fetch('https://api.epayethiopia.com/v1/transactions/initialize', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_test_your_key_here',
    'Content-Type': 'application/json',
    'x-idempotency-key': 'order_001',
  },
  body: JSON.stringify({
    amount: '250.00',
    currencyCode: 'ETB',
    customerPhone: '+251911234567',
    merchantReference: 'order_001',
  }),
});

const session = await res.json();
from epay import Epay

epay = Epay()  # reads EPAY_SECRET_KEY

session = epay.payments.initialize(
    amount="250.00",
    currency_code="ETB",
    customer_phone="+251911234567",
    merchant_reference="order_001",
    idempotency_key="order_001",
)
import requests

res = requests.post(
    "https://api.epayethiopia.com/v1/transactions/initialize",
    headers={
        "Authorization": "Bearer sk_test_your_key_here",
        "x-idempotency-key": "order_001",
    },
    json={
        "amount": "250.00",
        "currencyCode": "ETB",
        "customerPhone": "+251911234567",
        "merchantReference": "order_001",
    },
    timeout=30,
)

session = res.json()
use Epay\Epay;

$epay = new Epay(); // reads EPAY_SECRET_KEY

$session = $epay->payments->initialize([
    'amount' => '250.00',
    'currencyCode' => 'ETB',
    'customerPhone' => '+251911234567',
    'merchantReference' => 'order_001',
], idempotencyKey: 'order_001');
$ch = curl_init('https://api.epayethiopia.com/v1/transactions/initialize');

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer sk_test_your_key_here',
        'Content-Type: application/json',
        'x-idempotency-key: order_001',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => '250.00',
        'currencyCode' => 'ETB',
        'customerPhone' => '+251911234567',
        'merchantReference' => 'order_001',
    ]),
]);

$session = json_decode(curl_exec($ch), true);
curl_close($ch);
client, err := epay.NewClient() // reads EPAY_SECRET_KEY
if err != nil {
	log.Fatal(err)
}

session, err := client.Payments.Initialize(ctx, epay.InitializeParams{
	Amount:            "250.00",
	CurrencyCode:      "ETB",
	CustomerPhone:     "+251911234567",
	MerchantReference: "order_001",
	IdempotencyKey:    "order_001",
})
if err != nil {
	log.Fatal(err)
}
payload, _ := json.Marshal(map[string]string{
	"amount":            "250.00",
	"currencyCode":      "ETB",
	"customerPhone":     "+251911234567",
	"merchantReference": "order_001",
})

req, _ := http.NewRequest(http.MethodPost,
	"https://api.epayethiopia.com/v1/transactions/initialize", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer sk_test_your_key_here")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-idempotency-key", "order_001")

res, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer res.Body.Close()

var session struct {
	Reference   string `json:"reference"`
	CheckoutURL string `json:"checkoutUrl"`
}
json.NewDecoder(res.Body).Decode(&session)
Epay epay = Epay.fromEnvironment(); // reads EPAY_SECRET_KEY

InitializedPayment session = epay.payments().initialize(InitializeParams.builder()
        .amount("250.00")
        .currencyCode("ETB")
        .customerPhone("+251911234567")
        .merchantReference("order_001")
        .idempotencyKey("order_001")
        .build());
HttpClient http = HttpClient.newHttpClient();

String body = """
        {
          "amount": "250.00",
          "currencyCode": "ETB",
          "customerPhone": "+251911234567",
          "merchantReference": "order_001"
        }
        """;

HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://api.epayethiopia.com/v1/transactions/initialize"))
        .header("Authorization", "Bearer " + System.getenv("EPAY_SECRET_KEY"))
        .header("Content-Type", "application/json")
        .header("x-idempotency-key", "order_001")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());

if (response.statusCode() >= 400) {
    throw new IllegalStateException("ePay " + response.statusCode() + ": " + response.body());
}

// {"reference":"…","checkoutUrl":"…","status":"success","expiresAt":"…"}
String session = response.body();
// app.module.ts, once: EpayModule.forRoot({ isGlobal: true }) reads EPAY_SECRET_KEY

import { Controller, Param, Post, Redirect } from '@nestjs/common';
import { EpayService } from '@e-pay/nestjs';

@Controller('orders')
export class CheckoutController {
  constructor(
    private readonly epay: EpayService,
    private readonly orders: OrdersService,
  ) {}

  @Post(':id/pay')
  @Redirect()
  async pay(@Param('id') id: string) {
    const order = await this.orders.find(id);

    const session = await this.epay.payments.initialize(
      {
        amount: order.total,
        currencyCode: 'ETB',
        customerPhone: order.customerPhone,
        merchantReference: order.id,
        returnUrl: `https://yourstore.com/orders/${order.id}/complete`,
      },
      { idempotencyKey: order.id },
    );

    return { url: session.checkoutUrl, statusCode: 303 };
  }
}
# payments/clients.py — one client per process; it pools connections
from django.conf import settings
from epay import Epay

epay = Epay(api_key=settings.EPAY_SECRET_KEY)


# payments/views.py
from django.shortcuts import get_object_or_404, redirect
from django.views.decorators.http import require_POST

from .clients import epay
from .models import Order


@require_POST
def pay(request, order_id):
    order = get_object_or_404(Order, pk=order_id)

    session = epay.payments.initialize(
        amount=order.total,  # a DecimalField stays exact
        currency_code="ETB",
        customer_phone=order.customer_phone,
        merchant_reference=str(order.pk),
        return_url=request.build_absolute_uri(f"/orders/{order.pk}/complete"),
        idempotency_key=str(order.pk),
    )

    return redirect(session["checkoutUrl"])
from flask import Flask, redirect, url_for
from epay import Epay

app = Flask(__name__)
epay = Epay()  # one client per process; reads EPAY_SECRET_KEY


@app.post("/orders/<order_id>/pay")
def pay(order_id):
    order = Order.query.get_or_404(order_id)

    session = epay.payments.initialize(
        amount=order.total,
        currency_code="ETB",
        customer_phone=order.customer_phone,
        merchant_reference=order_id,
        return_url=url_for("order_complete", order_id=order_id, _external=True),
        idempotency_key=order_id,
    )

    return redirect(session["checkoutUrl"], code=303)
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from epay import AsyncEpay

epay = AsyncEpay()  # reads EPAY_SECRET_KEY


@asynccontextmanager
async def lifespan(app: FastAPI):
    yield
    await epay.aclose()  # release the connection pool on shutdown


app = FastAPI(lifespan=lifespan)


@app.post("/orders/{order_id}/pay")
async def pay(order_id: str):
    order = await orders.get(order_id)

    session = await epay.payments.initialize(
        amount=order.total,
        currency_code="ETB",
        customer_phone=order.customer_phone,
        merchant_reference=order_id,
        return_url=f"https://yourstore.com/orders/{order_id}/complete",
        idempotency_key=order_id,
    )

    return RedirectResponse(session["checkoutUrl"], status_code=303)
// The service provider is auto-discovered and reads EPAY_SECRET_KEY from .env.

use App\Models\Order;
use Epay\Epay;
use Illuminate\Http\RedirectResponse;

final class CheckoutController
{
    public function __construct(private readonly Epay $epay) {}

    public function store(Order $order): RedirectResponse
    {
        $session = $this->epay->payments->initialize([
            'amount' => $order->total,
            'currencyCode' => 'ETB',
            'customerPhone' => $order->customer_phone,
            'merchantReference' => (string) $order->id,
            'returnUrl' => route('orders.complete', $order),
        ], idempotencyKey: (string) $order->id);

        return redirect()->away($session['checkoutUrl']);
    }
}

// routes/web.php
// Route::post('/orders/{order}/pay', [CheckoutController::class, 'store']);
// application.yml
//   epay:
//     api-key: ${EPAY_SECRET_KEY}

@RestController
class CheckoutController {
    private final Epay epay;
    private final Orders orders;

    CheckoutController(Epay epay, Orders orders) {
        this.epay = epay;
        this.orders = orders;
    }

    @PostMapping("/orders/{id}/pay")
    ResponseEntity<Void> pay(@PathVariable String id) {
        Order order = orders.find(id);

        InitializedPayment session = epay.payments().initialize(InitializeParams.builder()
                .amount(order.total())
                .currencyCode("ETB")
                .customerPhone(order.phone())
                .merchantReference(order.id())
                .returnUrl("https://yourstore.com/orders/" + order.id() + "/complete")
                .idempotencyKey(order.id())
                .build());

        return ResponseEntity.status(HttpStatus.SEE_OTHER)
                .location(URI.create(session.checkoutUrl()))
                .build();
    }
}

Redirect the customer

The response contains a checkoutUrl and a reference to track the payment.

{
  "reference": "PAB12CD3420260813",
  "checkoutUrl": "https://checkout.epayethiopia.com/pay/PAB12CD3420260813",
  "status": "success",
  "expiresAt": "2026-08-13T12:30:00.000Z"
}

Redirect your customer to checkoutUrl. Save the reference — you'll use it to check status and reconcile.

Check the result

Poll for status or wait for a webhook event.

raw HTTP

Nothing NestJS-specific here — this is the same Node.js client call.

Nothing Django-specific here — this is the same Python client call.

Nothing Flask-specific here — this is the same Python client call.

Nothing FastAPI-specific here — this is the same Python client call.

Nothing Laravel-specific here — this is the same PHP client call.

Nothing Spring Boot-specific here — this is the same Java client call.

curl https://api.epayethiopia.com/v1/transactions/PAB12CD3420260813 \
  -H "Authorization: Bearer sk_test_your_key_here"
const transaction = await epay.transactions.retrieve(session.reference);

if (transaction.status === 'completed') {
  const receipt = await epay.payments.verify(session.reference);
}
const res = await fetch(
  `https://api.epayethiopia.com/v1/transactions/${session.reference}`,
  { headers: { Authorization: 'Bearer sk_test_your_key_here' } },
);

const transaction = await res.json();
transaction = epay.transactions.retrieve(session["reference"])

if transaction["status"] == "completed":
    receipt = epay.payments.verify(session["reference"])
res = requests.get(
    f"https://api.epayethiopia.com/v1/transactions/{session['reference']}",
    headers={"Authorization": "Bearer sk_test_your_key_here"},
    timeout=30,
)

transaction = res.json()
$transaction = $epay->transactions->retrieve($session['reference']);

if ($transaction['status'] === 'completed') {
    $receipt = $epay->payments->verify($session['reference']);
}
$ch = curl_init('https://api.epayethiopia.com/v1/transactions/' . $session['reference']);

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_test_your_key_here'],
]);

$transaction = json_decode(curl_exec($ch), true);
curl_close($ch);
transaction, err := client.Transactions.Retrieve(ctx, session.Reference)
if err != nil {
	log.Fatal(err)
}

if transaction.Status == epay.StatusCompleted {
	receipt, err := client.Payments.Verify(ctx, session.Reference)
	_ = receipt
	_ = err
}
req, _ := http.NewRequest(http.MethodGet,
	"https://api.epayethiopia.com/v1/transactions/"+session.Reference, nil)
req.Header.Set("Authorization", "Bearer sk_test_your_key_here")

res, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer res.Body.Close()

var transaction map[string]any
json.NewDecoder(res.Body).Decode(&transaction)
Transaction transaction = epay.transactions().retrieve(session.reference());

if (transaction.status() == TransactionStatus.COMPLETED) {
    VerifiedPayment receipt = epay.payments().verify(session.reference());
}
HttpClient http = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://api.epayethiopia.com/v1/transactions/PAB12CD3420260813"))
        .header("Authorization", "Bearer " + System.getenv("EPAY_SECRET_KEY"))
        .GET()
        .build();

HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());

if (response.statusCode() >= 400) {
    throw new IllegalStateException("ePay " + response.statusCode() + ": " + response.body());
}

// Parse with Jackson, Gson, or your JSON library of choice.
String transaction = response.body();
{
  "reference": "PAB12CD3420260813",
  "merchantReference": "order_001",
  "status": "completed",
  "amount": "250.00",
  "currencyCode": "ETB",
  "paidAt": "2026-08-13T11:47:00.000Z",
  "createdAt": "2026-08-13T11:30:00.000Z"
}

Test mode

Use your sk_test_… key to run sandbox payments without moving real money. Test mode is identical to live — same endpoints, same response shapes, same webhook events.

Never use your live key in development or CI. Keep secret keys out of version control.

See Test Accounts for magic phone numbers that trigger specific outcomes in the sandbox.


Next steps

On this page