Webhooks

Anis sends outcomes to your application’s verified HTTPS endpoint. You can keep serving customers without polling every operation. A notification contains the full public operation read, including meter tokens or direct recharge recipient details. Verify its signature before trusting it.

Register and verify your endpoint

Each application has one endpoint. Read it with GET /v1/webhook-endpoint and register it with PUT /v1/webhook-endpoint. These calls need no additional scope, but still require your application’s admitted signed request. The GET answer is unsigned; the PUT answer is signed. Sign changes with a body digest and a fresh nonce, without an Idempotency-Key. Use the endpoint reference for the published request and response fields.

The endpoint read returns url, status and generation. An unconfigured endpoint returns null for url and generation. status is notConfigured, pendingVerification, verified or disabled. Repeating PUT with the same URL preserves registration and verification; staff disable is never lifted by PUT. A changed URL needs a new verification. Send exactly {} for test and redeliver.

Register with this PUT body:

{"url":"https://pos.example.ly/hooks/anis"}

A configured endpoint answers GET and PUT with 200:

{"url":"https://pos.example.ly/hooks/anis","status":"pendingVerification","generation":7}

With no endpoint, GET answers 200:

{"url":null,"status":"notConfigured","generation":null}

The URL must use HTTPS, port 443 and a host name that contains at least one dot, with no user-info or fragment, and at most 2048 characters. Its destination must resolve to public addresses; local, private, link-local, shared, multicast and unspecified addresses are refused. Redirects are not followed.

Registration triggers a signed verification notification:

{"id":"11111111-1111-4111-8111-111111111111","type":"webhook_endpoint.verification","occurredAt":"2026-10-20T08:00:00Z","applicationId":"9b2d1241-413e-4ae8-ae32-b15c58dbe101","data":{"challenge":"MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY","generation":1}}

Verify its signature first. Within 10 seconds, answer with a 2xx status and exactly this JSON object, without additional members:

{"challenge":"MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY"}

The challenge expires after five minutes and is tied to this endpoint registration. A stale challenge cannot verify a replacement endpoint. Only after verification can operation outcomes be delivered. Anis staff can view and disable the endpoint.

Where webhooks come from

Anis sends every webhook, the verification challenge and test calls included, from one fixed address per deployment. If your firewall only accepts known sources, allow this address on port 443:

Deployment Address Anis calls from
Sandbox 20.19.1.248
Production 20.71.82.53

The address helps your firewall; it does not prove who sent a request. Always verify the signature: a request from this address with an invalid signature is not from Anis. A change of address is announced in the changelog before it takes effect.

Test reception

POST /v1/webhook-endpoint/test is a signed change whose answer is signed. It sends a real recorded test notification to your verified, enabled endpoint without creating a purchase. A queued test returns 202:

{"id":"22222222-2222-4222-8222-222222222222","type":"webhook_endpoint.test","status":"pending","queuedAt":"2026-10-20T08:00:00Z"}

The signed delivery is:

{"id":"22222222-2222-4222-8222-222222222222","type":"webhook_endpoint.test","occurredAt":"2026-10-20T08:00:00Z","applicationId":"9b2d1241-413e-4ae8-ae32-b15c58dbe101","data":{"message":"Anis webhook test"}}

Handle operation outcomes

Event type When sent
meterCharge.succeeded Charge succeeded
meterCharge.failed Refund landed, or nothing was taken
directRecharge.paid Paid, now pending
directRecharge.paymentFailed Wallet refused payment
directRecharge.suspended Paused for wrong recipient data
directRecharge.completed Delivered
directRecharge.rejected Rejected and refund landed
directRecharge.partiallyRefunded Partial refund settled; occurs only if partial refunds are enabled for the product
{
  "id": "4bceba8e-55d1-59ec-b560-6c1e08f2ae3a",
  "type": "meterCharge.succeeded",
  "occurredAt": "2026-10-20T08:13:02Z",
  "applicationId": "9b2d1241-413e-4ae8-ae32-b15c58dbe101",
  "data": {
    "completedAt": "2026-10-20T08:13:02Z",
    "createdAt": "2026-10-20T08:12:44Z",
    "externalReference": "POS-77-INV-1093",
    "meterNumber": "0268000710262",
    "operationId": "3c1e9b7a-0d42-4f5e-8a61-2b9c7d0e5f13",
    "package": {
      "gecolPackageName": "PKG_100",
      "id": "5f3b8c21-5f3b-4c21-8f3b-5f3b8c215f3b",
      "name": "باقة 100 دينار"
    },
    "personalPrice": {
      "amount": "100.000",
      "currency": "LYD"
    },
    "receipt": {
      "creditedAmount": "88.640",
      "receiptNumber": "260020103744",
      "taxes": [
        {
          "amount": "5.000",
          "description": "ضريبة الاستهلاك"
        },
        {
          "amount": "3.860",
          "description": "رسوم الخدمة"
        }
      ],
      "tokens": [
        {
          "code": "1234 5678 9012 3456 7890",
          "description": "Key change token 1",
          "label": "KCT1",
          "sequence": 1
        },
        {
          "code": "2345 6789 0123 4567 8901",
          "description": "Key change token 2",
          "label": "KCT2",
          "sequence": 2
        },
        {
          "code": "5821 0034 7719 2265 0418",
          "description": "Credit token",
          "label": "STS",
          "sequence": 3
        }
      ]
    },
    "status": "succeeded",
    "total": {
      "amount": "97.500",
      "currency": "LYD"
    },
    "walletId": "71825266-7182-4182-8182-718252667182"
  }
}

