erlang_pay - pure-Erlang payment gateway library for Alipay, WeChat Pay v3 and Stripe

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 Cfg map; 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 + jsone only. 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.

9 Likes

@leeyis Thanks for sharing! Could you please get rid of jsone and use Erlang’s built-in JSON module instead?

Sure, I’ll release the updated version later.

1 Like

**

[0.3.1] - 2026-09-28**

Changed / 变更

  • 移除 jsone 依赖,JSON 编解码改用 OTP 27+ 内置 json 模块 —— 运行时零第三方依赖。 Dropped the jsone dependency in favour of the OTP 27+ built-in json module — zero third-party runtime dependencies.
  • 最低版本要求提升至 Erlang/OTP 27(json 模块随 27 引入)。 Minimum supported Erlang/OTP raised to 27 (json was introduced in OTP 27).
1 Like

@leeyis that was quick. Thanks.