erlang_pay is a pure-Erlang payment gateway library providing one unified API over three gateways: Alipay (App pay), WeChat Pay v3 (JSAPI/Native) and Stripe (PaymentIntent). It was extracted from a production project of ours and released as a standalone library under Apache-2.0.
Why
Integrating Chinese payment gateways from Erlang usually means shelling out to a Java/Go SDK or hand-rolling signature verification. erlang_pay implements the protocols natively in Erlang — no ports, no NIFs, no external services.
Features
- One facade, three gateways — differences are isolated in tagged result maps, so calling code stays gateway-agnostic.
- Full lifecycle — create payment, refund, webhook verification, query, bill download, close, cancel.
- Verify-then-parse everywhere — WeChat responses & callbacks, Alipay responses & notifications, Stripe webhooks: a failed signature check never falls through to parsing the payload.
- Idempotent refunds (Stripe) — a stable refund number is required; without an idempotency key, nothing is sent.
- Credentials stay out of config — everything is passed in per call via a
Cfgmap; the library reads no application env. - Money is integer-only — amounts are always in the smallest currency unit (fen/cents); no floats anywhere.
- Tiny dependency footprint — OTP +
jsoneonly. Outbound traffic is https-only with TLS certificate and hostname verification.
Quick start
Alipay App pay:
Cfg = #{app_id => AppId, private_key => MchPriPem, public_key => AlipayPubPem,
notify_url => <<"https://example.com/pay/callback/alipay">>},
{ok, #{order_str := OrderStr}} =
erlang_pay:create_payment(alipay, Cfg, #{out_trade_no => <<"R123">>,
amount_fen => 1000}).
%% hand OrderStr to the client-side AlipaySDK
Stripe PaymentIntent:
Cfg = #{secret_key => <<"sk_...">>, webhook_secret => <<"whsec_...">>},
{ok, #{payment_no := Pi, client_secret := Cs}} =
erlang_pay:create_payment(stripe, Cfg, #{out_trade_no => <<"R123">>,
amount_fen => 1000, currency => <<"usd">>}).
Verifying a webhook (raw bytes are verified first, then parsed):
{ok, Event} = erlang_pay:verify_notify(wechat, Cfg, #{headers => H, body => RawBody}).
Design notes
- All failures return a uniform
{error, {Code, Msg}}with a documented set of codes (bad_signature,timestamp_expired,no_credential,http_error, …). - Missing credentials fail closed (
no_credential) instead of crashing; secrets and signed payloads never reach the logs. - A verified webhook event only proves authenticity — order matching, idempotency and posting decisions (
ORDER_MATCHED / IDEMPOTENT / POSTABLE) are deliberately left to the caller. - A timed-out request (
http_error) is treated as result unknown: query before retrying.
Status & testing
Current release is 0.3.0. It ships with 213 EUnit tests using real RSA key pairs and real signatures (mocking only at the HTTP boundary), a clean Dialyzer run, and a full clean-build gate script. One honest caveat: verification against the live provider sandboxes is still in progress, so please treat it as pre-1.0 — bug reports and feedback are very welcome.
Install with rebar3:
{deps, [{erlang_pay, {git, "https://github.com/imboy-pub/erlang_pay.git", {tag, "0.3.0"}}}]}.
A Gitee mirror is available at https://gitee.com/imboy-pub/erlang_pay. Licensed under Apache-2.0.