Reference

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

LanguagePackageInstallMinimum
Node.js, Bun, Deno@e-pay/nodenpm install @e-pay/nodeNode 18
NestJS@e-pay/nestjsnpm install @e-pay/nestjsNest 10
Pythonepaypip install epayPython 3.9
PHP, Laravelepay-et/php-sdkcomposer require epay-et/php-sdkPHP 8.1
Gogithub.com/epay-et/go-sdkgo get github.com/epay-et/go-sdkGo 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 HTTPSDK
RetriesYou write the backoff loopExponential backoff with jitter on 429, 5xx, and network errors
IdempotencyYou generate and thread the key yourselfOne key reused across every retry of an initialize, so a retry cannot double-charge
ValidationThe API tells you after a round tripamount, currencyCode, and customerPhone are checked and normalized locally
AmountsEasy to land on a binary floatKept exact — Decimal in Python, strings and minor-unit helpers in Go
PaginationYou carry the cursor and re-apply filtersIterate a page and it walks the rest, lazily, filters intact
WebhooksHand-rolled HMAC, and one === away from a timing leakConstant-time verification that fails closed, plus framework handlers
ErrorsStatus codesA 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

raw HTTP
npm install @e-pay/node
pip install epay
composer require epay-et/php-sdk
go get github.com/epay-et/go-sdk
curl 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
OptionEnvironment variableDefault
API keyEPAY_SECRET_KEYrequired
Webhook secretEPAY_WEBHOOK_SECRET
Base URLEPAY_BASE_URLhttps://api.e-pay.et/v1
Timeout30 seconds per attempt
Max retries2
HTTP clientfetch (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    # FastAPI

Take 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 onNode.js / NestJSPythonPHPGo
Local validation, no request sentEpayValidationErrorEpayValidationErrorEpayValidationExceptionErrValidation
Unusable client optionsEpayConfigErrorEpayConfigErrorEpayConfigExceptionErrConfig
400EpayBadRequestErrorEpayBadRequestErrorEpayBadRequestExceptionErrBadRequest
401EpayAuthenticationErrorEpayAuthenticationErrorEpayAuthenticationExceptionErrAuthentication
403EpayPermissionDeniedErrorEpayPermissionDeniedErrorEpayPermissionDeniedExceptionErrPermissionDenied
404EpayNotFoundErrorEpayNotFoundErrorEpayNotFoundExceptionErrNotFound
409EpayConflictErrorEpayConflictErrorEpayConflictExceptionErrConflict
429EpayRateLimitErrorEpayRateLimitErrorEpayRateLimitExceptionErrRateLimit
5xxEpayServerErrorEpayServerErrorEpayServerExceptionErrServer
Attempt exceeded the timeoutEpayTimeoutErrorEpayTimeoutErrorEpayTimeoutExceptionErrTimeout
No response at allEpayConnectionErrorEpayConnectionErrorEpayConnectionExceptionErrConnection
Bad webhook signatureEpayWebhookSignatureErrorEpayWebhookSignatureErrorEpayWebhookSignatureExceptionErrWebhookSignature

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.

On this page