SDKs & Plugins
Official client libraries for Node.js, Python, PHP, and Go — plus NestJS, Laravel, Flask, Django, and FastAPI integrations.
SDKs & Plugins
Every example on this site can be read as raw HTTP or as an official SDK. Use the SDK / HTTP toggle in the corner of any code block to switch — the choice is remembered across the whole site, so pick your language once and the docs follow.
Available libraries
| Language | Package | Install | Minimum |
|---|---|---|---|
| Node.js, Bun, Deno | @e-pay/node | npm install @e-pay/node | Node 18 |
| NestJS | @e-pay/nestjs | npm install @e-pay/nestjs | Nest 10 |
| Python | epay | pip install epay | Python 3.9 |
| PHP, Laravel | epay-et/php-sdk | composer require epay-et/php-sdk | PHP 8.1 |
| Go | github.com/epay-et/go-sdk | go get github.com/epay-et/go-sdk | Go 1.22 |
All of them are MIT licensed and track the same API surface: payments,
transactions, paymentProviders, and webhooks.
What the SDK adds
The endpoints, field names, and response shapes are identical either way. What a client library saves you is the part that is easy to get subtly wrong:
| Raw HTTP | SDK | |
|---|---|---|
| Retries | You write the backoff loop | Exponential backoff with jitter on 429, 5xx, and network errors |
| Idempotency | You generate and thread the key yourself | One key reused across every retry of an initialize, so a retry cannot double-charge |
| Validation | The API tells you after a round trip | amount, currencyCode, and customerPhone are checked and normalized locally |
| Amounts | Easy to land on a binary float | Kept exact — Decimal in Python, strings and minor-unit helpers in Go |
| Pagination | You carry the cursor and re-apply filters | Iterate a page and it walks the rest, lazily, filters intact |
| Webhooks | Hand-rolled HMAC, and one === away from a timing leak | Constant-time verification that fails closed, plus framework handlers |
| Errors | Status codes | A typed class per status, carrying the parsed body and headers |
Nothing is hidden. Every client exposes an escape hatch — epay.request in
Node, Python, and PHP, client.Do in Go — that reaches any endpoint the
library does not wrap yet, with the same auth, timeout, retry, and error
handling.
Quick start
npm install @e-pay/nodepip install epaycomposer require epay-et/php-sdkgo get github.com/epay-et/go-sdkcurl https://api.e-pay.et/v1/transactions/initialize \
-X POST \
-H "Authorization: Bearer sk_test_your_key_here" \
-H "Content-Type: application/json" \
-H "x-idempotency-key: order_123" \
-d '{
"amount": "250.00",
"currencyCode": "ETB",
"customerPhone": "+251911234567"
}'import { Epay } from '@e-pay/node';
const epay = new Epay(); // reads EPAY_SECRET_KEY
const session = await epay.payments.initialize({
amount: 250, // a number is formatted to '250.00'
currencyCode: 'etb', // uppercased
customerPhone: '0911234567', // rewritten to '+251911234567'
merchantReference: 'order_123',
});
redirect(session.checkoutUrl);
const res = await fetch('https://api.e-pay.et/v1/transactions/initialize', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.EPAY_SECRET_KEY}`,
'Content-Type': 'application/json',
'x-idempotency-key': 'order_123',
},
body: JSON.stringify({
amount: '250.00',
currencyCode: 'ETB',
customerPhone: '+251911234567',
}),
});
const session = await res.json();from decimal import Decimal
from epay import Epay
epay = Epay() # reads EPAY_SECRET_KEY
session = epay.payments.initialize(
amount=Decimal("250.00"), # str, int, Decimal, or float
currency_code="etb", # uppercased
customer_phone="0911234567", # rewritten to '+251911234567'
merchant_reference="order_123",
)
redirect(session["checkoutUrl"])
import os
import requests
res = requests.post(
"https://api.e-pay.et/v1/transactions/initialize",
headers={
"Authorization": f"Bearer {os.environ['EPAY_SECRET_KEY']}",
"x-idempotency-key": "order_123",
},
json={
"amount": "250.00",
"currencyCode": "ETB",
"customerPhone": "+251911234567",
},
timeout=30,
)
session = res.json()use Epay\Epay;
$epay = new Epay(); // reads EPAY_SECRET_KEY
$session = $epay->payments->initialize([
'amount' => 250, // string, int, or float
'currencyCode' => 'etb', // uppercased
'customerPhone' => '0911234567', // rewritten to '+251911234567'
'merchantReference' => 'order_123',
]);
header('Location: ' . $session['checkoutUrl']);
$ch = curl_init('https://api.e-pay.et/v1/transactions/initialize');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EPAY_SECRET_KEY'),
'Content-Type: application/json',
'x-idempotency-key: order_123',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => '250.00',
'currencyCode' => 'ETB',
'customerPhone' => '+251911234567',
]),
]);
$session = json_decode(curl_exec($ch), true);
curl_close($ch);package main
import (
"context"
"log"
epay "github.com/epay-et/go-sdk"
)
func main() {
client, err := epay.NewClient() // reads EPAY_SECRET_KEY
if err != nil {
log.Fatal(err)
}
session, err := client.Payments.Initialize(context.Background(), epay.InitializeParams{
Amount: "250.00",
CurrencyCode: "ETB",
CustomerPhone: "+251911234567",
MerchantReference: "order_123",
})
if err != nil {
log.Fatal(err)
}
log.Println(session.CheckoutURL)
}
payload, _ := json.Marshal(map[string]string{
"amount": "250.00",
"currencyCode": "ETB",
"customerPhone": "+251911234567",
})
req, _ := http.NewRequest(http.MethodPost,
"https://api.e-pay.et/v1/transactions/initialize", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("EPAY_SECRET_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-idempotency-key", "order_123")
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)The key prefix picks the environment — sk_test_… runs against the sandbox
and sk_live_… moves real money. There is no separate mode to configure. Read
it back with epay.mode / epay.isSandbox (client.Mode() /
client.IsSandbox() in Go).
Configuration
Every option falls back to an environment variable, so the common case is a bare constructor.
EPAY_SECRET_KEY=sk_test_your_key_here
EPAY_WEBHOOK_SECRET=your_webhook_secret| Option | Environment variable | Default |
|---|---|---|
| API key | EPAY_SECRET_KEY | required |
| Webhook secret | EPAY_WEBHOOK_SECRET | — |
| Base URL | EPAY_BASE_URL | https://api.e-pay.et/v1 |
| Timeout | — | 30 seconds per attempt |
| Max retries | — | 2 |
| HTTP client | — | fetch (Node), httpx (Python), PSR-18 (PHP), net/http (Go) |
Printing a client masks the key, so a client is safe to log.
In Go, WithTimeout applies per attempt. Setting Timeout on a custom
*http.Client instead bounds the whole call including retries, which is
usually not what you want.
Framework integrations
NestJS
@e-pay/nestjs wraps the Node client in a dynamic module, an injectable
service, a webhook guard, and a param decorator.
import { Module } from '@nestjs/common';
import { EpayModule } from '@e-pay/nestjs';
@Module({
imports: [
EpayModule.forRoot({
apiKey: process.env.EPAY_SECRET_KEY,
webhookSecret: process.env.EPAY_WEBHOOK_SECRET,
isGlobal: true, // inject EpayService anywhere without re-importing
}),
],
})
export class AppModule {}An invalid key fails at bootstrap rather than at the first payment.
forRootAsync accepts useFactory, useClass, and useExisting if you
configure through ConfigService.
Signature verification needs the raw body, so create the app with
NestFactory.create(AppModule, { rawBody: true }), then guard the route:
@Post('epay')
@HttpCode(200)
@UseGuards(EpayWebhookGuard)
async handle(@EpayEvent() event: WebhookEvent) {
// The signature is already verified; an invalid one never reaches here.
await this.queue.enqueue('epay-event', event);
}Laravel
The service provider is auto-discovered. Add your keys to .env and inject
Epay anywhere:
final class CheckoutController
{
public function __construct(private readonly Epay $epay) {}
public function store(Request $request)
{
$session = $this->epay->payments->initialize([
'amount' => $request->string('amount')->value(),
'currencyCode' => 'ETB',
'customerPhone' => $request->string('phone')->value(),
'merchantReference' => $order->id,
], idempotencyKey: $order->id);
return redirect()->away($session['checkoutUrl']);
}
}ePay sends no CSRF token, so exclude the webhook route and alias the middleware
in bootstrap/app.php (Laravel 11+):
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['webhooks/epay']);
$middleware->alias(['epay.webhook' => VerifyEpayWebhook::class]);
})Then VerifyEpayWebhook::event($request) hands you a verified event, and throws
rather than returning an unverified payload if the middleware was not applied.
Express, Next.js, Hono, Bun, Deno, Cloudflare Workers
// Express — mount before any global express.json()
import { expressEpayWebhook } from '@e-pay/node/express';
// Next.js route handler, Hono, Workers — verifies with WebCrypto,
// imports nothing from node:, runs unchanged on the edge
import { createEpayWebhookHandler } from '@e-pay/node/fetch';
export const POST = createEpayWebhookHandler({
secret: process.env.EPAY_WEBHOOK_SECRET!,
onEvent: async (event) => queue.enqueue(event),
});Flask, Django, FastAPI
One helper per framework, all raising EpayWebhookSignatureError on a bad
signature:
from epay.integrations.flask import construct_event_from_request # Flask
from epay.integrations.django import construct_event_from_request # Django
from epay.integrations.asgi import construct_event_from_request # FastAPITake the whole request object, not a parsed model — a re-serialized body has a
different digest and a valid delivery would be rejected. On Django, remember
@csrf_exempt.
Go net/http
verifier := epay.NewWebhookVerifier(os.Getenv("EPAY_WEBHOOK_SECRET"))
mux.Handle("/webhooks/epay", verifier.Handler(func(event *epay.WebhookEvent) error {
return queue.Enqueue(event)
}))Errors
Every failure is a typed error carrying the status, parsed body, and response
headers. 429, 5xx, and network errors are retried automatically first, so
seeing one means the retry budget was also exhausted.
| Raised on | Node.js / NestJS | Python | PHP | Go |
|---|---|---|---|---|
| Local validation, no request sent | EpayValidationError | EpayValidationError | EpayValidationException | ErrValidation |
| Unusable client options | EpayConfigError | EpayConfigError | EpayConfigException | ErrConfig |
400 | EpayBadRequestError | EpayBadRequestError | EpayBadRequestException | ErrBadRequest |
401 | EpayAuthenticationError | EpayAuthenticationError | EpayAuthenticationException | ErrAuthentication |
403 | EpayPermissionDeniedError | EpayPermissionDeniedError | EpayPermissionDeniedException | ErrPermissionDenied |
404 | EpayNotFoundError | EpayNotFoundError | EpayNotFoundException | ErrNotFound |
409 | EpayConflictError | EpayConflictError | EpayConflictException | ErrConflict |
429 | EpayRateLimitError | EpayRateLimitError | EpayRateLimitException | ErrRateLimit |
5xx | EpayServerError | EpayServerError | EpayServerException | ErrServer |
| Attempt exceeded the timeout | EpayTimeoutError | EpayTimeoutError | EpayTimeoutException | ErrTimeout |
| No response at all | EpayConnectionError | EpayConnectionError | EpayConnectionException | ErrConnection |
| Bad webhook signature | EpayWebhookSignatureError | EpayWebhookSignatureError | EpayWebhookSignatureException | ErrWebhookSignature |
429 exposes the retry-after in seconds. In Go the sentinels are matched with
errors.Is, and a cancelled caller context comes back as context.Canceled
unwrapped, so it is never mistaken for an SDK timeout.
See Error Codes for what each status means on the wire.
Testing
Point the client at a stub and no request leaves the process:
const epay = new Epay({
apiKey: 'sk_test_fake',
maxRetries: 0,
fetch: async () =>
new Response(JSON.stringify({ reference: 'PAB1', checkoutUrl: '…' }), {
status: 200,
}),
});Against the real sandbox, use a sk_test_… key with the magic phone numbers in
Test Accounts — and generate a fresh
idempotency key per run, or you will get the cached response instead of the
scenario.
Next: Quick Start — install a client and take your first payment.