---
title: "Meter charges — Anis Developers"
description: "Choose a package, charge a meter and follow tokens, failures and refunds."
url: https://developers.anis.ly/docs/meter-charges/
language: en
---

> Every page of this documentation: https://developers.anis.ly/llms.txt

# Meter charges

Choose a package for the wallet, confirm its total with the customer and request the charge. The charge continues after acceptance. Keep its operation id until you know whether it succeeded and, if it failed, whether money returned.

## Choose a package

Sign `GET /v1/wallets/{walletId}/meter-packages`. It requires `meter_charges:read`, also included in `meter_charges:create`. The answer is unsigned. Pass `nextCursor` unchanged as `cursor`; the final page has `nextCursor: null`.

```json
{
  "items": [
    {
      "id": "5f3b8c21-7a0d-4e96-b1c4-2d8e6f9a0b37",
      "name": "باقة 100 دينار",
      "gecolPackageName": "PKG_100",
      "logo": "https://cdn.anis.ly/meter/packages/100.png",
      "unitPrice": {
        "amount": "97.500",
        "currency": "LYD"
      },
      "businessPrice": {
        "amount": "97.500",
        "currency": "LYD"
      },
      "personalPrice": {
        "amount": "100.000",
        "currency": "LYD"
      }
    }
  ],
  "nextCursor": null
}
```

`unitPrice` is what this wallet pays. Send that Money value as `expectedTotal`. `personalPrice` is the retail display price; `businessPrice` appears only when applicable. The package’s `name` is one stored name, not a bilingual object. Do not calculate the payable total from the other display prices.

## Request a charge

Save a new UUID and the exact body before sending. Use that UUID as `Idempotency-Key`; it is also the charge’s `operationId` in reads, notifications and references. Sign with the create profile: body digest, fresh nonce and idempotency key. Requires `meter_charges:create`.

A temporarily missing read does not prove a charge was never created. Retry the exact saved body with the same id. A duplicate remains `202 processing` during lag; if the scoped read is still missing after a duplicate and 60 seconds since admission, Anis closes the retry 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.

```http
POST /v1/wallets/{walletId}/meter-charges
Idempotency-Key: 3c1e9b7a-0d42-4f5e-8a61-2b9c7d0e5f13
```

```json
{
  "packageId": "5f3b8c21-7a0d-4e96-b1c4-2d8e6f9a0b37",
  "meterNumber": "0268000710262",
  "expectedTotal": {
    "amount": "97.500",
    "currency": "LYD"
  },
  "externalReference": "POS-77-INV-1093"
}
```

A signed `202` reports acceptance or continuing uncertainty; it is not a token. An accepted charge can answer as follows, with `Location: /v1/meter-charges/{operationId}`:

```json
{
  "operationId": "3c1e9b7a-0d42-4f5e-8a61-2b9c7d0e5f13",
  "operationType": "meterCharge",
  "status": "processing"
}
```

Verify the signed answer before using it. If it is lost, cannot be verified or says processing, read the same operation or retry the exact saved body with the same key. Sign every attempt afresh. Never create a second charge just because the first answer is late. An unreachable meter check is an unknown outcome, not a definitive refusal.

## Read the result and tokens

`GET /v1/meter-charges/{operationId}` requires read or create permission. Sign it as a read and verify its signed answer. Another application’s charge looks exactly like a missing charge. A processing read has the charge’s head fields and no receipt. A successful read carries:

