API Documentation

Accept and send payments programmatically

Secure Fast Developer Friendly

🚀 Getting Started

1
Request an API Key

Create a key request from the merchant dashboard and wait for admin approval. The API Secret is shown only once when created or regenerated, so store it securely.

2
Set Permissions

Choose which endpoints your API key can access. Minimum one permission required.

3
Configure IP Whitelist

Optionally restrict the API key to your server's public outbound IP address. Do not enter a customer, browser, or CDN address.

4
Go Live

Test PayIn and Payout status updates with small amounts, then scale up. Always use HTTPS.

🌍 IP Whitelist

IP whitelisting is optional. If no IP address is configured, valid API credentials may be used from any IP. When one or more addresses are configured, requests from every other IP are rejected with AUTH_005.

Whitelist the public outbound IP address of the server that sends requests to OESPay. Do not use the customer's IP, browser IP, domain IP, or a CDN/proxy IP.

To check a Linux server's public IPv4 address:

curl -4 -s https://api.ipify.org
Dual-stack servers: A server may send requests over IPv4 or IPv6. Use a stable outbound address, configure your HTTP client to use the whitelisted IPv4 address, or whitelist every legitimate outbound address used by your server.

Multiple server IP addresses may be added to the same API key. Each API key has its own independent whitelist.

🔐 Authentication

Both X-API-Key and X-API-Secret are REQUIRED for all protected API endpoints. Provider and merchant webhook callbacks use their own verification rules.

Required Headers:

X-API-Key: oes_your_api_key_here
X-API-Secret: sk_your_secret_here
Important: Never expose your API credentials in client-side code. Always use server-side requests.

🔑 Permissions

Each API key can have one or more permissions. The all permission grants full access.

PermissionDescriptionEndpoints
payinCreate and manage payment invoices/create-invoice, /check-status, /merchant/payins
payoutCreate and manage payout requests/payout/create, /merchant/payouts
balanceCheck balance and statistics/merchant/balance, /merchant/stats
allFull access to all endpointsAll endpoints

🌐 Base URL

https://cashwanna.com/api

All endpoints are relative to this base URL.

📝 Create Invoice

POST /create-invoice Requires: payin

📤 Request

Required Headers:

Content-Type: application/json
X-API-Key: oes_your_api_key_here
X-API-Secret: sk_your_secret_here
{
    "amount": 100.00,
    "pay_way": "cashapp",
    "customer_email": "customer@example.com",
    "customer_name": "John Doe",
    "redirect_url": "https://merchant.example/payment/return"
}
ParameterTypeRequiredDescription
amountnumeric✅ YesMin 1, max 10000, at most 2 decimal places. Some wallets accept only the exact preset amounts listed below; hosted checkout wallets have their own min/max.
pay_waystring✅ Yescashapp, ecashapp, applepay, googlepay, card2, chime, paypal3
customer_emailstring❌ NoValid customer email, maximum 255 characters
customer_namestring❌ NoCustomer name, maximum 255 characters
redirect_urlURL❌ NoHTTPS return URL after checkout, maximum 2048 characters
Restricted invoice amounts:
ecashapp: 9.99, 14.99, 17.99, 19.99, 24.99, 29.99, 30.99, 39.99, 49.99, 59.99, 99.99, 124.99, 129.99, 149.99, 199.99, 249.99, 299.99, 399.99, 499.99
applepay: 9.99, 14.99, 17.99, 19.99, 24.99, 29.99, 30.99, 39.99, 49.99, 59.99, 99.99, 124.99, 129.99, 149.99, 199.99
googlepay: 9.99, 14.99, 17.99, 19.99, 24.99, 29.99, 30.99, 39.99, 49.99, 59.99, 99.99, 124.99, 129.99, 149.99, 199.99
card2: 9.99, 14.99, 17.99, 19.99, 24.99, 29.99, 30.99, 39.99, 49.99, 59.99, 99.99, 124.99, 129.99, 149.99, 199.99
Hosted checkout wallets: paypal3. Any amount within the wallet limits (no preset list). Send the customer to payment_url; the order turns success only after the payment is confirmed (webhook payin.status.updated), never from the return page.

Fee and net values vary according to the authenticated merchant's effective rate.