data is the operation’s CURRENT public read once the owner projection has caught up to at least the triggering event sequence. Anis never sends it before catch-up and never rebuilds history. The event type says what happened; treat data.status as truth. For example, a directRecharge.suspended delivery after recipient correction can contain data.status: "inProgress". Its operation id is the same id you saved for the purchase. The event id is different: keep it to deduplicate deliveries. The same event keeps the same id and body across retries and manual redelivery. Each attempt has fresh signature times and an attempt header.

Verify first, then record the event id and process the outcome once. A retry of an already handled event should still get a 2xx answer. Return a 2xx within 10 seconds after durably accepting the event; do slower work afterwards. Do not assume notifications arrive in order. Use the operation read when reconciling current state. A suspended application still receives outcomes for operations accepted before suspension.

Retries and recovery

Anis makes 12 attempts over about three days: now, then delays of 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, 8 hours, 12 hours, 24 hours and 24 hours, with ten-percent jitter. A valid Retry-After requesting a longer delay is honored on any failed HTTP response, for at most 24 hours after the failure; a longer value (including a far-future date) waits 24 hours. Any 2xx within 10 seconds is success; a redirect, timeout or other status fails the attempt.

After 24 hours of continuous failure, Anis emails your technical contact and alerts support. Your endpoint is not automatically disabled. Failed deliveries are kept for 30 days and remain eligible for redelivery.

List them with GET /v1/webhook-events?status=failed&since=2026-10-20T00:00:00Z. This signed read has an unsigned answer; pass the opaque nextCursor unchanged as cursor.

{"items":[{"id":"0f8c3e1a-6c7d-5b1e-9a40-2a1f5c9e7d11","type":"meterCharge.succeeded","status":"failed","attempts":12,"lastAttemptAt":"2026-10-23T08:00:00Z","nextAttemptAt":null,"lastResponseCode":503,"lastFailureKind":"httpStatus"}],"nextCursor":null}

Request another delivery with POST /v1/webhook-events/{eventId}/redeliver. It is a signed change and its 202 answer is signed:

{"id":"0f8c3e1a-6c7d-5b1e-9a40-2a1f5c9e7d11","status":"pending","queuedAt":"2026-10-23T08:01:00Z"}

Redelivery resends the event; it never repeats the purchase. Poll the signed meter charge or direct recharge read while your endpoint is unavailable.

Endpoint · Register · Test · Deliveries · Redeliver

This later-state example has a suspended trigger and corrected inProgress data. The read is current truth after catch-up:

{
  "id": "0ef5f62f-b3f0-5436-8e1f-5806629ad7c0",
  "type": "directRecharge.suspended",
  "occurredAt": "2026-10-20T10:00:00Z",
  "applicationId": "9b2d1241-413e-4ae8-ae32-b15c58dbe101",
  "data": {
    "commission": {
      "amount": "5.000",
      "currency": "LYD"
    },
    "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": "فودافون كاش مصر"
    },
    "recipient": [
      {
        "key": "recipient_name",
        "name": {
          "ar": "اسم المستلم",
          "en": "Recipient name"
        },
        "value": "Ahmed Ali Hassan"
      },
      {
        "key": "recipient_number",
        "name": {
          "ar": "رقم المحفظة",
          "en": "Wallet number"
        },
        "value": "01098765432"
      }
    ],
    "requestedAt": "2026-10-20T09:00:00Z",
    "status": "inProgress",
    "total": {
      "amount": "237.500",
      "currency": "LYD"
    },
    "walletId": "71825266-7182-4182-8182-718252667182"
  }
}