SDKs & code examples

The portal’s SDKs & Codegen tab has samples for 5 languages × 3 HTTP clients. They’re a useful starting point, but several won’t work as published (see 8.7). The examples below are complete, minimal clients that avoid those problems. Each one:

  • reads every value from configuration (3.2)
  • builds URLs by plain string joining, so /sandbox/payment/v1 is never dropped
  • sends the exact field names, including charginginformation, chargeMetaData and terminalID
  • reads the status from status, falling back to the names the SDK samples use
  • keeps the three first-run details (amount format, refund tranType, merchant values) in one place
Language Client Section
PHP / Laravel Laravel Http facade 8.1
JavaScript Node.js 18+ fetch 8.2
Python requests 8.3
Java Spring RestClient (Spring Boot 3.2+) 8.4
C# .NET 8 HttpClient 8.5

The full, verbatim portal samples for all 15 clients are in reference/ECOCASH-EIP-SDK-REFERENCE.md.

8.1 PHP / Laravel

config/services.php:

'ecocash' => [
    'base_url'            => env('EIP_BASE_URL', 'https://developers.ecocash.co.zw/sandbox/payment/v1'),
    'username'            => env('EIP_USERNAME'),
    'password'            => env('EIP_PASSWORD'),
    'merchant_code'       => env('EIP_MERCHANT_CODE'),
    'merchant_pin'        => env('EIP_MERCHANT_PIN'),
    'merchant_number'     => env('EIP_MERCHANT_NUMBER'),
    'terminal_id'         => env('EIP_TERMINAL_ID'),
    'merchant_name'       => env('EIP_MERCHANT_NAME'),
    'super_merchant_name' => env('EIP_SUPER_MERCHANT_NAME'),
    'channel'             => env('EIP_CHANNEL', 'WEB'),
    'location'            => env('EIP_LOCATION', 'Harare'),
    'refund_tran_type'    => env('EIP_REFUND_TRAN_TYPE', 'REF'),
],

app/Services/EcoCash.php:

<?php

namespace App\Services;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class EcoCash
{
    private function http(): PendingRequest
    {
        return Http::baseUrl(rtrim(config('services.ecocash.base_url'), '/'))
            ->withBasicAuth(config('services.ecocash.username'), config('services.ecocash.password'))
            ->acceptJson()
            ->asJson()
            ->timeout(30);
    }

    /** Fields every charge and refund carries. */
    private function merchant(): array
    {
        $c = config('services.ecocash');

        return [
            'merchantCode'      => $c['merchant_code'],
            'merchantPin'       => $c['merchant_pin'],
            'merchantNumber'    => $c['merchant_number'],
            'countryCode'       => 'ZW',
            'terminalID'        => $c['terminal_id'],
            'location'          => $c['location'],
            'superMerchantName' => $c['super_merchant_name'],
            'merchantName'      => $c['merchant_name'],
        ];
    }

    private function amount(string $amount, string $currency, string $description): array
    {
        return [
            'charginginformation' => [
                'amount'      => number_format((float) $amount, 2, '.', ''), // "2.00" - see section 4.5
                'currency'    => $currency,
                'description' => $description,
            ],
            'chargeMetaData' => ['channel' => config('services.ecocash.channel')],
        ];
    }

    /** Start a charge. Persist $correlator BEFORE calling this. */
    public function charge(string $msisdn, string $amount, string $currency, string $reference,
                           string $correlator, string $notifyUrl = ''): array
    {
        $payload = [
            'clientCorrelator'           => $correlator,
            'notifyUrl'                  => $notifyUrl,
            'referenceCode'              => $reference,
            'tranType'                   => 'MER',
            'endUserId'                  => $msisdn,
            'remarks'                    => $reference,
            'transactionOperationStatus' => 'Charged',
            'paymentAmount'              => $this->amount($amount, $currency, $reference),
        ] + $this->merchant();

        return $this->http()->post('/transactions/amount/', $payload)->throw()->json();
    }