```json
{
  "operationId": "3c1e9b7a-0d42-4f5e-8a61-2b9c7d0e5f13",
  "status": "succeeded",
  "walletId": "71825266-7182-4182-8182-718252667182",
  "meterNumber": "0268000710262",
  "package": {
    "id": "5f3b8c21-5f3b-4c21-8f3b-5f3b8c215f3b",
    "name": "باقة 100 دينار",
    "gecolPackageName": "PKG_100"
  },
  "total": {
    "amount": "97.500",
    "currency": "LYD"
  },
  "externalReference": "POS-77-INV-1093",
  "createdAt": "2026-10-20T08:12:44Z",
  "completedAt": "2026-10-20T08:13:02Z",
  "receipt": {
    "receiptNumber": "260020103744",
    "creditedAmount": "88.640",
    "tokens": [
      {
        "sequence": 1,
        "label": "KCT1",
        "code": "1234 5678 9012 3456 7890",
        "description": "Key change token 1"
      },
      {
        "sequence": 2,
        "label": "KCT2",
        "code": "2345 6789 0123 4567 8901",
        "description": "Key change token 2"
      },
      {
        "sequence": 3,
        "label": "STS",
        "code": "5821 0034 7719 2265 0418",
        "description": "Credit token"
      }
    ],
    "taxes": [
      {
        "description": "ضريبة الاستهلاك",
        "amount": "5.000"
      },
      {
        "description": "رسوم الخدمة",
        "amount": "3.860"
      }
    ]
  },
  "personalPrice": {
    "amount": "100.000",
    "currency": "LYD"
  }
}
```

Show every token in ascending `sequence`: key-change tokens first, then the credit token. Preserve `label`, `code` and `description`, including leading zeros and spaces; do not show only the last token. A token works only on the meter charged. `receiptNumber`, `creditedAmount` and `taxes` are the electricity supplier’s values. They need not equal the wallet total. Receipt amount strings pass through unchanged; Money’s three-decimal scale is not promised for them.

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 `package_unavailable`) or `422` (`meter_rejected`). These refusals happen before money moves: nothing was charged. On a read, `idempotency_conflict` (`409`) means no meter charge 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}/meter-charges` with `meter_charges:read` or `meter_charges:create`. The answer is unsigned. Only this application’s operations in the selected authorized wallet are returned, newest first. Optional filters, which combine: `status` (`processing`, `succeeded` or `failed`), `meterNumber` (exact match, 1 to 64 ASCII digits; keep leading zeros), `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. Receipts and every token remain on the signed charge read.

```json
{
  "items": [
    {
      "operationId": "3c1e9b7a-0d42-4f5e-8a61-2b9c7d0e5f13",
      "status": "succeeded",
      "meterNumber": "0268000710262",
      "package": {
        "id": "5f3b8c21-7a0d-4e96-b1c4-2d8e6f9a0b37",
        "name": "باقة 100 دينار",
        "gecolPackageName": "PKG_100"
      },
      "total": {
        "amount": "97.500",
        "currency": "LYD"
      },
      "externalReference": "POS-77-INV-1093",
      "createdAt": "2026-10-20T08:12:44Z",
      "completedAt": "2026-10-20T08:13:02Z"
    }
  ],
  "nextCursor": null
}
```

## Failure and money returned

Every state includes `total` and `personalPrice`. `completedAt` appears only on success. Electricity company tax descriptions stay in Arabic exactly as supplied.

A read can show `status: "failed"` while money is still being returned. Check `refund.status` before telling the customer money is back.

```json
{
  "operationId": "3c1e9b7a-0d42-4f5e-8a61-2b9c7d0e5f13",
  "status": "failed",
  "walletId": "71825266-7182-4182-8182-718252667182",
  "meterNumber": "0268000710262",
  "package": {
    "id": "5f3b8c21-5f3b-4c21-8f3b-5f3b8c215f3b",
    "name": "باقة 100 دينار",
    "gecolPackageName": "PKG_100"
  },
  "total": {
    "amount": "97.500",
    "currency": "LYD"
  },
  "externalReference": "POS-77-INV-1093",
  "createdAt": "2026-10-20T08:12:44Z",
  "failure": {
    "code": "meter_not_whitelisted",
    "message": {
      "ar": "العداد غير مسجل",
      "en": "Meter not enrolled"
    }
  },
  "refund": {
    "status": "refunded",
    "amount": {
      "amount": "97.500",
      "currency": "LYD"
    }
  },
  "personalPrice": {
    "amount": "100.000",
    "currency": "LYD"
  }
}
```

