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.

{
  "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.

POST /v1/wallets/{walletId}/meter-charges
Idempotency-Key: 3c1e9b7a-0d42-4f5e-8a61-2b9c7d0e5f13
{
  "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}:

{
  "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:

{
  "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.

{
  "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.

{
  "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 and verify each notification before processing it.

Packages · Create · Read · Find my operations

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

{
  "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"
  }
}