    public function lookup(string $msisdn, string $correlator): array
    {
        return $this->http()
            ->get('/'.rawurlencode($msisdn).'/transactions/amount/'.rawurlencode($correlator))
            ->throw()->json();
    }

    public function refund(string $msisdn, string $originalReference, string $amount,
                           string $currency, string $reference): array
    {
        $payload = [
            'clientCorrelator'         => (string) Str::uuid(), // a NEW correlator
            'referenceCode'            => $reference,
            'tranType'                 => config('services.ecocash.refund_tran_type'),
            'endUserId'                => $msisdn,
            'originalEcocashReference' => $originalReference,
            'remarks'                  => $reference,
            'paymentAmount'            => $this->amount($amount, $currency, 'Refund'),
        ] + $this->merchant();

        return $this->http()->post('/transactions/refund/', $payload)->throw()->json();
    }

    /** The API documents `status`; the portal's SDK samples read two other names. */
    public static function status(array $body): ?string
    {
        return $body['status'] ?? $body['transactionStatus'] ?? $body['transactionOperationStatus'] ?? null;
    }

    /** The reference a refund needs as originalEcocashReference. */
    public static function reference(array $body): ?string
    {
        return $body['ecocashReference'] ?? $body['transactionId'] ?? null;
    }
}

Usage:

$eip = app(\App\Services\EcoCash::class);

$correlator = (string) \Illuminate\Support\Str::uuid();
// save $correlator on the order first, then:
$res = $eip->charge('263771234567', '10.00', 'USD', 'ORDER-1001', $correlator,
                    route('ecocash.callback'));

$status = \App\Services\EcoCash::status($eip->lookup('263771234567', $correlator)); // PENDING → SUCCESS

->throw() turns 4xx/5xx into an Illuminate\Http\Client\RequestException. Its $e->response->json() holds statusCode and statusMessage.

8.2 Node.js

Node 18+ (built-in fetch), ES modules. ecocash.js:

import { randomUUID } from 'node:crypto';

const cfg = {
  baseUrl: (process.env.EIP_BASE_URL ?? 'https://developers.ecocash.co.zw/sandbox/payment/v1').replace(/\/+$/, ''),
  auth: 'Basic ' + Buffer.from(`${process.env.EIP_USERNAME}:${process.env.EIP_PASSWORD}`).toString('base64'),
  refundTranType: process.env.EIP_REFUND_TRAN_TYPE ?? 'REF',
};

const merchant = () => ({
  merchantCode: process.env.EIP_MERCHANT_CODE,
  merchantPin: process.env.EIP_MERCHANT_PIN,
  merchantNumber: process.env.EIP_MERCHANT_NUMBER,
  countryCode: 'ZW',
  terminalID: process.env.EIP_TERMINAL_ID,
  location: process.env.EIP_LOCATION ?? 'Harare',
  superMerchantName: process.env.EIP_SUPER_MERCHANT_NAME,
  merchantName: process.env.EIP_MERCHANT_NAME,
});

const paymentAmount = (amount, currency, description) => ({
  charginginformation: { amount: Number(amount).toFixed(2), currency, description }, // "2.00" - section 4.5
  chargeMetaData: { channel: process.env.EIP_CHANNEL ?? 'WEB' },
});

async function eip(path, init = {}) {
  const res = await fetch(cfg.baseUrl + path, {
    ...init,
    headers: { Authorization: cfg.auth, 'Content-Type': 'application/json', Accept: 'application/json' },
    signal: AbortSignal.timeout(30_000),
  });
  const body = await res.json().catch(() => ({}));
  if (!res.ok) {
    const err = new Error(`EIP ${res.status}: ${body.statusMessage ?? 'request failed'}`);
    err.status = res.status;
    err.body = body;
    throw err;
  }
  return body;
}

