Every path below is written host-relative, exactly as routed. Several
panels expose the same sub-path (customer and organization both have
/credit/summary), so always pair a path with its host.
Hosts
| Base URL | Serves |
https://hmzptest.ir | Machine-to-machine APIs — POS (/api/v1/pos), bank partner (/api/v1/bank), corporate B2B (/api/v1/corporate) — plus the hosted checkout page and the inbound provider webhooks. These routes carry no panel host restriction, so they answer on any host served by the app; use the apex. |
https://customer.hmzptest.ir | Customer panel API (/api/customer) — session-authenticated |
https://merchant.hmzptest.ir | Merchant panel API (/api/merchant) — session-authenticated |
https://organization.hmzptest.ir | Organization panel API (/api/org) — session-authenticated |
https://admin.hmzptest.ir | Admin panel API (/api/admin) — session-authenticated |
Conventions
Money | All amounts are integers in Rial. No decimals, no strings. |
Idempotency | Every mutating machine-to-machine call takes an idempotency key. Replaying a key returns the original result instead of performing the action twice — retry freely on a timeout. |
Errors | Failures return {"success": false, "error": {"code": "...", "message": "..."}}. Branch on error.code, never on the message text. |
Time | Signature timestamps are Unix seconds. Your server clock must be within 300 seconds of ours. |
Three different schemes, depending on which API you are calling.
Get this right first — it is where most integrations stall.
API key + secret
/api/v1/pos/*
Send both credentials as headers on every request.
X-API-KEY: hp_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-API-SECRET: <48-character secret>
You receive the pair once, when the merchant is provisioned
(or when the merchant rotates it from their panel). The secret is stored hashed on
our side and cannot be shown again — store it at issue time.
API key + HMAC signature
/api/v1/corporate/* · /api/v1/bank/*
Sign the exact raw request body with your secret.
X-API-KEY: <your api key>
X-TIMESTAMP: <unix seconds>
X-SIGNATURE: hmac_sha256( "<X-TIMESTAMP>" + "." + <raw body>, <secret> )
Sign the bytes you actually send. Serialize the JSON
once, sign that exact string, and transmit that same string — re-serializing after
signing (different key order or spacing) produces a signature we will reject. For a
GET, the body is the empty string.
ts=$(date +%s)
body='{"account_id":42,"amount":5000000,"reference_id":"INV-1","idempotency_key":"a1b2c3"}'
sig=$(printf '%s.%s' "$ts" "$body" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)
curl -X POST https://hmzptest.ir/api/v1/corporate/credit/reserve \
-H "X-API-KEY: $API_KEY" \
-H "X-TIMESTAMP: $ts" \
-H "X-SIGNATURE: $sig" \
-H "Idempotency-Key: a1b2c3" \
-H "Content-Type: application/json" \
-d "$body"
Requests older than 300 seconds are rejected as
expired_timestamp. During a secret rotation both the current and the previous
secret are accepted, so you can roll over without downtime.
Every mutating corporate route additionally requires an
Idempotency-Key header, and it must match the idempotency_key
field in the body when the body carries one.
Session cookie
/api/customer/* · /api/merchant/* · /api/org/* · /api/admin/*
Browser-facing panel APIs — not for server-to-server use.
These authenticate with the panel session cookie established by an OTP
login (password login for admin) and are CSRF-protected. They are meant for the panel
front-ends. If you are integrating a system, use the POS, bank-partner or corporate API
instead.
Webhook signature
/api/providers/*
Inbound provider callbacks are signed the same way, with their own headers.
X-Provider-Event-Id: <unique per event; used for deduplication>
X-Provider-Timestamp: <unix seconds>
X-Provider-Signature: hmac_sha256( "<timestamp>.<raw body>", <webhook secret> )
Replaying the same event id with an identical payload is safe and
returns the original outcome. Replaying it with a different payload is rejected as
an idempotency conflict. Bodies are capped at 64KB.
POS & Checkout API
API key + secret
Merchant point-of-sale and server-to-server checkout sessions.
Machine-to-machine, merchant API key + secret, /api/v1/pos/*
POST
/api/v1/pos/orders/initiate
Start a POS credit purchase (sends an SMS confirmation code to the customer)
Machine-to-machine endpoint for a merchant's POS integration. Auth: X-API-KEY + X-API-SECRET headers, matched against the merchant's own merchant_api_keys credential (see the merchant panel's "Rotate API Key" action, or the api_credential returned by the bank-partner upsert call). Only the customer's personal bank credit account is ever considered (organization-scoped credit is a separate product and is never spent through POS). No credit is reserved at this step — it only validates eligibility and sends a one-time SMS code to the customer's own phone; the actual reservation happens atomically in /orders/confirm.
Request body PosOrderInitiateRequest
| Field | Type | Notes |
|---|
customer_mobilerequired | string | e.g. 09121234567 |
amountrequired | integer | Total sale amount in rials.min 100000 |
cash_amount | integer | Portion paid in cash; the remainder (amount - cash_amount) is financed on credit.min 0 |
installments_countrequired | integer | min 1max 12 |
branch_id | integer | nullable |
Responses
| 201 | OTP sent, pending reference issuedPosOrderInitiateResponse |
| 422 | Rejected. Reasons about the merchant are reported as-is (merchant_not_active, merchant_sales_cap_exceeded, invalid_cash_amount, invalid_branch). Every reason concerning the customer — no account for this mobile, account not active, insufficient credit, credit window closed — collapses into the single opaque code credit_unavailable, so this endpoint cannot be used to discover who holds credit or to binary-search a customer's available limit.ApiError |
| 429 | OTP could not be sent (send rate limit or provider failure)ApiError |
POST
/api/v1/pos/orders/confirm
Confirm a POS purchase with the customer's SMS code (reserves and captures credit atomically)
Same auth as /orders/initiate.
Request body PosOrderConfirmRequest
| Field | Type | Notes |
|---|
pending_order_refrequired | string | |
otp_coderequired | string | |
Responses
| 201 | Credit reserved and capturedPosOrderConfirmResponse |
| 401 | OTP code invalid or expiredApiError |
| 404 | pending_order_ref not found, expired, or issued to a different merchantApiError |
| 409 | pending_order_superseded — a later /orders/initiate for the same customer replaced this order's SMS challenge. Each code confirms only the order it was issued for; start this purchase again.ApiError |
POST
/api/v1/pos/checkout/sessions
Create an online-store checkout session for credit payment
Same merchant auth as the POS endpoints. Returns a checkout_url that the merchant's online store redirects the customer's browser to. The customer confirms their own mobile number and an SMS code on that hosted page before any credit is touched; this endpoint alone never reserves credit. Idempotent per idempotency_key.
Request body CheckoutSessionCreateRequest
| Field | Type | Notes |
|---|
amountrequired | integer | min 100000 |
reference_idrequired | string | The merchant's own order/reference identifier. |
customer_mobile | string | Optional pre-fill; the customer still confirms it themselves on the hosted page.nullable |
installments_count | integer | min 1max 12 |
idempotency_keyrequired | string | |
Responses
| 201 | New checkout session createdCheckoutSessionResponse |
| 200 | Existing session returned for a repeated idempotency_keyCheckoutSessionResponse |
GET
/api/v1/pos/checkout/sessions/{code}
Poll an online checkout session's status
Same merchant auth as session creation.
Path parameters
| Name | Type | Notes |
|---|
coderequired | string | |
Responses
| 200 | Session statusCheckoutSessionResponse |
| 404 | Session not found for this merchantApiError |
Bank Partner API
API key + HMAC signature
Merchant provisioning from an external bank core system.
Machine-to-machine, API key + HMAC signature, /api/v1/bank/*
POST
/api/v1/bank/merchants/upsert
Register or sync a merchant from the external bank partner ("Hamerz Bank")
Machine-to-machine endpoint. Auth: X-API-KEY header (issued via php artisan bank-partner:credentials:issue) plus HMAC request signing — X-TIMESTAMP (unix seconds) and X-SIGNATURE = HMAC-SHA256(secret, "{X-TIMESTAMP}.{raw JSON body}"), tolerance window 300s by default. A replay-safe v2 scheme is also accepted (X-SIGNATURE-VERSION: 2 + X-NONCE, signing "METHOD\nPATH\nTIMESTAMP\nNONCE\nsha256(body)") — see docs/api/README.md §2-3. external_ref is the bank's own identifier for the business and is the real dedup key — the same owner_mobile may be reused across multiple external_refs for a person who owns several businesses; each becomes a separate merchant. A new merchant is created with status: pending and cannot transact until a HamerzPay admin activates it and sets its fee rate / credit sales cap. Repeat calls with an existing external_ref only update descriptive fields and never touch status, fee_rate, or credit_sales_cap.
Request body BankPartnerMerchantUpsertRequest
| Field | Type | Notes |
|---|
external_refrequired | string | The bank's own stable identifier for this business. Dedup key — never the mobile number. |
owner_mobilerequired | string | e.g. 09121234567 |
display_namerequired | string | |
legal_name | string | nullable |
city | string | nullable |
settlement_iban | string | nullable |
Responses
| 200 | Existing merchant's descriptive fields were updatedBankPartnerMerchantResponse |
| 201 | New merchant, owner login, and merchant API key were createdBankPartnerMerchantResponse |
| 401 | Missing/invalid API key, missing/expired/invalid signatureApiError |
| 429 | Rate limit exceededApiError |
GET
/api/v1/bank/merchants/{external_ref}
Look up a bank-provisioned merchant's status
Same auth as the upsert endpoint (API key + HMAC signature).
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
Responses
| 200 | Merchant foundBankPartnerMerchantResponse |
| 404 | No merchant registered for this external_ref under this credentialApiError |
Bank Partner — Customer Consent
API key + HMAC + customer consent token
Read a customer’s credit after they explicitly authorise it. No customer selector exists on these routes: the token identifies the customer.
Customer-consent flow for bank partners, API key + HMAC + Bearer consent token, /api/v1/bank/consent-*
POST
/api/v1/bank/consent-requests
Start a customer-consent request
Step 1 of the consent flow. Returns a consent_url to send the customer to. The customer proves who they are there with their own OTP and explicitly grants the requested scopes; nothing is readable until they do. redirect_uri must already be registered on the credential (exact match) — an unregistered value is refused, so the flow cannot be pointed at an attacker-controlled target.
Request body
| Field | Type | Notes |
|---|
redirect_urirequired | string | Must exactly match one of the credential's registered redirect URIs.maxLen 500 |
scopesrequired | string[] | |
state | string | Opaque value returned unchanged on the callback — use it for CSRF binding.maxLen 255 |
mobile_hint | string | Optional. If supplied, only that mobile number may complete the flow. It is a constraint, never an authorisation.nullable |
Responses
| 201 | Consent request created |
| 422 | redirect_uri_not_registered — the redirect URI is not on the credential's allowlistApiError |
| 429 | Consent rate limit exceeded (20/min per key by default)ApiError |
POST
/api/v1/bank/consent-tokens
Exchange the authorization code for tokens
Step 3, after the customer approves and is redirected back to your redirect_uri with ?code=...&state=.... The code is single-use and expires 60 seconds after approval.
Request body
| Field | Type | Notes |
|---|
coderequired | string | maxLen 200 |
Responses
| 201 | Tokens issuedBankConsentToken |
| 400 | invalid_grant — code is unknown, already used, or expiredApiError |
DELETE
/api/v1/bank/consent-tokens
Revoke the consent behind the presented access token
Immediately revokes the consent and every token issued under it. Requires the HMAC headers plus Authorization: Bearer <access_token>.
Responses
| 200 | Consent revoked |
| 401 | Consent token missing, invalid, expired or already revokedApiError |
POST
/api/v1/bank/consent-tokens/refresh
Rotate an access token using its refresh token
Refresh tokens rotate: the presented token is revoked as the new pair is issued, so replaying an old refresh token fails. Refresh tokens live 90 days, access tokens 60 minutes.
Request body
| Field | Type | Notes |
|---|
refresh_tokenrequired | string | maxLen 200 |
Responses
| 200 | New token pair issuedBankConsentToken |
| 400 | invalid_grant — refresh token is unknown, rotated away, expired or revokedApiError |
GET
/api/v1/bank/customers/credit
Read the consenting customer's credit
Requires the HMAC headers plus Authorization: Bearer <access_token>. There is deliberately no customer selector — not a path parameter, not a query string, not a body field. The customer is identified by the token the customer themselves authorised, so this endpoint cannot be walked over mobile numbers to discover who holds credit or to binary-search an available limit. Returns every credit line the customer holds: the personal bank credit plus any organization-backed credit, each with its own limit, consumption and scope.
Responses
| 200 | Credit summary for the consenting customerBankCustomerCredit |
| 401 | consent_token_required or consent_token_invalidApiError |
| 403 | consent_scope_insufficient — the consent does not grant credit.readApiError |
Corporate B2B API
API key + HMAC signature
Corporate partner credit lifecycle: check, reserve, capture, cancel, settle.
Machine-to-machine, API key + HMAC signature, /api/v1/corporate/*
GET
/api/v1/corporate/company
Authenticated company's profile and facility summary
Responses
| 200 | Company + facilityCorporateApiSuccess |
GET
/api/v1/corporate/credentials/status
Status of the calling API credential (masked key, is_active, created_at)
Responses
| 200 | Credential statusCorporateApiSuccess |
GET
/api/v1/corporate/plans
List the company's active credit plans (read-only)
Responses
| 200 | Plan listCorporateApiSuccess |
GET
/api/v1/corporate/customers
List/search the company's customers (paginated)
Query parameters
| Name | Type | Notes |
|---|
status | string | |
mobile | string | |
national_id | string | |
updated_since | string | date-time |
per_page | integer | min 1max 100 |
Responses
| 200 | Customer listCorporateApiSuccess |
POST
/api/v1/corporate/customers/upsert
Create or update a customer by external_ref
Requires the Idempotency-Key header.
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
external_refrequired | string | maxLen 80 |
namerequired | string | maxLen 160 |
mobile | string | maxLen 20nullable |
national_id | string | maxLen 20nullable |
email | string | emailnullable |
status | string | active | suspendednullable |
metadata | object | nullable |
Responses
| 200 | Existing customer updated |
| 201 | New customer created |
GET
/api/v1/corporate/customers/{external_ref}
Get one customer by external_ref
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
Responses
| 200 | Customer found |
| 404 | Not found (or belongs to another company)CorporateApiError |
GET
/api/v1/corporate/customers/{external_ref}/credit
Customer's corporate credit accounts + facility summary
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
GET
/api/v1/corporate/customers/{external_ref}/wallets
Eligible corporate wallets for a prospective purchase amount
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
Query parameters
| Name | Type | Notes |
|---|
amountrequired | integer | min 1 |
merchant_id | integer | |
Responses
| 200 | Wallet eligibility list |
GET
/api/v1/corporate/customers/{external_ref}/documents
List a customer's uploaded documents
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
POST
/api/v1/corporate/customers/{external_ref}/documents
Upload a customer document (multipart file, or base64 JSON payload)
Requires the Idempotency-Key header. Max 10MB; pdf/jpg/jpeg/png/webp only.
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
document_typerequired | string | maxLen 120 |
title | string | nullable |
file_content_base64 | string | nullable |
original_filename | string | nullable |
mime_type | string | application/pdf | image/jpeg | image/png | image/webpnullable |
metadata | object | nullable |
GET
/api/v1/corporate/customers/{external_ref}/documents/{document}
Get one customer document's metadata
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
documentrequired | integer | |
POST
/api/v1/corporate/customers/{external_ref}/credit-accounts/request
Request a new credit account for a customer (pending or needs_review)
Requires the Idempotency-Key header.
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
plan_id | integer | nullable |
credit_limitrequired | integer | min 1 |
settlement_days | integer | min 1max 365nullable |
reason | string | nullable |
allowed_merchant_ids | integer[] | |
metadata | object | |
Responses
| 201 | Account request created |
POST
/api/v1/corporate/customers/{external_ref}/credit-accounts/activate
Activate a credit account immediately (or flag needs_review)
Requires the Idempotency-Key header.
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
plan_idrequired | integer | |
credit_limitrequired | integer | min 1 |
settlement_days | integer | nullable |
allowed_merchant_ids | integer[] | |
metadata | object | |
Responses
| 201 | Account activated |
| 202 | Flagged needs_review instead (facility exceeded or settlement days exceed plan) |
POST
/api/v1/corporate/customers/{external_ref}/credit-limit/request
Request a credit-limit change for an existing account
Requires the Idempotency-Key header.
Path parameters
| Name | Type | Notes |
|---|
external_refrequired | string | |
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
account_idrequired | integer | |
requested_credit_limitrequired | integer | min 0 |
reasonrequired | string | maxLen 1000 |
effective_at | string | date-timenullable |
Responses
| 201 | Limit-change request created (pending or needs_review) |
POST
/api/v1/corporate/settlement-requests
Submit a self-reported settlement
Requires the Idempotency-Key header.
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
external_refrequired | string | |
account_idrequired | integer | |
usage_id | integer | nullable |
amountrequired | integer | min 1 |
reference_numberrequired | string | maxLen 120 |
paid_at | string | date-timenullable |
notes | string | nullable |
Responses
| 201 | Settlement request recorded |
GET
/api/v1/corporate/settlement-requests/{settlementRequest}
Get one settlement request's status
Path parameters
| Name | Type | Notes |
|---|
settlementRequestrequired | integer | |
POST
/api/v1/corporate/webhooks/test
Dispatch a test webhook event to all of the company's active endpoints
Request body
| Field | Type | Notes |
|---|
event | string | maxLen 120nullable |
message | string | maxLen 255nullable |
POST
/api/v1/corporate/credit/check
Check corporate-credit eligibility (tenant-scoped by account_id)
Request body
| Field | Type | Notes |
|---|
account_idrequired | integer | min 1 |
amountrequired | integer | min 1 |
POST
/api/v1/corporate/credit/reserve
Reserve corporate credit for a usage
Requires the Idempotency-Key header; account_id is tenant-scoped.
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
account_idrequired | integer | min 1 |
amountrequired | integer | min 1 |
reference_idrequired | string | maxLen 255 |
idempotency_keyrequired | string | maxLen 255 |
repayment_days | integer | min 1max 365nullable |
due_at | string | date-timenullable |
repayment_policy | string | nullable |
invoice_number | string | nullable |
invoice_date | string | datenullable |
metadata | object | |
POST
/api/v1/corporate/credit/capture
Capture a reserved corporate-credit usage
Requires the Idempotency-Key header; usage_id is tenant-scoped.
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
usage_idrequired | integer | min 1 |
idempotency_keyrequired | string | maxLen 255 |
repayment_schedule | object[] | |
POST
/api/v1/corporate/credit/cancel
Cancel a reserved (not yet captured) corporate-credit usage
Requires the Idempotency-Key header; usage_id is tenant-scoped.
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
usage_idrequired | integer | min 1 |
idempotency_keyrequired | string | maxLen 255 |
POST
/api/v1/corporate/credit/settle
Record a settlement against a captured corporate-credit usage
Requires the Idempotency-Key header; usage_id is tenant-scoped.
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
usage_idrequired | integer | min 1 |
amountrequired | integer | min 1 |
reference_numberrequired | string | maxLen 255 |
idempotency_keyrequired | string | maxLen 255 |
GET
/api/v1/corporate/credit-lines
List the company's revolving credit lines (Unified Credit Core) with utilization and collateral coverage
Read-only. Each line carries approved_limit, consumed/reserved/utilized/available amounts, eligible_collateral_value (sum of ACTIVE, unexpired collateral eligible values), collateral_backed_max_credit (= eligible × LTV) and within_collateral.
Query parameters
| Name | Type | Notes |
|---|
status | string | ACTIVE | SUSPENDED | CLOSED |
external_ref | string | Filter by customer external_ref |
Responses
| 200 | Credit lines + ltv_percentCorporateApiSuccess |
GET
/api/v1/corporate/collaterals
List the company's collateral records (paginated)
Query parameters
| Name | Type | Notes |
|---|
status | string | PENDING | ACTIVE | EXPIRED | RELEASED | CALLED |
credit_line_id | integer | |
external_ref | string | |
per_page | integer | min 1max 100 |
Responses
| 200 | Collateral list + paginationCorporateApiSuccess |
POST
/api/v1/corporate/collaterals
Register a collateral for one of the company's customers (created as PENDING; approval is done by HamerzPay)
Requires the Idempotency-Key header; the same key for the same customer replays the original record (200, replayed=true). credit_line_id is tenant-scoped: another company's line answers 404. eligible_value = nominal_value × (1 − haircut_percentage/100).
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
external_refrequired | string | maxLen 80 |
credit_line_id | integer | Must belong to the same customernullable |
typerequired | string | CHEQUE | PROMISSORY_NOTE | BANK_GUARANTEE | CASH_DEPOSIT | CONTRACTUAL | OTHER |
nominal_valuerequired | integer | min 1 |
haircut_percentage | number | min 0max 100 |
reference_number | string | maxLen 100nullable |
issue_date | string | datenullable |
expiry_date | string | datenullable |
Responses
| 201 | Collateral registered (PENDING) |
| 200 | Idempotent replay of an earlier registration |
| 404 | Unknown customer or another company's credit lineCorporateApiError |
| 422 | Validation failed or Idempotency-Key missing |
GET
/api/v1/corporate/receivables
List the company's per-purchase receivables (paginated)
Query parameters
| Name | Type | Notes |
|---|
status | string | OPEN | PARTIALLY_PAID | PAID | OVERDUE | DEFAULTED | WRITTEN_OFF |
from | string | issue_date >= fromdate |
to | string | issue_date <= todate |
due_from | string | due_date >= due_fromdate |
due_to | string | due_date <= due_todate |
credit_line_id | integer | |
external_ref | string | |
per_page | integer | min 1max 100 |
Responses
| 200 | Receivables (principalCorporateApiSuccess |
POST
/api/v1/corporate/receivables/{receivable}/payments
Register a (partial or full) payment against a receivable
Requires the Idempotency-Key header, unique per company: the same key replays the original payment (200, replayed=true) without deducting again; the same key with a different amount/receivable is rejected (422 corporate_credit_rule_violation). Over-payment and payments on PAID/WRITTEN_OFF receivables are rejected with 422. The receivable is tenant-scoped: another company's id answers 404. Paid principal frees the revolving line's limit; fees never do.
Path parameters
| Name | Type | Notes |
|---|
receivablerequired | integer | |
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
amountrequired | integer | min 1 |
reference_number | string | maxLen 120nullable |
Responses
| 201 | Payment registered; returns receivable |
| 200 | Idempotent replay; replayed=true |
| 404 | Receivable not found or belongs to another companyCorporateApiError |
| 422 | Rule violation (over-payment |
Hosted Checkout Page
Public
The customer-facing page your checkout session redirects to.
Public, customer-facing hosted page, /checkout/*
GET
/checkout/{code}
Hosted online-checkout page (customer-facing, not machine-callable)
Public HTML page (session + CSRF protected, throttled 20/min/IP), not part of the JSON API surface. The customer enters/confirms their mobile number, receives an SMS code, and confirms it here — the page then reserves and captures credit atomically and redirects to the merchant's callback_url (configured in merchant_settings), or shows a success message if none is configured. Returns 404 for an unknown code, 410 for an expired/cancelled session, 409 if the session was already completed.
Path parameters
| Name | Type | Notes |
|---|
coderequired | string | |
POST
/checkout/{code}/otp/send
Submit the customer's mobile number and trigger an SMS code
If the merchant pinned customer_mobile when creating the session, only that number is accepted; any other number is refused with an error flash rather than silently repointing the session.
Path parameters
| Name | Type | Notes |
|---|
coderequired | string | |
Request body application/x-www-form-urlencoded
| Field | Type | Notes |
|---|
mobilerequired | string | e.g. 09121234567 |
Responses
| 302 | Redirect back to the checkout page with a success or error flash message |
POST
/checkout/{code}/otp/verify
Confirm the SMS code and complete the purchase (reserve + capture, atomic)
Path parameters
| Name | Type | Notes |
|---|
coderequired | string | |
Request body application/x-www-form-urlencoded
| Field | Type | Notes |
|---|
otp_coderequired | string | |
Responses
| 302 | On success, redirects to the merchant's callback_url (?code&status=success) or back to the checkout page with a success message. On failure, redirects back to the checkout page with an error flash message. |
Provider Webhooks
HMAC signature (shared secret)
Inbound financial events you send us.
Inbound provider callbacks, HMAC-signed (not API key), /api/providers/*
POST
/api/providers/{provider}/financial-events
Inbound financial event callback from a payment/settlement/funding/payroll provider
Verified via HMAC-SHA256 over "{X-Provider-Timestamp}.{raw body}" using hamerzpay.finance.providers.{provider}.webhook_secret (with previous_webhook_secret accepted during rotation), not an API key. A 300-second freshness window applies and the raw body is capped at 64KB. Deduplicated by (provider, X-Provider-Event-Id) via the Inbox pattern — replays with the same event id and a different payload are rejected as an idempotency conflict.
Path parameters
| Name | Type | Notes |
|---|
providerrequired | string | payment | settlement | funding | payroll |
Required headers
| Name | Type | Notes |
|---|
X-Provider-Event-Idrequired | string | |
X-Provider-Timestamprequired | string | Unix seconds |
X-Provider-Signaturerequired | string | |
Request body
Provider-defined payload; must include event_type.
Responses
| 202 | Event accepted into the inbox and processed |
| 401 | Invalid signature (event is still recorded, marked invalid) |
| 413 | Payload exceeds 64KB |
| 422 | Missing event id, timestamp, or signature header |
Customer Panel API
Session cookie (OTP / password login)
Session-authenticated (OTP login), /api/customer/*
GET
/api/customer/health
Panel health check
Responses
| 200 | Panel is healthyPanelHealth |
GET
/api/customer/credit/summary
Authenticated customer's own credit summary
POST
/api/customer/credit/check
Check purchase eligibility against the customer's own credit account
Request body
| Field | Type | Notes |
|---|
account_idrequired | integer | |
amountrequired | integer | min 1 |
merchant_id | integer | nullable |
category | string | nullable |
Responses
| 200 | Eligibility resultCreditCheckResult |
| 403 | account_id does not belong to the authenticated customer |
POST
/api/customer/credit/reserve
Reserve credit for a purchase (step 1 of 2)
Request body
| Field | Type | Notes |
|---|
account_idrequired | integer | |
amountrequired | integer | min 1 |
installments_countrequired | integer | min 1max 12 |
merchant_id | integer | nullable |
idempotency_keyrequired | string | maxLen 200 |
Responses
| 201 | Order reserved (or the existing order for a repeated idempotency_key)CreditOrderSummary |
POST
/api/customer/credit/{order}/capture
Capture (finalize) a reserved order — step 2 of 2
Path parameters
| Name | Type | Notes |
|---|
orderrequired | integer | |
Request body
| Field | Type | Notes |
|---|
idempotency_keyrequired | string | maxLen 200 |
Responses
| 200 | Order capturedCreditOrderSummary |
POST
/api/customer/credit/{order}/cancel
Cancel a reservation before it is captured
Path parameters
| Name | Type | Notes |
|---|
orderrequired | integer | |
Request body
| Field | Type | Notes |
|---|
idempotency_keyrequired | string | maxLen 200 |
POST
/api/customer/credit/evaluate
Request a bank-credit underwriting decision
Request body
| Field | Type | Notes |
|---|
requested_amountrequired | integer | min 10000 |
requested_installmentsrequired | integer | min 1max 12 |
Responses
| 200 | Underwriting decision (approved / rejected / needs_guarantor / needs_collateral) |
POST
/api/customer/credit/activate
Activate a previously approved credit request
Request body
| Field | Type | Notes |
|---|
request_idrequired | integer | |
Responses
| 200 | Bank credit account activated with the approved limit |
GET
/api/customer/credit/orders
List the authenticated customer's own credit orders (paginated)
GET
/api/customer/credit/installments
List the authenticated customer's own installments, with overdue/upcoming breakdown
Responses
| 200 | Installment summary and list |
POST
/api/customer/credit/installments/{installment}/pay
Submit a repayment for one installment
Requires the Idempotency-Key header (not a body field).
Path parameters
| Name | Type | Notes |
|---|
installmentrequired | integer | |
Required headers
| Name | Type | Notes |
|---|
Idempotency-Keyrequired | string | |
Request body
| Field | Type | Notes |
|---|
amountrequired | integer | min 1 |
Responses
| 202 | Repayment operation accepted (processed asynchronously) |
Merchant Panel API
Session cookie (OTP / password login)
Session-authenticated (OTP login), /api/merchant/*
GET
/api/merchant/health
Panel health check
Responses
| 200 | Panel is healthyPanelHealth |
POST
/api/merchant/checkout/customers/{customerId}/otp
Send a purchase-confirmation SMS code to a corporate customer
Step 1 of corporate-wallet checkout. The code goes to the corporate customer's own mobile; the customer reads it to the cashier in person. Rate limits and lockouts are the shared OTP policy. Requires merchant role owner/manager/branch_manager/cashier (merchant.corporate-checkout.create).
Path parameters
| Name | Type | Notes |
|---|
customerIdrequired | integer | |
Responses
| 202 | Code sent; data.customer_id, data.mobile_masked |
| 403 | Operator role not allowed |
| 404 | CUSTOMER_NOT_FOUND |
| 429 | OTP_SEND_FAILED (cooldown / rate limit / provider) |
POST
/api/merchant/checkout/customers/{customerId}/otp/verify
Exchange the customer's SMS code for a short-lived checkout_token
Step 2. The token is bound to (this merchant, this customer), lives 10 minutes, and authorises exactly one checkout (one idempotency_key; retries with the same key are allowed).
Path parameters
| Name | Type | Notes |
|---|
customerIdrequired | integer | |
Request body
| Field | Type | Notes |
|---|
otp_coderequired | string | maxLen 10 |
Responses
| 200 | data.checkout_token, data.expires_at, data.customer_id |
| 422 | OTP_INVALID (wrong, expired, or issued for another merchant's request) |
POST
/api/merchant/checkout/session
Corporate-wallet checkout (check + reserve + capture in one call)
For paying with a customer's *corporate* wallet credit specifically — unrelated to the bank-credit online checkout under /api/v1/pos/checkout. Contract change 2026-10-03: customer_id and checkout_token (from /otp/verify) are now required, and corporate_account_id must belong to that customer. The company must also explicitly allow this merchant (corporate_allowed_merchants); a company with no rows is now denied unless CORPORATE_ALLOW_ALL_MERCHANTS_WHEN_UNRESTRICTED=true.
Request body
| Field | Type | Notes |
|---|
customer_idrequired | integer | min 1 |
checkout_tokenrequired | string | maxLen 128 |
corporate_account_idrequired | integer | min 1 |
amountrequired | integer | min 1 |
reference_idrequired | string | maxLen 191 |
idempotency_keyrequired | string | maxLen 191 |
metadata | object | |
Responses
| 201 | Corporate wallet checkout completed |
| 403 | CUSTOMER_VERIFICATION_REQUIRED (missing/expired/foreign token) or operator role not allowed |
| 409 | CHECKOUT_TOKEN_USED — token already spent on a different idempotency_key |
| 422 | Checkout rejected (e.g. insufficient corporate credit, account not owned by customer, merchant not allowed)ApiError |
GET
/api/merchant/checkout/customers/{customerId}/wallets
List a customer's eligible corporate wallets for a prospective purchase
Contract change 2026-10-03: requires a checkout_token for this customer (query param or X-Checkout-Token header) obtained via /otp/verify.
Path parameters
| Name | Type | Notes |
|---|
customerIdrequired | integer | |
Query parameters
| Name | Type | Notes |
|---|
merchant_idrequired | integer | |
amountrequired | integer | min 1 |
checkout_token | string | Required unless sent as the X-Checkout-Token header |
Responses
| 200 | Eligible wallets, with auto_select_wallet_id when exactly one qualifies |
| 403 | MERCHANT_MISMATCH, CUSTOMER_VERIFICATION_REQUIRED, or operator role not allowed |
Organization Panel API
Session cookie (OTP / password login)
Session-authenticated (OTP login), /api/org/*
GET
/api/org/health
Panel health check
Responses
| 200 | Panel is healthyPanelHealth |
GET
/api/org/credit/summary
Organization-wide credit/budget summary
Responses
| 200 | Budget snapshotBudgetSummary |
GET
/api/org/budget/summary
Alias of /credit/summary (legacy path kept for backward compatibility)
Responses
| 200 | Budget snapshotBudgetSummary |
POST
/api/org/credit/check
Check purchase eligibility against an organization-scoped credit account
Request body
| Field | Type | Notes |
|---|
account_idrequired | integer | |
amountrequired | integer | min 1 |
merchant_id | integer | nullable |
category | string | nullable |
Responses
| 200 | Eligibility resultCreditCheckResult |
POST
/api/org/credit/allocate
Allocate/update a member's credit limit
Caller must be owner, admin, manager, or a finance/HR operator of the organization. The organization is taken from the authenticated panel session; member_id and policy_id must both belong to it.
Request body
| Field | Type | Notes |
|---|
member_idrequired | integer | |
credit_limitrequired | integer | min 0 |
policy_id | integer | nullable |
idempotency_keyrequired | string | Required. Replaying the same key returns the original allocation instead of posting a second ledger entry.maxLen 200 |
Responses
| 200 | Allocation applied |
| 403 | Member or policy belongs to a different organizationApiError |
POST
/api/org/allocations/reduce
Reduce a member's allocation (owner/admin/finance-operator only, strictly a decrease)
Request body
| Field | Type | Notes |
|---|
member_idrequired | integer | |
new_limitrequired | integer | Must be strictly lower than the member's current credit_limit.min 0 |
idempotency_keyrequired | string | maxLen 191 |
Responses
| 200 | Allocation reduced |
| 422 | new_limit is not lower than the current allocation |
GET
/api/org/credit/members
List organization members and their credit status
POST
/api/org/credit/members/create
Add a new member with an initial credit allocation (one-shot onboarding)
Request body
| Field | Type | Notes |
|---|
mobilerequired | string | |
namerequired | string | minLen 2maxLen 100 |
credit_limitrequired | integer | min 0 |
role | string | employee | manager | unit_manager |
policy_id | integer | nullable |
employee_code | string | nullable |
position_title | string | nullable |
unit_id | integer | nullable |
branch_id | integer | nullable |
Responses
| 201 | Member and credit account created |
GET
/api/org/credit/policies
List the organization's credit policies
Admin Panel API
Session cookie (OTP / password login)
Session-authenticated (password login), /api/admin/*
GET
/api/admin/health
Panel health check
Responses
| 200 | Panel is healthyPanelHealth |
GET
/api/admin/kpi/summary
Admin KPI summary
Responses
| 200 | KPI snapshotKpiSummary |
GET
/api/admin/corporate/settlements/reconciliation
Corporate settlement reconciliation report (read-only)
Requires the admin.corporate.data.view ability.
Query parameters
| Name | Type | Notes |
|---|
company_id | integer | |
merchant_id | integer | |
from | string | date |
to | string | date |
status | string | |
GET
/api/admin/corporate/risk/overdue
Corporate overdue-risk report (read-only)
Requires the admin.corporate.data.view ability.
Query parameters
| Name | Type | Notes |
|---|
company_id | integer | |
customer_id | integer | |
status | string | |
min_overdue_amount | integer | |
from | string | date |
to | string | date |
Responses
| 200 | Overdue items, count, and total_overdue_amount |
GET
/api/admin/corporate/operations/summary
Corporate production-operations health summary (locked accounts, failed settlements/webhooks, queue backlog)
Requires the admin.corporate.data.view ability.
Responses
| 200 | Operations summary with an overall status of ok/warning/critical |
POST
/api/admin/corporate/accounts/{account}/{action}
Lock, unlock, suspend, or close a corporate credit account
Requires the admin.corporate.account.manage ability.
Path parameters
| Name | Type | Notes |
|---|
accountrequired | integer | |
actionrequired | string | lock | unlock | suspend | close |
Request body
| Field | Type | Notes |
|---|
reasonrequired | string | minLen 3maxLen 500 |
Responses
| 200 | Account status changed |
POST
/api/admin/corporate/webhooks/{delivery}/retry
Manually retry a failed corporate webhook delivery
Requires the admin.corporate.webhook.manage ability.
Path parameters
| Name | Type | Notes |
|---|
deliveryrequired | integer | |
Request body
| Field | Type | Notes |
|---|
reasonrequired | string | minLen 3maxLen 500 |
Responses
| 200 | Retry scheduled/attempted |
POST
/api/admin/corporate/customer-documents/{document}/review
Approve or reject a corporate customer's uploaded document
Requires the admin.corporate.customer.manage ability. Triggers a customer.document.{approved,rejected} partner webhook.
Path parameters
| Name | Type | Notes |
|---|
documentrequired | integer | |
Request body
| Field | Type | Notes |
|---|
statusrequired | string | approved | rejected |
rejection_reason | string | Required when status=rejected.nullable |
POST
/api/admin/corporate/settlements/{settlement}/mark-failed
Mark a corporate settlement as failed
Requires the admin.corporate.settlement.manage ability.
Path parameters
| Name | Type | Notes |
|---|
settlementrequired | integer | |
Request body
| Field | Type | Notes |
|---|
reasonrequired | string | minLen 3maxLen 500 |
Responses
| 200 | Settlement marked failed |
POST
/api/admin/corporate/settlements/{settlement}/retry-processing
Retry processing a corporate settlement
Requires the admin.corporate.settlement.manage ability.
Path parameters
| Name | Type | Notes |
|---|
settlementrequired | integer | |
Request body
| Field | Type | Notes |
|---|
reasonrequired | string | minLen 3maxLen 500 |
Responses
| 200 | Settlement processing retried |