ECASHPAYECASHPAYDocumentation
Back to site

Introduction

  • EcashPay API
  • Quickstart

Core concepts

  • Environments and API keys
  • Signing requests
  • Errors

Accept payments

  • Creating a payment
  • Payment methods
  • Confirming the outcome
  • Refunds, capture and cancel

Testing

  • Testing

API reference

  • API reference

Confirming the outcome

How a payment reaches its final status, why your server must ask rather than be told, and how to poll without hammering the API.

Who tells whom

Read this before you ship. It is the part of the integration that decides whether you ever hand goods to someone who did not pay, or refuse someone who did.

EcashPay does not currently call your server when a payment finishes. There is no merchant webhook endpoint to register, no callback URL field on a payment, and no signed event delivered to you. The payment services call us, we verify what they send and update the payment — and your server finds out by asking.

So the shape of a correct integration is: start the payment, let the customer go, then ask the API — when the customer comes back, and again from a background job for the ones who never do.

Do not build a webhook receiver yet

If your platform expects one, keep the handler unwired for now. Server-to-server delivery is on the roadmap; when it exists it will be documented here with its signature scheme, and polling will keep working regardless.

Return URLs

successReturnUrl and cancelReturnUrl are where the customer lands after checkout. They are navigation, not notification:

  • A hit on the success URL is not proof of payment. It is a URL in a browser — anyone can open it, and a customer can open it early, twice, or from a shared link.
  • A missed hit is not proof of failure. Customers close tabs, lose signal and get redirected through apps that drop the last hop. The payment can be SUCCEEDED with nobody ever landing on your page.

Treat both as a hint that it is worth asking now. Put your own order reference in the URL, look the payment up server-side, and render the page from the status the API returns — never from which of the two URLs was hit.

Point them at pages that tolerate a wait, too: "we are confirming your payment" is honest for an over-the-counter rail, where the answer can be hours away.

Polling for the final status

GET /v1/payments/:paymentId is the authoritative answer. Poll it with a backoff, and stop once the status is terminal:

Polling one payment
// Poll with a backoff, and stop. Terminal statuses never change again.
const TERMINAL = new Set([
  'SUCCEEDED', 'FAILED', 'EXPIRED', 'CANCELLED', 'REFUNDED', 'PARTIALLY_REFUNDED',
]);

const DELAYS_MS = [2_000, 5_000, 10_000, 30_000, 60_000];  // then every 5 minutes

async function settle(paymentId) {
  for (const delay of DELAYS_MS) {
    const payment = await callEcashPay('GET', `/v1/payments/${paymentId}`);
    if (TERMINAL.has(payment.status)) return payment;   // done, whatever the answer
    await sleep(delay);
  }
  return null;   // still pending — hand it to the slow reconciliation job
}

Three things to get right:

  • Back off. Wallet payments settle in seconds, counter payments in hours. A fixed one-second loop will earn a 429 and learn nothing faster.
  • Sign every attempt afresh. Each poll is a new request: new timestamp, new nonce. Reusing them is SIGNATURE_REPLAYED.
  • Be idempotent. You will see SUCCEEDED more than once — from a poll and from a return-URL lookup at the same moment. Key your fulfilment on the payment id so the second sighting does nothing.

Reconciling in bulk

Polling per payment does not cover the customer who pays at a counter tomorrow, so run one background job that sweeps everything still open. The ledger endpoint filters for exactly that:

One sweep instead of many polls
// Every few minutes: one call for every payment still open.
const { items } = await callEcashPay(
  'GET',
  '/v1/transactions?status=PENDING,REQUIRES_ACTION,AUTHORIZED&from=2026-08-01T00:00:00Z&pageSize=200',
);

for (const transaction of items) {
  await applyStatus(transaction.referenceId, transaction.status);
}

Between the two you have full coverage: the fast loop for the customer who is still watching, and the sweep for everyone else. Add GET /v1/transactions/:txId when you need the event timeline — it shows what happened and when, which is the fastest way to answer "the customer says they paid".

The sweep is also your safety net for the two ambiguous errors. A 502 or 504 from create-payment means we could not tell whether the payment service accepted it, so never retry blindly: reuse the same referenceId, and let the duplicate guard or the sweep tell you what really exists.

PreviousPayment methodsNextRefunds, capture and cancel

On this page

  • Who tells whom
  • Return URLs
  • Polling for the final status
  • Reconciling in bulk