/** Start a charge. Persist `correlator` BEFORE calling this. */
export function charge({ msisdn, amount, currency = 'USD', reference, correlator, notifyUrl = '' }) {
  return eip('/transactions/amount/', {
    method: 'POST',
    body: JSON.stringify({
      clientCorrelator: correlator,
      notifyUrl,
      referenceCode: reference,
      tranType: 'MER',
      endUserId: msisdn,
      remarks: reference,
      transactionOperationStatus: 'Charged',
      paymentAmount: paymentAmount(amount, currency, reference),
      ...merchant(),
    }),
  });
}

export function lookup(msisdn, correlator) {
  return eip(`/${encodeURIComponent(msisdn)}/transactions/amount/${encodeURIComponent(correlator)}`);
}

export function refund({ msisdn, originalReference, amount, currency = 'USD', reference }) {
  return eip('/transactions/refund/', {
    method: 'POST',
    body: JSON.stringify({
      clientCorrelator: randomUUID(), // a NEW correlator
      referenceCode: reference,
      tranType: cfg.refundTranType,
      endUserId: msisdn,
      originalEcocashReference: originalReference,
      remarks: reference,
      paymentAmount: paymentAmount(amount, currency, 'Refund'),
      ...merchant(),
    }),
  });
}

/** `status` is documented; the SDK samples read two other names. */
export const statusOf = (b) => b.status ?? b.transactionStatus ?? b.transactionOperationStatus ?? null;
export const referenceOf = (b) => b.ecocashReference ?? b.transactionId ?? null;

/** Poll every 3 s until the status is final or the timeout passes. */
export async function waitForResult(msisdn, correlator, timeoutMs = 90_000) {
  const until = Date.now() + timeoutMs;
  while (Date.now() < until) {
    const s = statusOf(await lookup(msisdn, correlator));
    if (s === 'SUCCESS' || s === 'FAILED') return s;
    await new Promise((r) => setTimeout(r, 3_000));
  }
  return 'UNKNOWN'; // not final: flag for review, never assume failure
}

Usage:

import { randomUUID } from 'node:crypto';
import { charge, waitForResult } from './ecocash.js';

const correlator = randomUUID(); // save it on the order first
await charge({ msisdn: '263771234567', amount: '10.00', reference: 'ORDER-1001', correlator });
console.log(await waitForResult('263771234567', correlator)); // SUCCESS | FAILED | UNKNOWN

Keep this on your server. Never call the EIP API from a browser or mobile app: the credentials and merchant PIN would be exposed.

8.3 Python

pip install requests, then ecocash.py:

import os
import time
import uuid

import requests

BASE_URL = os.environ.get(
    "EIP_BASE_URL", "https://developers.ecocash.co.zw/sandbox/payment/v1"
).rstrip("/")
REFUND_TRAN_TYPE = os.environ.get("EIP_REFUND_TRAN_TYPE", "REF")

session = requests.Session()
session.auth = (os.environ["EIP_USERNAME"], os.environ["EIP_PASSWORD"])
session.headers.update({"Content-Type": "application/json", "Accept": "application/json"})


class EcoCashError(Exception):
    def __init__(self, status, body):
        super().__init__(f"EIP {status}: {body.get('statusMessage', 'request failed')}")
        self.status, self.body = status, body


def _merchant():
    return {
        "merchantCode": os.environ["EIP_MERCHANT_CODE"],
        "merchantPin": os.environ["EIP_MERCHANT_PIN"],
        "merchantNumber": os.environ["EIP_MERCHANT_NUMBER"],
        "countryCode": "ZW",
        "terminalID": os.environ["EIP_TERMINAL_ID"],
        "location": os.environ.get("EIP_LOCATION", "Harare"),
        "superMerchantName": os.environ["EIP_SUPER_MERCHANT_NAME"],
        "merchantName": os.environ["EIP_MERCHANT_NAME"],
    }


def _amount(amount, currency, description):
    return {
        "charginginformation": {
            "amount": f"{float(amount):.2f}",  # "2.00" - see section 4.5
            "currency": currency,
            "description": description,
        },
        "chargeMetaData": {"channel": os.environ.get("EIP_CHANNEL", "WEB")},
    }


