Direct recharge

A direct recharge product defines the recipient service. A direct recharge is your purchase of that product. You pay LYD from your wallet; Anis staff deliver the payout to the recipient. This can take time. It is not an API for moving money between Anis wallets.

Choose a product

Sign GET /v1/wallets/{walletId}/direct-recharge-products?type=external. The enum retains billing, but billing products are not available yet; they cannot be quoted or purchased. Reads need direct_recharges:read, also included in direct_recharges:create. Catalogue, location and quote answers are unsigned. Pass nextCursor unchanged as cursor; the last page has nextCursor: null.

{
  "items": [
    {
      "id": "e7c2a9d4-1b3f-4a85-9c60-8d2e4f7b1a93",
      "title": "فودافون كاش مصر",
      "description": "تحويل إلى محافظ فودافون كاش",
      "type": "external",
      "category": {
        "id": "4b9e...",
        "name": "مصر",
        "icon": "https://cdn.anis.ly/circle/eg.png"
      },
      "payoutCurrency": {
        "name": "جنيه مصري"
      },
      "available": true,
      "limits": {
        "minimumTotal": {
          "amount": "10.000",
          "currency": "LYD"
        },
        "maximumTotal": {
          "amount": "5000.000",
          "currency": "LYD"
        },
        "minimumPayout": "50.000",
        "maximumPayout": "30000.000"
      }
    }
  ],
  "nextCursor": null
}

Pricing and limits depend on this wallet’s tier. available: false includes a missing wallet price or a Fazaa-only price; partners cannot buy through Fazaa. Limits may be absent without a matching tier. Payout currency is a display name, not an ISO code: do not turn it into Money or invent a code.

Collect fields and locations

Read GET /v1/wallets/{walletId}/direct-recharge-products/{productId} for the product and its ordered fields.

{
  "id": "e7c2a9d4-1b3f-4a85-9c60-8d2e4f7b1a93",
  "title": "فودافون كاش مصر",
  "type": "external",
  "payoutCurrency": {
    "name": "جنيه مصري"
  },
  "available": true,
  "limits": {
    "minimumTotal": {
      "amount": "10.000",
      "currency": "LYD"
    },
    "maximumTotal": {
      "amount": "5000.000",
      "currency": "LYD"
    },
    "minimumPayout": "50.000",
    "maximumPayout": "30000.000"
  },
  "recipientFields": [
    {
      "key": "recipient_number",
      "name": {
        "ar": "رقم المحفظة",
        "en": "Wallet number"
      },
      "kind": "text",
      "for": "recipient",
      "order": 1,
      "validation": "^01[0-9]{9}$",
      "validationMessage": {
        "ar": "رقم غير صحيح",
        "en": "Invalid number"
      }
    },
    {
      "key": "recipient_name",
      "name": {
        "ar": "اسم المستلم",
        "en": "Recipient name"
      },
      "kind": "text",
      "for": "recipient",
      "order": 2
    },
    {
      "key": "recipient_location",
      "name": {
        "ar": "المدينة",
        "en": "City"
      },
      "kind": "list",
      "for": "recipient",
      "order": 3
    }
  ]
}
Field What to do
key Preserve it in each submitted key/value entry
name Show the Arabic or English label
kind text is text; list offers choices; note is display guidance
for recipient or sender, as supplied
order Show fields in this order
validation, validationMessage Apply the supplied rule and localized message when present

For location choices use GET /v1/wallets/{walletId}/direct-recharge-products/{productId}/locations?search=القاهرة. Search is optional; continue with cursor.

{
  "items": [
    {
      "id": "1a2b...",
      "name": "القاهرة"
    },
    {
      "id": "3c4d...",
      "name": "الإسكندرية"
    }
  ],
  "nextCursor": null
}

Send the selected location id as its field’s value, not the display name.

Quote both sides

GET /v1/wallets/{walletId}/direct-recharge-products/{productId}/quote?amount=1500.000&amountIn=fx checks the price without reserving money. fx means input in payout currency; lyd means input in dinars. The answer always shows both sides.

{
  "productId": "e7c2a9d4-1b3f-4a85-9c60-8d2e4f7b1a93",
  "payout": {
    "amount": "1500.000",
    "currency": {
      "name": "جنيه مصري"
    }
  },
  "exchangeRate": "0.155000",
  "price": {
    "amount": "232.500",
    "currency": "LYD"
  },
  "commission": {
    "amount": "5.000",
    "currency": "LYD"
  },
  "total": {
    "amount": "237.500",
    "currency": "LYD"
  }
}

total is the wallet cost. Applicable location and holder adjustments are inside price; commission is separate. Amounts are decimal strings, never floating-point JSON numbers. The exchange rate is also a decimal string, illustrated with six decimals.

Request a direct recharge

