Skip to content

Iranian payment gateways

iranian-payment-gateways

Integrate Zarinpal and other Shaparak redirect gateways with a server-side verify call, Rial and Toman amounts, and one paid transition per authority. Use when adding an Iranian payment gateway or fixing callback, verify, or amount handling for IRR or IRT stores.

Version 0.3.2  ·  MIT  ·  Source  ·  Rule

Install this skill

Copy the skill and its rule into your project:

npx @sadrmanesh/commerce-agent-skills add iranian-payment-gateways

The skill goes to .agents/skills/ and the rule to .agents/rules/. See Command line for other directories.

Iranian card payments run through Shaparak, the national card switch, behind a payment facilitator or bank gateway such as Zarinpal. They share one flow: request a token, redirect the shopper, receive a callback, and verify on the server. Zarinpal request and verify fields are in the Zarinpal notes. The general rules in the payment gateway skill still apply.

Procedure

  1. Create the shop order first, unpaid, with the total in the store currency.
  2. Convert the total to the unit the gateway expects. Zarinpal accepts currency IRR (Rial) or IRT (Toman). Bank gateways usually take Rial only. One Toman is ten Rial.
  3. Send the request call from the server with merchant_id, amount, callback_url, description, and the order id in metadata. Store the returned authority on the order before redirecting.
  4. Redirect to the gateway's start page, for Zarinpal https://payment.zarinpal.com/pg/StartPay/{authority}.
  5. On the callback, look up the order by Authority. If Status is NOK, the payment failed or was canceled: keep the order unpaid.
  6. If Status is OK, call verify from the server with the same amount and authority. The callback query is not proof of payment.
  7. Mark the order paid only when verify returns success, and store ref_id, the masked card_pan, and the gateway fee.

Verify results

Verify code Meaning Shop effect
100 Verified now Mark paid once. Show ref_id to the shopper.
101 Already verified earlier Paid. Do not send a second receipt or reduce stock again.
Any other code Not verified Leave unpaid and show the gateway message.

Run node scripts/check-verify.mjs order.json verify.json to see what a stored order and a verify response mean together. It flags a mismatched amount, a mismatched authority, and the codes above.

Amounts and currency

  • Store IRR or IRT on the order and convert in one function. A store priced in Toman that sends Toman to a Rial-only gateway charges a tenth of the price.
  • Amounts are whole numbers. Reject a fractional amount instead of rounding it silently.
  • Send the same amount and unit in request and verify. Verify fails when they differ.
  • Show prices in the store's unit with Persian digits if the storefront is Persian, but keep Latin digits in API calls.

Unverified payments

If the shopper pays but your server never verifies (a closed tab, a timeout, a crash), Shaparak gateways return the money to the payer after a window the provider sets. Run a job that lists unverified authorities for recent unpaid orders and verifies them, so a paid shopper is not refunded by mistake. Zarinpal offers an unverified transactions call for this.

Gotchas

  • Verifying in the browser exposes merchant_id and lets a shopper fake success. Verify only from the server.
  • The callback URL can be opened twice: on a refresh, or by the shopper going back. The second verify returns 101, so treat it as already paid, not as an error.
  • Matching the callback by order id in the query string lets a shopper swap orders. Match by Authority from your own record.
  • Some gateways reject callback URLs on a different domain from the registered site. Register the exact domain.
  • Gateways often need an Iranian server IP or a whitelisted IP for API calls. A timeout from abroad is not a declined payment.

Verification

  • A callback with Status=NOK leaves the order unpaid and does not call verify.
  • A callback with Status=OK but a verify code other than 100 or 101 leaves the order unpaid.
  • Opening the callback URL twice sends one receipt.
  • A Toman store sends currency: "IRT" to Zarinpal, or ten times the amount in Rial to a Rial-only gateway.
  • An order paid without a callback is found and verified by the reconciliation job.

Evaluation

Worked examples and the checks they must satisfy are in evals/tasks.json.

References

Zarinpal v4 notes

Base URL: https://payment.zarinpal.com/pg/v4/payment/. Bodies are JSON. Send Content-Type: application/json and Accept: application/json. A sandbox runs on sandbox.zarinpal.com with the same paths.

Request

POST request.json

Field Required Notes
merchant_id yes 36-character merchant code. Keep it on the server.
amount yes Whole number
currency no IRR (Rial) or IRT (Toman)
description yes Shown to the shopper
callback_url yes Return page on the registered domain
metadata no mobile, email, and order_id

Success: data.code is 100 and data.authority holds the payment token. Then redirect to https://payment.zarinpal.com/pg/StartPay/{authority}.

Callback

The shopper returns to callback_url?Authority=...&Status=OK or Status=NOK. Only OK is worth verifying.

Verify

POST verify.json with merchant_id, amount, and authority.

Field Notes
data.code 100 verified now, 101 verified before
data.ref_id Payment reference to show the shopper and store on the order
data.card_pan Masked card number, such as 502229******5995
data.card_hash SHA-256 hash of the card, useful for risk checks without the number
data.fee_type, data.fee Who paid the fee, and how much

Errors arrive in errors with a negative code and a message. Log the code and message with the order id, never the merchant_id.

Example

curl -X POST https://payment.zarinpal.com/pg/v4/payment/verify.json \
  -H 'content-type: application/json' -H 'accept: application/json' \
  -d '{"merchant_id":"'"$ZARINPAL_MERCHANT_ID"'","amount":1250000,"authority":"A000000000000000000000000000abcdef"}'

Evaluation

Fixtures from evals/tasks.json. See evaluation fixtures for how the checks work.

zarinpal-callback

Write the Zarinpal callback handler for a store: what it checks, which call it makes, and when the order becomes paid.

Must include: Authority, Status, verify, ref_id, 101

Must not include: mark the order paid when Status=OK

Worked example

Find the order by the Authority query value stored when the request call returned. If Status is NOK, keep the order unpaid and show a retry link.

If Status is OK, call verify.json from the server with merchant_id, the same amount, and the authority. Code 100 marks the order paid and stores ref_id and the masked card_pan. Code 101 means it was verified before, so show the receipt without sending another email or reducing stock. Any other code leaves the order unpaid.

toman-store

A WooCommerce store prices in Toman (IRT) and must charge 125,000 Toman through a bank gateway that only accepts Rial. What amount is sent?

Must include: 1250000, IRR, IRT

Must not include: amount: 125000 to the Rial gateway

Worked example

One Toman is ten Rial, so 125,000 IRT becomes 1250000 IRR. Send amount: 1250000 to the Rial-only gateway, and send the same amount in the verify call.

With Zarinpal you can instead send amount: 125000 with currency: "IRT". Keep the conversion in one function and store the order currency as IRT.