Accept and send payments programmatically
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.
Choose which endpoints your API key can access. Minimum one permission required.
Optionally restrict the API key to your server's public outbound IP address. Do not enter a customer, browser, or CDN address.
Test PayIn and Payout status updates with small amounts, then scale up. Always use HTTPS.
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
Multiple server IP addresses may be added to the same API key. Each API key has its own independent whitelist.
Required Headers:
X-API-Key: oes_your_api_key_here
X-API-Secret: sk_your_secret_here
Each API key can have one or more permissions. The all permission grants full access.
| Permission | Description | Endpoints |
|---|---|---|
| payin | Create and manage payment invoices | /create-invoice, /check-status, /merchant/payins |
| payout | Create and manage payout requests | /payout/create, /merchant/payouts |
| balance | Check balance and statistics | /merchant/balance, /merchant/stats |
| all | Full access to all endpoints | All endpoints |
https://cashwanna.com/api
All endpoints are relative to this base URL.
/create-invoice
Requires: payin
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"
}
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | numeric | ✅ Yes | Min 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_way | string | ✅ Yes | cashapp, ecashapp, applepay, googlepay, card2, chime, paypal3 |
| customer_email | string | ❌ No | Valid customer email, maximum 255 characters |
| customer_name | string | ❌ No | Customer name, maximum 255 characters |
| redirect_url | URL | ❌ No | HTTPS return URL after checkout, maximum 2048 characters |
Fee and net values vary according to the authenticated merchant's effective rate.
| Wallet type | payment_url / checkout_url |
|---|---|
| eCashApp, Apple Pay, Google Pay, Card | the provider's hosted checkout page |
| Cash App / Bitcoin (Lightning) | our invoice page /pay/invoice/<token> |
| Hosted checkout wallets | our 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.
{
"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"
}
}
{
"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-status/{order_id}
Requires: payin
{
"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"
}
}
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).
/merchant/balance
Requires: balance
{
"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.
/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.
{
"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"
}
]
}
}
/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.
{
"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"
}
]
}
}
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/stats
Requires: balance
{
"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"
}
}
}
/merchant/profile
Requires: all
{
"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"
}
}
/payout/create
Requires: payout
Content-Type: application/json
X-API-Key: oes_your_api_key
X-API-Secret: sk_your_secret
Idempotency-Key: unique_request_id_123
{
"amount": 50.00,
"pay_way": "ach_payout",
"recipient": {
"accountNumber": "123456789",
"routingNumber": "021000021"
}
}
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | numeric | ✅ Yes | Min 0.01, at most 2 decimal places; per-wallet min/max may apply (see below). Amount + fee is taken from your available balance. |
| pay_way | string | ✅ Yes | cashapp_payout, paypal_payout, chime_payout, chime_payout3 |
| recipient | object | ✅ Yes | Fields depend on the wallet (table below). Single-field wallets also accept a plain account_info string. |
| pay_way | Wallet | recipient fields | Processing | Limits |
|---|---|---|---|---|
| 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) |
{
"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.
| Case | HTTP | Body |
|---|---|---|
| Manual wallet (*_payout3) | 201 | status: "pending", message: "Payout request submitted successfully. Please wait for admin approval." |
| Same Idempotency-Key, same data (retry) | 200 | the existing payout, message: "Duplicate request - returning existing payout"; nothing new is created or charged |
| Same Idempotency-Key, different data | 409 | PAYOUT_005 |
| Provider rejects the payout at once (automatic wallets) | 422 | success: 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. |
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.
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
{
"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
}
}
{
"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.
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.
| Code | HTTP Status | Message |
|---|---|---|
| AUTH_001 | 401 | API key required |
| AUTH_002 | 401 | Invalid API credentials |
| AUTH_003 | 401 | API key is inactive or expired |
| AUTH_004 | 401 | Invalid API credentials |
| AUTH_005 | 403 | Request IP is not allowed |
| AUTH_006 | 403 | Permission denied for this endpoint |
| AUTH_007 | 401 | API secret required |
| AUTH_008 | 403 | Merchant account is inactive |
| AUTH_009 | 403 | API permission configuration missing |
| AUTH_010 | 401 | Merchant not authenticated |
| PAYIN_001 | 422 | Payment method is unavailable |
| PAYIN_002 | 422 | Calculated fee exceeds the invoice amount |
| PAYIN_003 | 502 | Upstream payment provider could not create the invoice |
| PAYIN_004 | 422 | Unsupported payment method |
| PAYIN_005 | 404 | Order not found for the authenticated merchant |
| PAYIN_006 | 503 | No active manual wallet is available |
| PAYIN_007 | 422 | Invalid amount: not in the wallet's preset list (response includes allowed_amounts), or outside the min/max of a hosted checkout wallet |
| PAYIN_008 | 403 | Payment method is locked for this account (limited account). Contact support to unlock it. |
| PAYOUT_003 | 400 | Payment method not available for payout |
| PAYOUT_004 | 422 | Valid Idempotency-Key header is required |
| PAYOUT_005 | 409 | Idempotency key was already used with different payout data |
| PAYOUT_006 | 422 | Invalid recipient, amount outside the wallet limits, or insufficient balance (see errors) |
| PAYOUT_007 | 500 | Payout could not be created. Retry with the same Idempotency-Key. |
| FEE_001 | 422 | Fee not configured for this wallet. Contact support. |
| RATE_001 | 429 | Rate limit exceeded (5000/min) |
| (no code) | 422 | Request validation failed (missing/invalid amount, unknown pay_way, invalid email or redirect_url, ...). Body: {"message": "...", "errors": {"field": ["..."]}} |
| (no code) | 422 | Payout rejected by the provider at once: success: false, data.status: "failed", data.reason; balance already refunded |
| 500 | 500 | Internal 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.
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
}