Save a UUID and the exact body before POST /v1/wallets/{walletId}/direct-recharges. Requires direct_recharges:create. Sign the body digest, fresh nonce and Idempotency-Key; the key is the operation id. Send payoutAmount and quoted expectedTotal. Do not send price, priceWithCommission or extraFee.

{
  "productId": "e7c2a9d4-1b3f-4a85-9c60-8d2e4f7b1a93",
  "payoutAmount": "1500.000",
  "expectedTotal": {
    "amount": "237.500",
    "currency": "LYD"
  },
  "recipient": [
    {
      "key": "recipient_number",
      "value": "01012345678"
    },
    {
      "key": "recipient_name",
      "value": "Ahmed Ali"
    },
    {
      "key": "recipient_location",
      "value": "1a2b..."
    }
  ],
  "note": "Family support",
  "externalReference": "POS-77-TR-551"
}

Verify the signed 202. An accepted direct recharge can answer with Location: /v1/direct-recharges/{operationId} and:

{
  "operationId": "7d4e1a90-5b2c-4e7f-8a13-0c9d6e2b4f81",
  "operationType": "directRecharge",
  "status": "processing"
}

The balance check is exact up to 999,999.999 LYD. This bound applies to the LYD wallet balance, not to the named payout currency.

Before recording the purchase, Anis checks the wallet balance; insufficient funds return insufficient_balance, 409. A later competing debit may still cause paymentFailed.

A missing read is not proof of absence. Retry the saved id/body. After a duplicate, 202 processing allows projection lag; if the application-scoped read is still missing after 60 seconds since admission, the retry closes with idempotency_conflict, 409. That code does not prove nothing was charged: the first request may already have been accepted, so read the operation before any new attempt.

Payment and delivery continue. If the answer is lost or the outcome unknown, read the same operation or retry its exact saved body with the same key and a fresh signature. Never use a new id to work around uncertainty.

Read progress and refunds

Sign GET /v1/direct-recharges/{operationId} with read or create permission and verify the signed answer. Another application’s direct recharge looks like a missing one.

{
  "commission": {
    "amount": "5.000",
    "currency": "LYD"
  },
  "completedAt": "2026-10-20T10:00:00Z",
  "exchangeRate": "0.155000",
  "externalReference": "POS-77-TR-551",
  "note": "Family support",
  "number": 30412,
  "operationId": "7d4e1a90-5b2c-4e7f-8a13-0c9d6e2b4f81",
  "payout": {
    "amount": "1500.000",
    "currency": {
      "name": "جنيه مصري"
    }
  },
  "price": {
    "amount": "232.500",
    "currency": "LYD"
  },
  "product": {
    "id": "e7c2a9d4-1b3f-4a85-9c60-8d2e4f7b1a93",
    "title": "فودافون كاش مصر"
  },
  "proofUrl": "https://cdn.anis.ly/circle/proofs/example.png",
  "recipient": [
    {
      "key": "recipient_name",
      "name": {
        "ar": "اسم المستلم",
        "en": "Recipient name"
      },
      "value": "Ahmed Ali"
    },
    {
      "key": "recipient_number",
      "name": {
        "ar": "رقم المحفظة",
        "en": "Wallet number"
      },
      "value": "01012345678"
    }
  ],
  "requestedAt": "2026-10-20T09:00:00Z",
  "status": "completed",
  "total": {
    "amount": "237.500",
    "currency": "LYD"
  },
  "walletId": "71825266-7182-4182-8182-718252667182"
}
status Meaning
awaitingPayment Recorded; money is being taken
paymentFailed Payment refused; nothing taken (final)
pending Paid; waiting for an officer
inProgress An officer is working on it
suspendedDataError Correct recipient data; reason explains
completed Delivered (final)
rejectedAwaitingRefund Rejected; refund on its way
rejectedWithRefund Rejected; money returned (final)

reason appears for suspended and both rejected states. proofUrl, when supplied, points to recorded delivery proof. Partial refunds occur only if partial refunds are enabled for the product; all current new direct recharges, including those made by apps, are ineligible. A settled partial refund retains the app’s merged status and adds this block; it introduces no new status.

{
  "partialRefund": {
    "refundedPayout": "500.000",
    "refundedTotal": {
      "amount": "77.500",
      "currency": "LYD"
    },
    "remainingPayout": "1000.000",
    "remainingTotal": {
      "amount": "160.000",
      "currency": "LYD"
    }
  }
}

refundedTotal is money actually returned; remainingTotal is the remaining wallet cost. Payout amounts are strings in the display currency. Do not call a requested but unsettled refund completed. Rejected notifications follow full refund settlement; partial-refund notifications follow partial settlement.

A read can also answer a refusal that was recorded for the create under this id, with Idempotency-Replayed: true: 409 (insufficient_balance, price_changed or direct_recharge_product_unavailable) or 422 (amount_out_of_range, or recipient data that was refused). Nothing was charged. On a read, idempotency_conflict (409) means no direct recharge of your application exists under this id; use a new Idempotency-Key only for a genuinely new operation.

