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:
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¶
- Create the shop order first, unpaid, with the total in the store currency.
- Convert the total to the unit the gateway expects. Zarinpal accepts
currencyIRR(Rial) orIRT(Toman). Bank gateways usually take Rial only. One Toman is ten Rial. - Send the request call from the server with
merchant_id,amount,callback_url,description, and the order id inmetadata. Store the returnedauthorityon the order before redirecting. - Redirect to the gateway's start page, for Zarinpal
https://payment.zarinpal.com/pg/StartPay/{authority}. - On the callback, look up the order by
Authority. IfStatusisNOK, the payment failed or was canceled: keep the order unpaid. - If
StatusisOK, call verify from the server with the sameamountandauthority. The callback query is not proof of payment. - Mark the order paid only when verify returns success, and store
ref_id, the maskedcard_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
IRRorIRTon 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
amountand 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_idand 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
Authorityfrom 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=NOKleaves the order unpaid and does not call verify. - A callback with
Status=OKbut a verify code other than100or101leaves 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.