Wallet typepayment_url / checkout_url
eCashApp, Apple Pay, Google Pay, Cardthe provider's hosted checkout page
Cash App / Bitcoin (Lightning)our invoice page /pay/invoice/<token>
Hosted checkout walletsour redirect page /pay/stx/<order_no> (sends the customer to the provider)
Manual wallets (Chime, PayPal, Venmo)checkout_url = our checkout page /checkout/<token> (32-character token)

Both responses use HTTP 201 Created.

✅ Response (Auto Wallet)

{
    "success": true,
    "data": {
        "order_id": 123,
        "order_no": "API_1788264000_abc123",
        "amount": "99.99",
        "fee": "14.80",
        "net": "85.19",
        "pay_way": "applepay",
        "payment_url": "https://provider.example/checkout/xxx",
        "expires_at": "2026-09-01T12:00:00Z",
        "status": "processing"
    }
}

✅ Response (Manual Wallet)

{
    "success": true,
    "data": {
        "order_id": 124,
        "order_no": "API_1788264000_def456",
        "amount": "100.00",
        "fee": "10.30",
        "net": "89.70",
        "pay_way": "chime",
        "manual_code": "W6X-F1",
        "wallet_address": "$ExampleTag",
        "checkout_url": "https://cashwanna.com/checkout/Xy7Qp2LmN8vR4tKc9WbZ3aHs6DfJ1eGu",
        "expires_at": "2026-09-01T20:00:00Z",
        "status": "pending_manual"
    }
}

🔍 Check Order Status

GET /check-status/{order_id} Requires: payin

✅ Response

{
    "success": true,
    "data": {
        "order_id": 123,
        "order_no": "API_1788264000_abc123",
        "amount": "99.99",
        "real_amount": "99.99",
        "fee": "14.80",
        "net": "85.19",
        "pay_way": "applepay",
        "payment_url": "https://provider.example/checkout/xxx",
        "manual_code": null,
        "status": "success",
        "created_at": "2026-09-01T10:00:00Z",
        "success_time": "2026-09-01T10:05:00Z",
        "expires_at": "2026-09-01T12:00:00Z"
    }
}

📊 Status Values

pending processing success failed pending_manual expired cancelled disputed closed

closed: the provider closed or the customer cancelled the payment. expired: the payment window ended (orders expired by the timer do not send a webhook; poll this endpoint).

💰 Get Balance

GET /merchant/balance Requires: balance

✅ Response

{
    "success": true,
    "data": {
        "balance": "1500.00",
        "bonus_balance": "50.00",
        "available_balance": "1400.00",
        "on_hold": "100.00",
        "currency": "USD"
    }
}

on_hold is part of balance (reserve or admin hold) but cannot be paid out or withdrawn. Payouts can use up to available_balance.

📋 Get PayIns

GET /merchant/payins Requires: payin

Query Parameters:

?limit=20&status=success&pay_way=cashapp

limit accepts 1–100 and defaults to 20. Results are returned newest first. total counts all rows matching the filters.

✅ Response

{
    "success": true,
    "data": {
        "total": 45,
        "payins": [
            {
                "id": 123,
                "order_no": "API_1706000000_abc123",
                "merchant_order_no": "MCH_1706000000_xyz789",
                "pay_way": "cashapp",
                "amount": "100.00",
                "real_amount": "100.00",
                "fee": "11.00",
                "net": "89.00",
                "status": "success",
                "cashier_url": "https://provider.example/checkout/xxx",
                "created_at": "2026-01-15 10:00:00",
                "success_time": "2026-01-15 10:05:00",
                "expire_time": "2026-01-15 12:00:00"
            }
        ]
    }
}

📋 Get Payouts

GET /merchant/payouts Requires: payout

Query Parameters:

?limit=20&status=approved&pay_way=chime_payout3

limit accepts 1–100 and defaults to 20. Results are returned newest first. total counts all rows matching the filters.

✅ Response