Find my operations

Sign GET /v1/wallets/{walletId}/direct-recharges with direct_recharges:read or direct_recharges:create. The answer is unsigned. Only this application’s operations in the selected authorized wallet are returned, newest first. Optional filters, which combine: status (awaitingPayment, paymentFailed, pending, inProgress, suspendedDataError, completed, rejectedAwaitingRefund or rejectedWithRefund), productId (a UUID), from and to (UTC), cursor and pageSize. Each window is at most 31 days; omitted dates mean the last 31 days. The page size defaults to 50 and is at most 100. Pass nextCursor unchanged with the same filters and pageSize until it is null. An empty result has items: [] and nextCursor: null. After validation_failed (422), correct the filters or cursor; do not repeat a purchase to recover a list. Statuses match the detail read; a partial refund keeps its merged status, and hasPartialRefund reports it separately. Recipient details and proof remain on the signed detail read.

{
  "items": [
    {
      "operationId": "7d4e1a90-5b2c-4e7f-8a13-0c9d6e2b4f81",
      "status": "completed",
      "number": 30412,
      "product": {
        "id": "e7c2a9d4-1b3f-4a85-9c60-8d2e4f7b1a93",
        "title": "فودافون كاش مصر"
      },
      "payout": {
        "amount": "1500.000",
        "currency": {
          "name": "جنيه مصري"
        }
      },
      "total": {
        "amount": "237.500",
        "currency": "LYD"
      },
      "externalReference": "POS-77-TR-551",
      "requestedAt": "2026-10-20T09:00:00Z",
      "completedAt": "2026-10-20T09:14:00Z",
      "hasPartialRefund": false
    }
  ],
  "nextCursor": null
}

Correct a suspended recipient

Only while status is suspendedDataError, send PATCH /v1/direct-recharges/{operationId}/recipient. Requires create permission, signed digest and a fresh nonce. This is a signed change, not a new purchase; do not send Idempotency-Key. Send the complete corrected recipient list. It replaces the saved list. Include every required field and any optional values you want to retain, including recipient_location.

{
  "recipient": [
    {
      "key": "recipient_number",
      "value": "01098765432"
    },
    {
      "key": "recipient_name",
      "value": "Ahmed Ali Hassan"
    },
    {
      "key": "recipient_location",
      "value": "1a2b..."
    }
  ]
}

Verify the signed answer. A successful correction returns signed 202 Accepted with Location: /v1/direct-recharges/{operationId} and the standard processing acknowledgement: only operationId, operationType: "directRecharge" and status: "processing". It carries no direct recharge or order body. Follow Location to read the direct recharge or wait for a directRecharge.* webhook. Acceptance does not mean the corrected state is visible yet. Read before sending again: an already-applied correction can be refused as direct_recharge_not_suspended. The response headers and processing acknowledgement:

HTTP/1.1 202 Accepted
Location: /v1/direct-recharges/7d4e1a90-5b2c-4e7f-8a13-0c9d6e2b4f81
{
  "operationId": "7d4e1a90-5b2c-4e7f-8a13-0c9d6e2b4f81",
  "operationType": "directRecharge",
  "status": "processing"
}

Validate the supplied fields against the product’s rules. A recipient validation problem may contain this fragment:

{"extensions":{"fields":{"recipient_number":"Invalid number"}}}

direct_recharge_not_suspended is 409; validation_failed is 422; an unknown or foreign direct recharge is resource_not_found, 404.

Refusals before purchase

Code HTTP What it means
price_changed 409 Quote moved; quote and confirm again
direct_recharge_product_unavailable 409 This wallet cannot buy it
amount_out_of_range 422 Payout or total outside limits
validation_failed 422 Correct fields in extensions.fields
purchase_not_allowed 403 You cannot buy your own product
insufficient_balance 409 Insufficient funds
spending_limit_exceeded 429 Shared application cap reached
idempotency_conflict 409 Changed saved intent, or a repeated create still unknown after the 60-second lag window; the first request may already be charged, so read it first

Purchases share the daily cap and purchase request limit with card orders and meter charges. After a definitive refusal closes the operation, a corrected purchase needs a new id. Admission, signing or transport failure does not prove an earlier attempt failed.

idempotency_conflict and replay_detected do not say the first request failed: it may already have been accepted and charged, so never assume nothing was charged. Read the original operation with GET /v1/direct-recharges/{operationId} before any new attempt. To repeat the same intent after replay_detected, keep the Idempotency-Key and send a fresh nonce and signature. After idempotency_conflict, reuse the key only for identical intent and use a new Idempotency-Key only for a genuinely new operation. insufficient_balance, price_changed and the unavailable-product refusals happen before money moves: nothing was charged.

Receive outcomes through webhooks and verify each notification before processing it.

Products · Product · Locations · Quote · Create · Read · Recipient · Find my operations