---
title: "Webhooks — Anis Developers"
description: "Verify an endpoint, receive operation outcomes and recover failed deliveries."
url: https://developers.anis.ly/docs/webhooks/
language: en
---

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

# 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:

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

A configured endpoint answers `GET` and `PUT` with `200`:

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

With no endpoint, `GET` answers `200`:

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

```json
{"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](https://developers.anis.ly/docs/verifying-webhooks.md) first. Within 10 seconds, answer with a 2xx status and exactly this JSON object, without additional members:

```json
{"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](https://developers.anis.ly/docs/verifying-webhooks.md): a request from this address with an invalid signature is not from Anis. A change of address is announced in the [changelog](https://developers.anis.ly/changelog.md) 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`:

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

The signed delivery is:

```json
{"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 |

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

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

```json
{"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](https://developers.anis.ly/reference/read-webhook-endpoint.md) · [Register](https://developers.anis.ly/reference/set-webhook-endpoint.md) · [Test](https://developers.anis.ly/reference/test-webhook-endpoint.md) · [Deliveries](https://developers.anis.ly/reference/get-webhook-events.md) · [Redeliver](https://developers.anis.ly/reference/redeliver-webhook-event.md)

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

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