Azupay
Elixir client library for the AzuPay Payments API.
Installation
Add azupay to your list of dependencies in mix.exs:
def deps do
[
{:azupay, "~> 0.3.0"}
]
endConfiguration
Configure environments in your config/runtime.exs:
config :azupay,
environments: [
uat: [
base_url: "https://api-uat.azupay.com.au/v1",
api_key: System.get_env("AZUPAY_UAT_API_KEY"),
client_id: System.get_env("AZUPAY_UAT_CLIENT_ID")
],
prod: [
base_url: "https://api.azupay.com.au/v1",
api_key: System.get_env("AZUPAY_PROD_API_KEY"),
client_id: System.get_env("AZUPAY_PROD_CLIENT_ID")
]
]Usage
Creating a Client
# Create a client for an environment
client = Azupay.Client.new(environment: :uat)
# Shortcut for UAT
client = Azupay.Client.uat()Payment Requests
Generate PayID or virtual account details for receiving payments.
# Create a payment request
{:ok, response} = Azupay.Client.PaymentRequests.create(client, %{
"payID" => "user@example.com",
"payIDType" => "EMAIL",
"amount" => "100.00",
"clientTransactionId" => "txn-123"
})
# Get a payment request
{:ok, response} = Azupay.Client.PaymentRequests.get(client, "request-id")
# Search payment requests
{:ok, results} = Azupay.Client.PaymentRequests.search(client, %{
"fromDate" => "2025-01-01T00:00:00Z",
"toDate" => "2025-01-31T23:59:59Z"
})
# Refund a payment request (full or partial)
{:ok, response} = Azupay.Client.PaymentRequests.refund(client, "request-id")
{:ok, response} = Azupay.Client.PaymentRequests.refund(client, "request-id", refund_amount: "50.00")
# Delete a payment request
{:ok, response} = Azupay.Client.PaymentRequests.delete(client, "request-id")Payments
Disburse funds via PayID or BSB/account number.
# Create a payment
{:ok, response} = Azupay.Client.Payments.create(client, %{
"clientPaymentId" => "pay-123",
"amount" => "250.00",
"payID" => "recipient@example.com",
"payIDType" => "EMAIL"
})
# Get a payment
{:ok, response} = Azupay.Client.Payments.get(client, "payment-id")
# Search payments
{:ok, results} = Azupay.Client.Payments.search(client, %{
"clientPaymentId" => "pay-123"
})Payment Initiations (PayTo)
Collect funds from payers with active payment agreements.
# Create a payment initiation
{:ok, response} = Azupay.Client.PaymentInitiations.create(client, %{
"clientTransactionId" => "pi-123",
"amount" => "100.00",
"paymentAgreementId" => "agreement-id"
})
# Get a payment initiation
{:ok, response} = Azupay.Client.PaymentInitiations.get(client, "initiation-id")
# Refund a payment initiation
{:ok, response} = Azupay.Client.PaymentInitiations.refund(client, "initiation-id")Payment Agreements (PayTo Mandates)
# Create a payment agreement
{:ok, response} = Azupay.Client.PaymentAgreements.create(client, %{
"contractId" => "contract-123",
"payerPayID" => "payer@example.com",
"payerPayIDType" => "EMAIL"
})
# Search agreements
{:ok, results} = Azupay.Client.PaymentAgreements.search(client, %{
"contractId" => "contract-123"
})
# Amend an agreement
{:ok, response} = Azupay.Client.PaymentAgreements.amend(client, %{
"paymentAgreementId" => "agreement-id",
"amount" => "200.00"
})
# Change agreement status (ACTIVE, SUSPENDED, CANCELLED)
{:ok, response} = Azupay.Client.PaymentAgreements.change_status(client, "agreement-id", "SUSPENDED")Payment Agreement Requests
Initiate payer approval flow for PayTo agreements.
{:ok, response} = Azupay.Client.PaymentAgreementRequests.create(client, %{
"contractId" => "contract-123",
"payerPayID" => "payer@example.com",
"payerPayIDType" => "EMAIL"
})
# Response includes `sessionUrl` for redirecting the payerAccount Enquiry
# Check BSB reachability
{:ok, response} = Azupay.Client.Accounts.check_bsb(client, %{"bsb" => "062000"})
# Check PayID status
{:ok, response} = Azupay.Client.Accounts.check_payid(client, %{
"payID" => "user@example.com",
"payIDType" => "EMAIL"
})
# Confirmation of Payee (CoP)
{:ok, response} = Azupay.Client.Accounts.check_account(client, %{
"accountCheckId" => "check-123",
"bsb" => "062000",
"accountNumber" => "12345678",
"accountName" => "John Doe",
"purposeCode" => "SALARY",
"additionalDetails" => "Payroll"
})Balances
{:ok, balance} = Azupay.Client.Balances.get(client)Balance Adjustments
{:ok, response} = Azupay.Client.BalanceAdjustments.create(client, %{
"clientTransactionId" => "adj-123",
"adjustmentAmount" => "500.00",
"adjustmentType" => "CREDIT",
"reason" => "Top up",
"clientId" => "client-id"
})Reports
# List reports
{:ok, reports} = Azupay.Client.Reports.list(client, %{
"clientId" => "client-id",
"month" => "2025-01",
"timezone" => "Australia/Sydney"
})
# Get download URL
{:ok, url} = Azupay.Client.Reports.download_url(client, "client-id", "report-id")API Keys
# Create an API key for a sub-merchant
{:ok, key} = Azupay.Client.ApiKeys.create(client, %{"clientId" => "sub-merchant-id"})
# List all API keys
{:ok, keys} = Azupay.Client.ApiKeys.list(client)
# Get a specific API key
{:ok, key} = Azupay.Client.ApiKeys.get(client, "key-id")
# Update an API key
{:ok, key} = Azupay.Client.ApiKeys.update(client, "key-id", %{"enabled" => false})Sub-Client Management
# Create a sub-client
{:ok, response} = Azupay.Client.Clients.create(client, %{
"clientTransactionId" => "client-123",
"name" => "Sub Merchant"
})
# Disable a sub-client
{:ok, response} = Azupay.Client.Clients.disable(client, "sub-client-id")
# Set low balance alert threshold
{:ok, response} = Azupay.Client.Clients.set_low_balance_threshold(client, "client-id", "1000.00")
# Set alert email addresses
{:ok, response} = Azupay.Client.Clients.set_alert_emails(client, "client-id", ["alerts@example.com"])PayID Domains
# List configured PayID domains
{:ok, domains} = Azupay.Client.PayIdDomains.list(client)
# Upsert PayID domains
{:ok, response} = Azupay.Client.PayIdDomains.upsert(client, [
%{"domain" => "example.com", "clientId" => "client-id"}
])
# Delete a PayID domain
{:ok, response} = Azupay.Client.PayIdDomains.delete(client, "example.com")API Resources
| Module | Description |
|---|---|
Azupay.Client.PaymentRequests | PayID/virtual account payment collection |
Azupay.Client.Payments | Fund disbursement via PayID or BSB |
Azupay.Client.PaymentInitiations | PayTo fund collection |
Azupay.Client.PaymentAgreements | PayTo recurring mandates |
Azupay.Client.PaymentAgreementRequests | PayTo payer approval flow |
Azupay.Client.Accounts | BSB, PayID, and account enquiry |
Azupay.Client.Balances | Account balance |
Azupay.Client.BalanceAdjustments | Ledger adjustments |
Azupay.Client.Reports | Report listing and download |
Azupay.Client.ApiKeys | API key management |
Azupay.Client.Clients | Sub-client management |
Azupay.Client.PayIdDomains | PayID domain configuration |
Webhooks
AzuPay sends webhook notifications when payment entity statuses change. This library provides a Plug to receive these webhooks using transaction-level notifications.
When creating a transaction (e.g. a payment request), you set the paymentNotification fields — paymentNotificationEndpointUrl and paymentNotificationAuthorizationHeaderValue — to configure where AzuPay sends the webhook and what Authorization header value it uses. Since the auth value is unique per transaction, your handler is responsible for looking it up and verifying it.
1. Implement a handler
defmodule MyApp.AzupayWebhookHandler do
@behaviour Azupay.Webhook.Handler
@impl true
def handle_event("PaymentRequest", payload, context) do
with :ok <- verify_authorization(payload, context) do
# Process payment request status update
:ok
end
end
def handle_event(_type, _payload, _context), do: :ok
defp verify_authorization(payload, %{authorization: auth}) do
expected = lookup_expected_auth(payload["uniqueReference"])
if Plug.Crypto.secure_compare(auth || "", expected || "") do
:ok
else
{:error, :unauthorized}
end
end
end2. Mount the plug in your router
Mount once per environment at different paths:
forward "/webhooks/azupay/uat", Azupay.Webhook.Plug,
environment: :uat,
handler: MyApp.AzupayWebhookHandler
forward "/webhooks/azupay/prod", Azupay.Webhook.Plug,
environment: :prod,
handler: MyApp.AzupayWebhookHandlerImportant notes
- Your webhook endpoint must respond within 5 seconds (AzuPay requirement).
- AzuPay delivers webhooks at least once — handle duplicates gracefully.
- Webhooks may arrive out of order — don't assume sequential delivery.
-
Webhook support requires the optional
plugdependency.
Error Handling
All functions return {:ok, data} or {:error, reason}. Error reasons include:
| Error | HTTP Status |
|---|---|
:unauthorized | 401 |
:forbidden | 403 |
:not_found | 404 |
{:validation_error, details} | 422 |
{:client_error, status, body} | Other 4xx |
{:server_error, status, body} | 5xx |
{:request_failed, reason} | Network failure |
Use Azupay.Client.Error.new/1 for structured error handling:
case Azupay.Client.PaymentRequests.create(client, params) do
{:ok, response} -> handle_success(response)
{:error, reason} ->
error = Azupay.Client.Error.new(reason)
if Azupay.Client.Error.validation_error?(error) do
handle_validation_error(error)
else
handle_error(error)
end
endMock Server
A built-in mock server provides a local HTTP implementation of the AzuPay API for integration testing and development without hitting real environments. It uses Ecto/Postgres for persistent storage and Bandit as the HTTP server.
Currently supported APIs: Payment Requests (create, get, delete, refund, search).
Setup
Add the optional dependencies to your mix.exs:
{:bandit, "~> 1.0"},
{:ecto_sql, "~> 3.10"},
{:postgrex, ">= 0.0.0"}Create a migration:
defmodule MyApp.Repo.Migrations.AddAzupayMockServer do
use Ecto.Migration
def up, do: Azupay.MockServer.Migrations.up(version: 1)
def down, do: Azupay.MockServer.Migrations.down(version: 1)
end
Run mix ecto.migrate.
Configuration
# config/test.exs or config/dev.exs
config :azupay, :mock_server,
enabled: true,
port: 4502,
repo: MyApp.Repo
# Point the client at the mock server
config :azupay,
environments: [
uat: [
base_url: "http://localhost:4502/v1",
api_key: "test_api_key",
client_id: "test_client_id"
]
]
The mock server starts automatically with your application when enabled: true and a :repo is configured.
Embedded Mode (Phoenix)
To serve the mock API through your Phoenix router instead of a standalone server:
config :azupay, :mock_server,
enabled: true,
embedded: true,
base_url: "http://localhost:4000/azupay-mock",
repo: MyApp.Repo
# In your Phoenix router
forward "/azupay-mock", Azupay.MockServer.RouterUsage in Tests
setup do
Azupay.MockServer.reset()
:ok
end
test "creates and retrieves a payment request" do
client = Azupay.Client.new(environment: :uat)
{:ok, created} = Azupay.Client.PaymentRequests.create(client, %{
"clientTransactionId" => "txn-001",
"paymentDescription" => "Test payment"
})
assert created["paymentRequestId"]
assert created["status"] == "WAITING"
{:ok, fetched} = Azupay.Client.PaymentRequests.get(client, created["paymentRequestId"])
assert fetched["clientTransactionId"] == "txn-001"
endSeeding Data
{:ok, pr} = Azupay.MockServer.seed_payment_request(%{
"clientTransactionId" => "txn-seed-001",
"paymentDescription" => "Seeded payment",
"paymentAmount" => 25.00
})Simulation Endpoints
The mock server provides extra endpoints for testing scenarios that require external triggers:
| Endpoint | Description |
|---|---|
POST /mock/simulate/pay/:id | Simulate a payment received (WAITING → PAID) |
POST /_mock/reset | Clear all mock data |
GET /_mock/state | Return all stored payment requests |
# Simulate a payment via HTTP
Req.post!("http://localhost:4502/mock/simulate/pay/#{payment_request_id}")Mock API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /v1/paymentRequest | Create a payment request |
GET | /v1/paymentRequest?id=X | Get a payment request |
DELETE | /v1/paymentRequest?id=X | Delete a payment request |
POST | /v1/paymentRequest/refund?id=X | Refund a payment request |
POST | /v1/paymentRequest/search | Search payment requests |
Testing
Tests use Req's built-in plug adapter for HTTP mocking (no real HTTP calls):
client = Azupay.Client.new(
environment: :uat,
req_options: [
plug: fn conn ->
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, Jason.encode!(%{"status" => "ok"}))
end
]
)License
MIT