def _call(method, path, payload=None):
    resp = session.request(method, BASE_URL + path, json=payload, timeout=30)
    try:
        body = resp.json()
    except ValueError:
        body = {}
    if not resp.ok:
        raise EcoCashError(resp.status_code, body)
    return body


def charge(msisdn, amount, reference, correlator, currency="USD", notify_url=""):
    """Start a charge. Persist `correlator` BEFORE calling this."""
    return _call("POST", "/transactions/amount/", {
        "clientCorrelator": correlator,
        "notifyUrl": notify_url,
        "referenceCode": reference,
        "tranType": "MER",
        "endUserId": msisdn,
        "remarks": reference,
        "transactionOperationStatus": "Charged",
        "paymentAmount": _amount(amount, currency, reference),
        **_merchant(),
    })


def lookup(msisdn, correlator):
    q = requests.utils.quote
    return _call("GET", f"/{q(msisdn, safe='')}/transactions/amount/{q(correlator, safe='')}")


def refund(msisdn, original_reference, amount, reference, currency="USD"):
    return _call("POST", "/transactions/refund/", {
        "clientCorrelator": str(uuid.uuid4()),  # a NEW correlator
        "referenceCode": reference,
        "tranType": REFUND_TRAN_TYPE,
        "endUserId": msisdn,
        "originalEcocashReference": original_reference,
        "remarks": reference,
        "paymentAmount": _amount(amount, currency, "Refund"),
        **_merchant(),
    })


def status_of(body):
    """`status` is documented; the portal's SDK samples read two other names."""
    return body.get("status") or body.get("transactionStatus") or body.get("transactionOperationStatus")


def reference_of(body):
    return body.get("ecocashReference") or body.get("transactionId")


def wait_for_result(msisdn, correlator, timeout=90, every=3):
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        status = status_of(lookup(msisdn, correlator))
        if status in ("SUCCESS", "FAILED"):
            return status
        time.sleep(every)
    return "UNKNOWN"  # not final: flag for review, never assume failure

Usage:

import uuid
import ecocash

correlator = str(uuid.uuid4())  # save it on the order first
ecocash.charge("263771234567", "10.00", "ORDER-1001", correlator)
print(ecocash.wait_for_result("263771234567", correlator))

8.4 Java / Spring Boot

Spring Boot 3.2+ (spring-boot-starter-web). application.yml:

