---
title: "PHP SDK"
description: "Backend-only PHP client for every EPS API, with HMAC signing, input validation and typed errors built in."
canonical: "https://eps.eko.in/docs/sdk/php"
---


> **Canonical URL:** https://eps.eko.in/docs/sdk/php
> This is a machine-readable Markdown version of the page for AI agents and LLMs. The primary (HTML) version lives at the canonical URL above.

# PHP SDK

`ekoindia/eps-sdk` is a client for every EPS API that needs nothing beyond
`ext-curl` and `ext-json`. One generic `call($slug, $params)` covers all of
them: the endpoint catalog, each endpoint's params and which of them are
required are baked into the package from the same surface these docs are built
from, so the client validates your input **before** it signs and sends anything.

> Backend only. `$accessKey` signs every request — it must never reach a browser,
> a mobile app, or any template a user can view.

## Install

```bash
composer require ekoindia/eps-sdk
```

Registry: Packagist — https://packagist.org/packages/ekoindia/eps-sdk

| Fact | Value |
| --- | --- |
| Package | `ekoindia/eps-sdk` |
| Requires | PHP 8.1 or newer |
| Dependencies | None beyond the `curl` and `json` extensions. |
| Source | https://github.com/ekoindia/eps-platform/tree/main/packages/sdk-php |

- Published from a read-only mirror (`ekoindia/eps-sdk-php`), subtree-split from the monorepo.


## Your first call

Read the two keys from your secret store, pick an environment, and call an
endpoint by its slug:

```php
<?php
use Eko\Eps\EpsClient;

$client = new EpsClient(
    developerKey: getenv('EPS_DEVELOPER_KEY'),
    accessKey: getenv('EPS_ACCESS_KEY'),
    initiatorId: '9962981729',
    environment: 'sandbox'
);

$result = $client->call('pan-lite', [
    'pan_number' => 'ABCDE1234F',
    'name' => 'Rajesh Kumar',
    'dob' => '1994-08-29'
]);
print_r($result);
```

Use named arguments — the constructor takes six parameters and only the first
three are required. `$initiatorId` and `$userCode` are near-constant per
developer, so set them once on the client. They are injected into every call as
the wire params `initiator_id` and `user_code` (note the snake_case wire names)
— pass either in `$params` to override it for a single call.

## Client options

| Option | Type | Required | Notes |
| --- | --- | --- | --- |
| `$developerKey` | `string` | Yes | Your EPS developer key, sent as the `developer_key` header. |
| `$accessKey` | `string` | Yes | Server-side secret used to sign every request. Never ships to a browser. |
| `$environment` | `"sandbox" \| "production"` | Yes | Selects the base URL from the embedded surface. |
| `$initiatorId` | `string` | No | Default `initiator_id` (registered mobile of the API user) injected into every call. |
| `$userCode` | `string` | No | Default `user_code` (retailer/agent code) injected into every call. |
| `$timeout` | `float` | No | Whole-request budget, applied as `CURLOPT_TIMEOUT_MS` so sub-second values are not truncated. (seconds, default 30.0) |

## API surface

The SDK is deliberately small. There is no method per endpoint — `call()` takes
the slug, and the baked surface supplies the method, path, and validation rules.
`resolveTarget()`, `curlOptions()` and the static `decodeResponse()` are the
three seams the conformance tests drive, and they are just as useful for
debugging a request without sending it.

| Member | Kind | Signature | What it does |
| --- | --- | --- | --- |
| `Eko\Eps\EpsClient` | class | `new EpsClient(string $developerKey, string $accessKey, string $environment, ?string $initiatorId = null, ?string $userCode = null, float $timeout = 30.0)` | The client. Use named arguments. |
| `call` | method | `$client->call(string $slug, array $params = []): array` | Validates, signs and sends one endpoint call; returns the decoded envelope. |
| `resolveTarget` | method | `$client->resolveTarget(string $slug, array $params = []): array` | The signed url/body/method for a call, without sending it. Exposed for testing. |
| `curlOptions` | method | `$client->curlOptions(array $target): array` | The cURL option map `call()` uses, including the timeout. Exposed for testing. |
| `decodeResponse` | method | `EpsClient::decodeResponse(int $status, string $url, string $raw): array` | Static. Applies the shared response contract to one raw response — the seam the conformance tests drive. |
| `signSecretKey` | method | `EpsClient::signSecretKey(string $accessKey, string $timestamp): string` | Static. The raw signing primitive, exposed for debugging. |
| `EpsClient::MULTIPART_JSON_FIELD` | constant | `const MULTIPART_JSON_FIELD = 'form-data'` | Name of the single form field carrying the JSON envelope on file-upload endpoints. |

## Authentication

You never compute a signature yourself. On every request the client derives
`secret-key = base64(HMAC-SHA256(timestamp, base64($accessKey)))` and sends it
with `secret-key-timestamp` and `developer_key`. `EpsClient::signSecretKey()` is
public only so you can reproduce a signature while debugging a `403`.

## File uploads

A single `type: "file"` param flips the whole request to `multipart/form-data`.
You still pass every parameter flat; on the wire the SDK packs them the way the
API expects — one form field named `form-data` holding all the non-file params
as a single JSON object, plus one part per upload. A `null` param is dropped (a
form field has no null encoding), while a `null` nested inside an array value is
preserved.

File params accept:

- A local file path (wrapped in a `CURLFile` for you)
- A `\CURLFile` you built yourself

## Errors and timeouts

A non-2xx response **throws** — an auth or infrastructure failure is never
returned as if it were a result. The decoded envelope is still on the exception,
so you can read `status` and `message` off `->body`.

| Type | Raised when | Fields |
| --- | --- | --- |
| `Eko\Eps\EpsHttpException` | Any non-2xx response. | `->status`, `->url`, `->body` (decoded envelope or null), `->raw` |
| `Eko\Eps\EpsException` | Transport failure, a malformed surface asset, or a 2xx body that is not JSON. Extends `\RuntimeException`. | — |
| `\InvalidArgumentException` | Unknown environment or slug, missing required param, wrong param type. SPL already has the right class, and it is a `\LogicException`, so it cannot share a base with the runtime failures. | — |

| HTTP status | Meaning |
| --- | --- |
| 200 | OK — response returned by our system. |
| 403 | Forbidden — incorrect secret-key or timestamp. |
| 404 | Not Found — wrong request URL. |
| 405 | Method Not Allowed — incorrect HTTP method. |
| 415 | Unsupported Media Type — wrong Content-Type header. |
| 500 | Internal Server Error — connectivity or URL misconfiguration. |


## Environments

Switch with the `$environment` argument; there is no base-URL override.

| Environment | Base URL | Notes |
| --- | --- | --- |
| `sandbox` (UAT / Sandbox) | https://staging.eko.in/ekoapi/v3 | Self-serve credentials available immediately on signup. |
| `production` (Production) | https://api.eko.in/ekoicici/v3 | Credentials issued after organizational KYC. |

## PHP-specific notes

- Presence is checked with `isset()`, so a param explicitly set to `null` counts as missing — matching every other SDK.

## Every endpoint

`call()` accepts every slug in the EPS catalog. Each endpoint's reference page
shows its parameters, response fields and a ready-to-paste PHP snippet.

[Browse the API reference](/docs)