| `refund.status` | Meaning |
| --- | --- |
| `refunded` | The money is back |
| `notCharged` | The wallet refused payment; nothing was taken |
| `pending` | Money has not returned yet; Anis staff are alerted |

`failure.message` carries Arabic and English display messages. Branch on `failure.code`. These values belong to a charge read; they are not HTTP problem codes.

| `failure.code` | Meaning |
| --- | --- |
| `meter_number_invalid` | Meter number invalid |
| `meter_not_whitelisted` | Meter not enrolled |
| `meter_not_linked` | Meter not linked |
| `meter_removed` | Meter removed |
| `meter_inactive` | Meter inactive |
| `meter_account_blocked` | Account blocked |
| `meter_data_incomplete` | Meter data incomplete |
| `meter_debt_exceeds_package` | Debt exceeds package |
| `package_unavailable` | Package unavailable |
| `package_limit_reached` | Package limit reached |
| `token_not_written` | Token not written |
| `insufficient_balance` | Insufficient balance |
| `gecol_unavailable` | The electricity company did not answer in time |
| `charge_failed` | Charge failed |

A `meterCharge.failed` notification is sent only after the refund lands or when nothing was taken. A pending refund can be visible in the read before a failed notification is sent. Success notifications contain the full read body, including every token.

## Refused before money moves

| Code | HTTP | What to do |
| --- | --- | --- |
| `meter_rejected` | `422` | Check the meter number |
| `package_unavailable` | `409` | Read current packages |
| `price_changed` | `409` | Confirm the current payable price again |
| `insufficient_balance` | `409` | Make funds available |
| `spending_limit_exceeded` | `429` | Wait for the cap reset given by Retry-After |
| `idempotency_conflict` | `409` | Restore a changed saved body; read the operation first, because it may already be charged; use a fresh id only for a genuinely new operation |

After a definitive refusal closes the operation, a corrected new charge needs a new id. An access, signature, rate or transport failure does not prove an earlier attempt failed. Meter charges share the application’s purchase limit and daily spending cap with card orders and direct recharges.

`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/meter-charges/{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](https://developers.anis.ly/docs/webhooks.md) and [verify each notification](https://developers.anis.ly/docs/verifying-webhooks.md) before processing it.

[Packages](https://developers.anis.ly/reference/get-meter-packages.md) · [Create](https://developers.anis.ly/reference/create-meter-charge.md) · [Read](https://developers.anis.ly/reference/read-meter-charge.md) · [Find my operations](https://developers.anis.ly/reference/get-meter-charges.md)

A pending refund is visible in the read, but has no sendable notification yet:

```json
{
  "operationId": "3c1e9b7a-0d42-4f5e-8a61-2b9c7d0e5f13",
  "status": "failed",
  "walletId": "71825266-7182-4182-8182-718252667182",
  "meterNumber": "0268000710262",
  "package": {
    "id": "5f3b8c21-5f3b-4c21-8f3b-5f3b8c215f3b",
    "name": "باقة 100 دينار",
    "gecolPackageName": "PKG_100"
  },
  "total": {
    "amount": "97.500",
    "currency": "LYD"
  },
  "externalReference": "POS-77-INV-1093",
  "createdAt": "2026-10-20T08:12:44Z",
  "failure": {
    "code": "meter_not_whitelisted",
    "message": {
      "ar": "العداد غير مسجل",
      "en": "Meter not enrolled"
    }
  },
  "refund": {
    "status": "pending",
    "amount": {
      "amount": "97.500",
      "currency": "LYD"
    }
  },
  "personalPrice": {
    "amount": "100.000",
    "currency": "LYD"
  }
}
```