eip:
  base-url: ${EIP_BASE_URL:https://developers.ecocash.co.zw/sandbox/payment/v1}
  username: ${EIP_USERNAME}
  password: ${EIP_PASSWORD}
  merchant-code: ${EIP_MERCHANT_CODE}
  merchant-pin: ${EIP_MERCHANT_PIN}
  merchant-number: ${EIP_MERCHANT_NUMBER}
  terminal-id: ${EIP_TERMINAL_ID}
  merchant-name: ${EIP_MERCHANT_NAME}
  super-merchant-name: ${EIP_SUPER_MERCHANT_NAME}
  channel: ${EIP_CHANNEL:WEB}
  location: ${EIP_LOCATION:Harare}
  refund-tran-type: ${EIP_REFUND_TRAN_TYPE:REF}

EcoCashClient.java. It uses Maps so the unusual field names are sent exactly as written:

package com.example.ecocash;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class EcoCashClient {

    private static final ParameterizedTypeReference<Map<String, Object>> JSON =
            new ParameterizedTypeReference<>() {};

    private final RestClient http;
    private final Map<String, Object> merchant = new LinkedHashMap<>();
    private final String channel;
    private final String refundTranType;

    public EcoCashClient(
            @Value("${eip.base-url}") String baseUrl,
            @Value("${eip.username}") String username,
            @Value("${eip.password}") String password,
            @Value("${eip.merchant-code}") String merchantCode,
            @Value("${eip.merchant-pin}") String merchantPin,
            @Value("${eip.merchant-number}") String merchantNumber,
            @Value("${eip.terminal-id}") String terminalId,
            @Value("${eip.merchant-name}") String merchantName,
            @Value("${eip.super-merchant-name}") String superMerchantName,
            @Value("${eip.location}") String location,
            @Value("${eip.channel}") String channel,
            @Value("${eip.refund-tran-type}") String refundTranType) {

        this.http = RestClient.builder()
                .baseUrl(baseUrl.replaceAll("/+$", ""))
                .defaultHeaders(h -> {
                    h.setBasicAuth(username, password);
                    h.setContentType(MediaType.APPLICATION_JSON);
                    h.setAccept(java.util.List.of(MediaType.APPLICATION_JSON));
                })
                .build();
        merchant.put("merchantCode", merchantCode);
        merchant.put("merchantPin", merchantPin);
        merchant.put("merchantNumber", merchantNumber);
        merchant.put("countryCode", "ZW");
        merchant.put("terminalID", terminalId);
        merchant.put("location", location);
        merchant.put("superMerchantName", superMerchantName);
        merchant.put("merchantName", merchantName);
        this.channel = channel;
        this.refundTranType = refundTranType;
    }

    private Map<String, Object> paymentAmount(BigDecimal amount, String currency, String description) {
        Map<String, Object> info = new LinkedHashMap<>();
        info.put("amount", amount.setScale(2, RoundingMode.HALF_UP).toPlainString()); // "2.00" - section 4.5
        info.put("currency", currency);
        info.put("description", description);
        return Map.of("charginginformation", info, "chargeMetaData", Map.of("channel", channel));
    }

    /** Start a charge. Persist the correlator BEFORE calling this. */
    public Map<String, Object> charge(String msisdn, BigDecimal amount, String currency,
                                      String reference, String correlator, String notifyUrl) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("clientCorrelator", correlator);
        body.put("notifyUrl", notifyUrl == null ? "" : notifyUrl);
        body.put("referenceCode", reference);
        body.put("tranType", "MER");
        body.put("endUserId", msisdn);
        body.put("remarks", reference);
        body.put("transactionOperationStatus", "Charged");
        body.put("paymentAmount", paymentAmount(amount, currency, reference));
        body.putAll(merchant);
        return http.post().uri("/transactions/amount/").body(body).retrieve().body(JSON);
    }

    public Map<String, Object> lookup(String msisdn, String correlator) {
        return http.get()
                .uri("/{endUserId}/transactions/amount/{clientCorrelator}", msisdn, correlator)
                .retrieve().body(JSON);
    }

    public Map<String, Object> refund(String msisdn, String originalReference, BigDecimal amount,
                                      String currency, String reference) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("clientCorrelator", UUID.randomUUID().toString()); // a NEW correlator
        body.put("referenceCode", reference);
        body.put("tranType", refundTranType);
        body.put("endUserId", msisdn);
        body.put("originalEcocashReference", originalReference);
        body.put("remarks", reference);
        body.put("paymentAmount", paymentAmount(amount, currency, "Refund"));
        body.putAll(merchant);
        return http.post().uri("/transactions/refund/").body(body).retrieve().body(JSON);
    }

    /** `status` is documented; the portal's SDK samples read two other names. */
    public static String statusOf(Map<String, Object> b) {
        for (String k : new String[] {"status", "transactionStatus", "transactionOperationStatus"}) {
            if (b != null && b.get(k) != null) return b.get(k).toString();
        }
        return null;
    }

    public static String referenceOf(Map<String, Object> b) {
        Object v = b.get("ecocashReference") != null ? b.get("ecocashReference") : b.get("transactionId");
        return v == null ? null : v.toString();
    }
}

RestClient throws HttpClientErrorException (4xx) or HttpServerErrorException (5xx). getResponseBodyAsString() holds statusCode and statusMessage.

8.5 C# / .NET

.NET 8, HttpClient with no extra packages. Note the trailing slash on the base address and no leading slash on paths: that’s what keeps /sandbox/payment/v1 in the URL.

