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_hereYour 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/v1All 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.
| Language | Package | Install |
|---|---|---|
| Node.js, Bun, Deno | @e-pay/node | npm install @e-pay/node |
| NestJS | @e-pay/nestjs | npm install @e-pay/nestjs |
| Python 3.9+ | epay | pip install epay |
| PHP 8.1+, Laravel | epay-et/php-sdk | composer require epay-et/php-sdk |
| Java 17+ | com.epayethiopia:epay-java | implementation("com.epayethiopia:epay-java:0.1.0") |
| Spring Boot 3.5+ | com.epayethiopia:epay-spring-boot-starter | implementation("com.epayethiopia:epay-spring-boot-starter:0.1.0") |
| Go 1.22+ | github.com/epay-et/go-sdk | go 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_secretSee 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
npm install @e-pay/nodenpm install @e-pay/nestjspip install epaypip install epaypip install epaypip install epaycomposer require epay-et/php-sdkcomposer require epay-et/php-sdkimplementation("com.epayethiopia:epay-java:0.1.0")implementation("com.epayethiopia:epay-spring-boot-starter:0.1.0")go get github.com/epay-et/go-sdkNothing 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.
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
- Initialize a Payment — full request and response reference
- Webhooks — receive payment events without polling
- Verify a Payment — confirm completion before fulfilling an order