{
    "success": true,
    "data": {
        "total": 12,
        "payouts": [
            {
                "id": 456,
                "payout_id": 456,
                "transaction_id": "POU-123456",
                "amount": "50.00",
                "fee": "3.50",
                "total_deducted": "53.50",
                "pay_way": "chime_payout3",
                "recipient": "$ExampleTag",
                "status": "approved",
                "rejection_reason": null,
                "reason": null,
                "needs_review": false,
                "invoice_number": "INV-20260906-000456",
                "completion_image": "https://proof.provider.example/photo/abc.jpg",
                "created_at": "2026-09-06 12:49:35",
                "approved_at": "2026-09-06 12:52:00",
                "completed_at": "2026-09-06 12:52:00"
            }
        ]
    }
}

📊 Payout Status Values

pending processing approved rejected cancelled failed

completion_image is a full URL to the payment proof (or null). reason explains a failure (provider message or our reason) or why the payout needs review. needs_review is true while an automatic payout is waiting for a manual check by our team; the money is not refunded automatically in that case.

Merchant webhooks are enabled. Use this endpoint as a polling fallback and match the returned payout_id or transaction_id. Do not create a second payout while checking an existing request.

📊 Get Stats

GET /merchant/stats Requires: balance

✅ Response

{
    "success": true,
    "data": {
        "balance": "1500.00",
        "bonus_balance": "50.00",
        "payin": {
            "total_count": 45,
            "total_amount": "5000.00",
            "total_success": "4500.00",
            "total_pending": "500.00",
            "total_failed": "0.00"
        },
        "payout": {
            "total_count": 12,
            "total_amount": "1200.00",
            "total_approved": "1100.00",
            "total_pending": "100.00",
            "total_failed": "0.00"
        }
    }
}

👤 Get Profile

GET /merchant/profile Requires: all

✅ Response

{
    "success": true,
    "data": {
        "id": 12,
        "merchant_no": "MCH_6650A1B2C3D4E",
        "name": "Example Merchant",
        "email": "merchant@example.com",
        "role": "merchant",
        "status": "active",
        "created_at": "2026-01-10 09:00:00"
    }
}

📤 Create Payout (API)

POST /payout/create Requires: payout
The Idempotency-Key header is required and must not exceed 100 characters. Reuse the same key only when retrying the exact same payout request; use a new key for every new payout.

📤 Headers

Content-Type: application/json
X-API-Key: oes_your_api_key
X-API-Secret: sk_your_secret
Idempotency-Key: unique_request_id_123

📤 Request Body

{
    "amount": 50.00,
    "pay_way": "ach_payout",
    "recipient": {
        "accountNumber": "123456789",
        "routingNumber": "021000021"
    }
}
ParameterTypeRequiredDescription
amountnumeric✅ YesMin 0.01, at most 2 decimal places; per-wallet min/max may apply (see below). Amount + fee is taken from your available balance.
pay_waystring✅ Yescashapp_payout, paypal_payout, chime_payout, chime_payout3
recipientobject✅ YesFields depend on the wallet (table below). Single-field wallets also accept a plain account_info string.
Wallet codes: <wallet>_payout and <wallet>_payout2 are automatic (sent to the payout provider at once, two provider accounts); <wallet>_payout3 is manual (our team approves and pays it). Card payouts are automatic only. Venmo and Zelle payouts are not available through the API. Only the codes listed in the table below are open right now.

👤 Recipient fields per wallet

pay_wayWalletrecipient fieldsProcessingLimits
cashapp_payout Cash App cashtag Cashtag ($) Automatic (sent at once, final status by webhook)
paypal_payout PayPal email PayPal email Automatic (sent at once, final status by webhook)
chime_payout Chime chimeSign Chime sign ($) Automatic (sent at once, final status by webhook)
chime_payout3 Chime 2 chimeSign Chime sign ($) Manual (admin approval)
Formats: cashtag $ + 2-19 letters/numbers/_/-; chimeSign $ + 3-49 characters (the $ is added for you if missing, for both); PayPal email a valid email (stored lowercase); ACH accountNumber 4-17 digits and routingNumber 9 digits (spaces removed); card cardNumber 12-19 digits passing the Luhn check (spaces/dashes allowed) and cardValid MM/YYYY, not in the past. Card numbers are passed to the provider only and never stored; responses and webhooks show Card ****1234. ACH is shown as ACH ****6789 / 021000021.
Automatic payouts start as processing; if the provider fails or cancels them the status becomes failed with a reason and the full amount (amount + fee) is refunded to your balance. If the provider gives no clear answer, the payout stays processing (no refund) until it is confirmed; it may show needs_review: true meanwhile.