using System.Globalization;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Nodes;

public sealed class EcoCashClient
{
    private readonly HttpClient _http;
    private readonly Dictionary<string, string> _cfg;

    public EcoCashClient(HttpClient http, Dictionary<string, string> cfg)
    {
        _cfg = cfg;
        _http = http;
        _http.BaseAddress = new Uri(cfg["EIP_BASE_URL"].TrimEnd('/') + "/");
        var token = Convert.ToBase64String(
            Encoding.UTF8.GetBytes($"{cfg["EIP_USERNAME"]}:{cfg["EIP_PASSWORD"]}"));
        _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token);
        _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        _http.Timeout = TimeSpan.FromSeconds(30);
    }

    private JsonObject Body(string correlator, string tranType, string msisdn, decimal amount,
                            string currency, string reference, string description)
    {
        return new JsonObject
        {
            ["clientCorrelator"] = correlator,
            ["referenceCode"] = reference,
            ["tranType"] = tranType,
            ["endUserId"] = msisdn,
            ["remarks"] = reference,
            ["paymentAmount"] = new JsonObject
            {
                ["charginginformation"] = new JsonObject
                {
                    ["amount"] = amount.ToString("0.00", CultureInfo.InvariantCulture), // "2.00" - section 4.5
                    ["currency"] = currency,
                    ["description"] = description,
                },
                ["chargeMetaData"] = new JsonObject { ["channel"] = _cfg["EIP_CHANNEL"] },
            },
            ["merchantCode"] = _cfg["EIP_MERCHANT_CODE"],
            ["merchantPin"] = _cfg["EIP_MERCHANT_PIN"],
            ["merchantNumber"] = _cfg["EIP_MERCHANT_NUMBER"],
            ["countryCode"] = "ZW",
            ["terminalID"] = _cfg["EIP_TERMINAL_ID"],
            ["location"] = _cfg["EIP_LOCATION"],
            ["superMerchantName"] = _cfg["EIP_SUPER_MERCHANT_NAME"],
            ["merchantName"] = _cfg["EIP_MERCHANT_NAME"],
        };
    }

    /// <summary>Start a charge. Persist the correlator BEFORE calling this.</summary>
    public Task<JsonObject> ChargeAsync(string msisdn, decimal amount, string currency,
                                        string reference, string correlator, string notifyUrl = "")
    {
        var body = Body(correlator, "MER", msisdn, amount, currency, reference, reference);
        body["notifyUrl"] = notifyUrl;
        body["transactionOperationStatus"] = "Charged";
        return SendAsync(HttpMethod.Post, "transactions/amount/", body);
    }

    public Task<JsonObject> LookupAsync(string msisdn, string correlator) =>
        SendAsync(HttpMethod.Get,
            $"{Uri.EscapeDataString(msisdn)}/transactions/amount/{Uri.EscapeDataString(correlator)}", null);

    public Task<JsonObject> RefundAsync(string msisdn, string originalReference, decimal amount,
                                        string currency, string reference)
    {
        var body = Body(Guid.NewGuid().ToString(), _cfg["EIP_REFUND_TRAN_TYPE"], msisdn,
                        amount, currency, reference, "Refund"); // a NEW correlator
        body["originalEcocashReference"] = originalReference;
        return SendAsync(HttpMethod.Post, "transactions/refund/", body);
    }

    private async Task<JsonObject> SendAsync(HttpMethod method, string path, JsonObject? body)
    {
        using var req = new HttpRequestMessage(method, path);
        if (body is not null) req.Content = JsonContent.Create(body);
        using var res = await _http.SendAsync(req);
        var text = await res.Content.ReadAsStringAsync();
        var json = (string.IsNullOrWhiteSpace(text) ? null : JsonNode.Parse(text) as JsonObject) ?? new JsonObject();
        if (!res.IsSuccessStatusCode)
            throw new HttpRequestException(
                $"EIP {(int)res.StatusCode}: {json["statusMessage"]}", null, res.StatusCode);
        return json;
    }

    /// <summary>`status` is documented; the SDK samples read two other names.</summary>
    public static string? StatusOf(JsonObject b) =>
        (b["status"] ?? b["transactionStatus"] ?? b["transactionOperationStatus"])?.ToString();

    public static string? ReferenceOf(JsonObject b) =>
        (b["ecocashReference"] ?? b["transactionId"])?.ToString();
}

