Zoorya Pay Developers

Recommended merchant pattern

The safe way to take a payment, step by step.

This is the safe way to take a payment with the Checkout API. The idea is simple: the customer's browser can be faked, so your server creates the payment link and your server confirms the payment before you ship anything.

Follow these five steps for every order.

  1. 1

    Create the payment link on your server

    When the customer clicks Pay, your server logs in (or reuses a token that has not expired) and calls Create one-time link with the amount, a new externalUniqueId, a landingUrl and a callbackUrl.

    Save the externalUniqueId and paymentId against the order in your database, with status "awaiting payment".

  2. 2

    Send the customer to the link

    Redirect the customer's browser to completionUrl, or send it by SMS, Telegram or email. They choose a payment method and pay on the Zoorya Pay page.

  3. 3

    The customer comes back

    After paying, the browser goes to your landingUrl. Show a "we are confirming your payment" page.

    This redirect is not proof of payment. Anyone can open that URL. Never release goods because of it.

  4. 4

    Receive the webhook

    Zoorya Pay POSTs to your callbackUrl when the payment finishes. Treat it as a signal only: "go and check this order". See Handling webhooks.

  5. 5

    Confirm, then fulfil

    Your server calls Get a payment with the paymentId. Release the goods only if status is SUCCESSFUL and amount matches your order.

Code#

const BASE = process.env.ZOORYA_BASE_URL; // https://sandbox.eclipse.et/eclipse-conductor/rest/v1

// Log in with the API key: token + tenantId, valid for about 15 minutes
async function login() {
  const apiKey = process.env.ZOORYA_API_KEY;
  const colon = apiKey.indexOf(":");
  const res = await fetch(`${BASE}/authentication/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ identity: apiKey.slice(0, colon), password: apiKey.slice(colon + 1) }),
  });
  if (!res.ok) throw new Error(`Log in failed: ${res.status}`);
  return res.json(); // { headerValue, tenantId, expires, … }
}

// Step 1 — create the payment link
async function startPayment(auth, order) {
  const res = await fetch(`${BASE}/tenants/${auth.tenantId}/organisations/${process.env.ZOORYA_ORG_ID}/payments`, {
    method: "POST",
    headers: { Authorization: auth.headerValue, "Content-Type": "application/json" },
    body: JSON.stringify({
      amount: order.total,
      currency: "ETB",
      type: "GLOBAL_PAYMENT_LINK",
      externalUniqueId: order.reference, // new for every payment, letters and digits only
      destinationWalletId: Number(process.env.ZOORYA_WALLET_ID),
      landingUrl: `https://shop.example.com/orders/${order.id}/done`,
      additionalFields: [
        { id: "HMACKey", value: process.env.ZOORYA_HMAC_KEY },
        { id: "callbackUrl", value: "https://shop.example.com/webhooks/zoorya" },
      ],
    }),
  });
  if (!res.ok) throw new Error(JSON.stringify(await res.json()));
  const payment = await res.json();
  return { paymentId: payment.paymentId, url: payment.completionUrl }; // send the customer to url
}

// Step 5 — confirm before fulfilling
async function isPaid(auth, paymentId, expectedAmount) {
  const res = await fetch(`${BASE}/tenants/${auth.tenantId}/payments/${paymentId}`, {
    headers: { Authorization: auth.headerValue },
  });
  if (!res.ok) return false;
  const payment = await res.json();
  return payment.status === "SUCCESSFUL" && Number(payment.amount) === Number(expectedAmount);
}

Also check#

  • Reuse the token until expires instead of logging in for every payment. Log in again when a call returns 401.
  • You have not already fulfilled this order. A webhook can arrive more than once.
  • Keep the create response on your server — it contains your HMACKey. Send only completionUrl to the customer.