✅ Response (201 Created)

{
    "success": true,
    "data": {
        "payout_id": 456,
        "transaction_id": "POU-20260901-A1B2C3D4E5",
        "amount": "50.00",
        "fee": "3.50",
        "total_deducted": "53.50",
        "pay_way": "ach_payout",
        "recipient": "ACH ****6789 / 021000021",
        "status": "processing",
        "reason": null,
        "needs_review": false,
        "created_at": "2026-09-01 10:00:00",
        "message": "Payout sent to the provider. Final status comes by webhook."
    }
}

transaction_id format: POU-YYYYMMDD- + 10 characters.

Other responses

CaseHTTPBody
Manual wallet (*_payout3)201status: "pending", message: "Payout request submitted successfully. Please wait for admin approval."
Same Idempotency-Key, same data (retry)200the existing payout, message: "Duplicate request - returning existing payout"; nothing new is created or charged
Same Idempotency-Key, different data409PAYOUT_005
Provider rejects the payout at once (automatic wallets)422success: false, no code; data.status: "failed" with data.reason, message: "Rejected by the provider; the balance was refunded." The amount + fee is already back in your balance.

🔄 Merchant Webhooks

Outgoing merchant webhooks are enabled for API-created PayIns and Payouts. OESPay sends an asynchronous signed callback whenever the resource status or supported result details change.

⚙️ Setup

Open Merchant Dashboard → API Keys, configure an HTTPS webhook URL, enable delivery, and securely copy the separately generated webhook signing secret.

The webhook secret is different from the X-API-Secret used to call protected API endpoints. Never expose either secret in client-side code.

Webhook delivery applies only to PayIns and Payouts linked to the API key on which the webhook is enabled.

📤 Request Headers

Content-Type: application/json
X-OESPay-Event-Id: 85d5cb6f-93a9-48d4-a2a7-20a9b271111e
X-OESPay-Timestamp: 1788264000
X-OESPay-Signature: 3f9a1c0e5b7d2a8f6c4e1b9d0a7f3e2c5b8d1a4f7e0c3b6a9d2f5e8b1c4a7d0e
User-Agent: Cashwanna-OESPay-Webhook/1.0

📥 PayIn Status Event

{
    "event_id": "550e8400-e29b-41d4-a716-446655440000",
    "event": "payin.status.updated",
    "created_at": "2026-09-07T16:57:02+00:00",
    "data": {
        "local_order_no": "MCH_1788800033_example",
        "order_id": "API_1788800033_example",
        "transaction_id": 37050,
        "status": "failed",
        "amount": "20.00",
        "real_amount": null,
        "fee": "3.10",
        "net": "16.90",
        "pay_way": "chime",
        "admin_notes": "Payment was not received from the provided account.",
        "success_time": null
    }
}

📤 Payout Status Event

{
    "event_id": "a3d38cb1-7a89-42dd-8fac-e2e017199edb",
    "event": "payout.status.updated",
    "created_at": "2026-09-07T17:02:00+00:00",
    "data": {
        "local_order_no": "PO_1788800120_aB3dE5gH",
        "order_id": 456,
        "transaction_id": "POU-20260907-A1B2C3D4E5",
        "status": "approved",
        "amount": "50.00",
        "fee": "3.50",
        "total_deducted": "53.50",
        "pay_way": "paypal_payout",
        "recipient": "customer@example.com",
        "rejection_reason": null,
        "reason": null,
        "needs_review": false,
        "invoice_number": "INV-20260907-ABC123",
        "completion_image": "https://proof.provider.example/photo/abc.jpg",
        "completed_at": "2026-09-07T17:01:30+00:00"
    }
}

Payout event: order_id is the payout id (same as payout_id in API responses). local_order_no is the provider order number for automatic payouts and null for manual ones. reason / needs_review as in GET /merchant/payouts. Payin event: order_id is the order_no from create-invoice, transaction_id the numeric order_id, local_order_no our internal order number.

When events are sent

  • PayIn: when the status (or the admin note) changes.
  • Payout: when the status, the rejection reason, the failure / review reason, the proof photo or the invoice number changes.
  • Only for orders created with the API key that has the webhook enabled.
  • PayIns that expire by the payment timer are closed in bulk and do not send a webhook: poll GET /check-status/{order_id} for open orders after their expires_at.

