ExPaymob
Elixir client for the Paymob payment gateway. Supports Egypt, UAE, KSA, and Oman regions.
Features
- Payment Intentions - Create and manage payments via the V1 API
- Transaction Management - Retrieve, inquire, refund, void, and capture
- Subscriptions - Plan and subscription CRUD
- Webhook Verification - HMAC-SHA512 with timing-safe comparison
- Phoenix Integration - Plug for webhook endpoints with automatic verification
- Multi-Region - Egypt, UAE, KSA, Oman with per-request overrides
- Swappable HTTP Client - Default Req adapter, bring your own via behaviour
- Igniter Installer - One command Phoenix setup
Installation
Add ex_paymob to your dependencies:
def deps do
[
{:ex_paymob, "~> 0.1.0"}
]
endFor Phoenix projects, run the installer after adding the dependency:
mix deps.get
mix ex_paymob.installConfiguration
# config/config.exs
config :ex_paymob,
region: :egypt # :egypt | :uae | :ksa | :oman
# config/runtime.exs
config :ex_paymob,
secret_key: System.get_env("PAYMOB_SECRET_KEY"),
public_key: System.get_env("PAYMOB_PUBLIC_KEY"),
hmac_secret: System.get_env("PAYMOB_HMAC_SECRET")Every option can be overridden per-request:
ExPaymob.Intention.create(params, secret_key: "sk_other", region: :uae)Configuration Resolution
Values are resolved in order (first match wins):
- Per-request keyword opts
-
Application environment (
config :ex_paymob, key: value) -
System environment (
PAYMOB_SECRET_KEY,PAYMOB_PUBLIC_KEY,PAYMOB_HMAC_SECRET)
Regions
| Region | Base URL |
|---|---|
:egypt (default) | https://accept.paymob.com |
:uae | https://uae.paymob.com |
:ksa | https://ksa.paymob.com |
:oman | https://oman.paymob.com |
Usage
Payment Intentions
# Create a payment intention
{:ok, intention} = ExPaymob.Intention.create(%{
amount: 10000,
currency: "EGP",
payment_methods: [integration_id],
billing_data: %{
first_name: "John",
last_name: "Doe",
email: "john@example.com",
phone_number: "+201234567890"
},
items: []
})
# Build checkout redirect URL
url = ExPaymob.Intention.checkout_url(intention["client_secret"], "pk_public_key")
# Update an intention
{:ok, updated} = ExPaymob.Intention.update(intention["client_secret"], %{amount: 20000})Transactions
# Retrieve by ID
{:ok, txn} = ExPaymob.Transaction.retrieve("12345")
# Inquire by merchant order ID
{:ok, txn} = ExPaymob.Transaction.inquire(%{merchant_order_id: "order_123"})Refund, Void, and Capture
# Refund (partial or full)
{:ok, _} = ExPaymob.Refund.create("transaction_id", 5000)
# Void a transaction
{:ok, _} = ExPaymob.Void.create("transaction_id")
# Capture an authorized transaction
{:ok, _} = ExPaymob.Capture.create("transaction_id", 10000)Subscriptions
# Create a plan
{:ok, plan} = ExPaymob.SubscriptionPlan.create(%{
name: "Premium",
amount: 5000,
currency: "EGP",
interval: "month"
})
# Manage plans
{:ok, _} = ExPaymob.SubscriptionPlan.suspend(plan["id"])
{:ok, _} = ExPaymob.SubscriptionPlan.resume(plan["id"])
{:ok, plans} = ExPaymob.SubscriptionPlan.list()
# Create a subscription
{:ok, sub} = ExPaymob.Subscription.create(%{plan_id: plan["id"]})
{:ok, _} = ExPaymob.Subscription.suspend(sub["id"])
{:ok, _} = ExPaymob.Subscription.resume(sub["id"])Error Handling
All API calls return {:ok, map()} or {:error, %ExPaymob.Error{}}:
case ExPaymob.Intention.create(params) do
{:ok, intention} ->
intention["client_secret"]
{:error, %ExPaymob.Error{source: :paymob, status: 422, message: message}} ->
Logger.error("Validation error: #{message}")
{:error, %ExPaymob.Error{source: :network, message: message}} ->
Logger.error("Network error: #{message}")
end
Error sources: :paymob (API errors), :network (transport failures), :internal (decode errors).
Webhook Verification
Paymob signs transaction callbacks with HMAC-SHA512. ExPaymob verifies these signatures using timing-safe comparison.
Standalone Verification
# Verify and parse in one step
{:ok, event} = ExPaymob.Webhook.verify_and_parse(payload, hmac_from_query)
# Or verify separately
:ok = ExPaymob.Webhook.verify_hmac(payload, hmac, hmac_secret: "secret")
# Subscription webhooks use a different format
:ok = ExPaymob.Webhook.verify_subscription_hmac(payload, hmac)Phoenix Webhook Endpoint
Step 1: Configure raw body caching in your endpoint (before Plug.Parsers):
# lib/my_app_web/endpoint.ex
plug Plug.Parsers,
parsers: [:json],
pass: ["application/json"],
json_decoder: Jason,
body_reader: {ExPaymob.Plug.RawBodyReader, :read_body, []}Step 2: Add the webhook route in your router:
# lib/my_app_web/router.ex
scope "/webhooks" do
pipe_through :api
forward "/paymob", ExPaymob.Plug.WebhookPlug,
handler: MyAppWeb.PaymobWebhookHandler
endStep 3: Implement your handler:
defmodule MyAppWeb.PaymobWebhookHandler do
def handle_event(conn, event) do
# event is the "obj" map from the webhook payload
if event["success"] do
# Payment succeeded - update your order, send confirmation, etc.
else
# Payment failed
end
# Return conn. The plug sends 200 automatically if you don't send a response.
conn
end
endThe plug handles HMAC verification automatically — returns 401 for invalid signatures, 400 for malformed requests.
Custom HTTP Client
Replace the default Req adapter by implementing ExPaymob.HttpClient:
defmodule MyApp.CustomHttpClient do
@behaviour ExPaymob.HttpClient
@impl true
def request(method, url, headers, body, opts) do
# Your HTTP implementation
# Must return {:ok, status, resp_headers, resp_body} | {:error, reason}
end
end# Global
config :ex_paymob, http_client: MyApp.CustomHttpClient
# Per-request
ExPaymob.Intention.create(params, http_client: MyApp.CustomHttpClient)Testing
For testing, use Mox with the ExPaymob.HttpClient behaviour:
# test/test_helper.exs
Mox.defmock(ExPaymob.HttpClientMock, for: ExPaymob.HttpClient)
Application.put_env(:ex_paymob, :http_client, ExPaymob.HttpClientMock)
# test/my_test.exs
import Mox
test "handles successful payment" do
expect(ExPaymob.HttpClientMock, :request, fn :post, _url, _headers, _body, _opts ->
{:ok, 200, [], Jason.encode!(%{"id" => "123", "client_secret" => "cs_test"})}
end)
assert {:ok, %{"id" => "123"}} = ExPaymob.Intention.create(%{amount: 1000})
endLicense
MIT - see LICENSE for details.