Usage (e.g. in Program.cs, with the EIP_* values from configuration):

var cfg = new[] { "EIP_BASE_URL", "EIP_USERNAME", "EIP_PASSWORD", "EIP_MERCHANT_CODE",
                  "EIP_MERCHANT_PIN", "EIP_MERCHANT_NUMBER", "EIP_TERMINAL_ID", "EIP_MERCHANT_NAME",
                  "EIP_SUPER_MERCHANT_NAME", "EIP_CHANNEL", "EIP_LOCATION", "EIP_REFUND_TRAN_TYPE" }
    .ToDictionary(k => k, k => Environment.GetEnvironmentVariable(k) ?? "");

var eip = new EcoCashClient(new HttpClient(), cfg);
var correlator = Guid.NewGuid().ToString();   // save it on the order first
await eip.ChargeAsync("263771234567", 10.00m, "USD", "ORDER-1001", correlator);
var status = EcoCashClient.StatusOf(await eip.LookupAsync("263771234567", correlator));

8.6 Polling pattern

Prefer callbacks (notifyUrl) in production. Poll when you have no callback, or to confirm one:

  1. Wait about 3 seconds after the charge.
  2. Look it up every 3 seconds. Sandbox transactions usually resolve in 12–30 seconds.
  3. Stop at SUCCESS or FAILED.
  4. After about 90 seconds, stop polling and mark the payment unknown, not failed. Re-check it later in a background job before telling the customer anything final.

8.7 Pitfalls in the portal’s SDK samples

The portal’s SDKs & Codegen tab has 15 clients. If you copy one, fix these first:

Pitfall Affects Fix
The status field is read under two different names: transactionStatus (Java, PHP, JavaScript, C#) and transactionOperationStatus (Python). The API documents status. All 15 Read status first and fall back to the others, as the examples above do
The base path is dropped. A base URL with a path plus a request path starting with / loses /sandbox/payment/v1 in some clients. PHP Guzzle, PHP Symfony HttpClient, C# HttpClient, Python aiohttp (high risk); C# RestSharp (verify) Give the base URL a trailing / and drop the leading / from paths, or join strings yourself
Refunds send tranType: "MER" plus an undocumented currencyCode All 15 refund samples See 4.5
amount is a string ("5.00") All 15 Fine. Keep it consistent (4.5).
Hard-coded "REF-001" correlator Most samples Generate a new unique value per attempt (only Java Feign and C# Refit do)
C# HttpClient uses await using on a class that is only IDisposable C# HttpClient Use using var, or implement IAsyncDisposable. It won’t compile as published.
Refit auth: username/password are undeclared, using System;/using System.Text; are missing, and Refit sends Bearer by default C# Refit Declare them, add the usings, and put [Headers("Authorization: Basic")] on the interface
DTO types are never defined (PaymentRequest, RefundRequest, PaymentAmount, ChargingInfo, ChargeMetaData, TransactionResponse) Java, C# Refit/RestSharp Write them yourself, with explicit JSON names (charginginformation, terminalID, chargeMetaData)
btoa for Basic auth JS Axios, Fetch Use Buffer.from(...).toString('base64') in Node.js
Got’s prefixUrl throws if a path starts with / JS Got Keep paths without a leading /, as the sample does

This page is generated from section 8 of the README in README.md. Spotted something wrong? Open an issue.


Back to top

MIT licensed. Written and maintained by John Mugabe under 67even. Independent and community-maintained - not affiliated with or endorsed by EcoCash Holdings Zimbabwe. "EcoCash" and the EcoCash logo belong to their owner.