Verification: X-OESPay-Signature is the lowercase hex HMAC-SHA256 of the exact raw HTTP request body, keyed with the webhook signing secret. There is no sha256= prefix. Compare with a timing-safe comparison.

$expected = hash_hmac('sha256', $rawBody, $webhookSecret);
$valid = hash_equals($expected, $_SERVER['HTTP_X_OESPAY_SIGNATURE'] ?? '');

Important: Read the raw body before decoding JSON. Do not re-encode, reformat, sort, or otherwise modify the JSON before calculating the signature.

Timestamp: X-OESPay-Timestamp is informational and is not part of the signature.

Duplicate protection: Store X-OESPay-Event-Id (also event_id in the body) and ignore an event that has already been processed. This is your protection against replays and retries.

Acknowledgement: Return any HTTP 2xx within 30 seconds after safely recording the event. Redirects are not followed: a 3xx counts as a failure.

Retries: up to 5 attempts in total; after a failure the next attempt waits about 10 s, 30 s, 2 min, then 5 min. Process events idempotently.

Webhook URL: must be a public https:// URL. localhost, *.local and private or reserved IP addresses are refused.

Fallback: If a callback is delayed, use GET /check-status/{order_id} for PayIns or GET /merchant/payouts for Payouts.

💳 Supported Wallets

PayIn

cashapp (Cash App)ecashapp (eCashApp)applepay (Apple Pay)googlepay (Google Pay)card2 (Card)chime (Chime)paypal3 (PayPal & Venmo)

Payout

cashapp_payout (Cash App)paypal_payout (PayPal)chime_payout (Chime)chime_payout3 (Chime 2)

⚠️ Error Codes

CodeHTTP StatusMessage
AUTH_001401API key required
AUTH_002401Invalid API credentials
AUTH_003401API key is inactive or expired
AUTH_004401Invalid API credentials
AUTH_005403Request IP is not allowed
AUTH_006403Permission denied for this endpoint
AUTH_007401API secret required
AUTH_008403Merchant account is inactive
AUTH_009403API permission configuration missing
AUTH_010401Merchant not authenticated
PAYIN_001422Payment method is unavailable
PAYIN_002422Calculated fee exceeds the invoice amount
PAYIN_003502Upstream payment provider could not create the invoice
PAYIN_004422Unsupported payment method
PAYIN_005404Order not found for the authenticated merchant
PAYIN_006503No active manual wallet is available
PAYIN_007422Invalid amount: not in the wallet's preset list (response includes allowed_amounts), or outside the min/max of a hosted checkout wallet
PAYIN_008403Payment method is locked for this account (limited account). Contact support to unlock it.
PAYOUT_003400Payment method not available for payout
PAYOUT_004422Valid Idempotency-Key header is required
PAYOUT_005409Idempotency key was already used with different payout data
PAYOUT_006422Invalid recipient, amount outside the wallet limits, or insufficient balance (see errors)
PAYOUT_007500Payout could not be created. Retry with the same Idempotency-Key.
FEE_001422Fee not configured for this wallet. Contact support.
RATE_001429Rate limit exceeded (5000/min)
(no code)422Request validation failed (missing/invalid amount, unknown pay_way, invalid email or redirect_url, ...). Body: {"message": "...", "errors": {"field": ["..."]}}
(no code)422Payout rejected by the provider at once: success: false, data.status: "failed", data.reason; balance already refunded
500500Internal server error

Error bodies with a code look like {"success": false, "error": "...", "code": "PAYIN_001"}; PAYIN_007 from a preset-amount wallet also includes allowed_amounts, and PAYOUT_006 includes errors.

⏱️ Rate Limit

5000 requests per minute per API key.

Only authenticated requests are counted. Successful responses carry:

X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1706000000

Over the limit you receive 429 Too Many Requests with headers X-RateLimit-Limit, X-RateLimit-Remaining: 0 and Retry-After (seconds; no X-RateLimit-Reset on a 429), and this body:

{
    "success": false,
    "error": "Rate limit exceeded",
    "code": "RATE_001",
    "limit": 5000,
    "reset_in": 42
}

Need Help?

Contact our support team for integration assistance.

Get API Keys →