Alya Books

    Developer docs

    REST API, webhooks, MCP server and CLI for UAE e-invoicing

    Webhooks

    Receive signed HTTP notifications when invoices are reported, paid, received and more.

    Webhooks push events to an https endpoint you control. Create them in the app under API access → Webhooks or with the API (scope webhooks:manage).

    What an invoice event carries

    Every invoice.* payload has the invoice header (id, number, type, status, dates, currency, totals, buyer, isTest, metadata, externalId) and two blocks that let a receiver act without calling back:

    • validation: validity and the blocking errors[] from the last check, each with rule, field, message and a public docsUrl.
    • rejection: on invoice.rejected, the stage (taxilla, buyer or fta), the provider's error text, the submissionId and when it happened; null otherwise.

    For the explained version of each error (API field path, English and Arabic explanation and fix) call GET /invoices/{id}/validation.

    Create a subscription

    curl -X POST "$BASE/webhooks" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{
      "name": "ERP sync",
      "targetUrl": "https://erp.example.com/hooks/einvoicing",
      "events": ["invoice.reported", "invoice.rejected", "invoice.paid", "incoming.validated"],
      "headers": { "Authorization": "Bearer my-inbound-token" }
    }'
    

    The response includes secret (whsec_…) once. Use ["*"] to subscribe to every event. Targets must be public https hosts; private networks, localhost and cloud metadata addresses are rejected, and DNS answers pointing to private ranges are refused at delivery time.

    Events

    EventWhen
    invoice.createdAn invoice, credit note or debit note was created (draft or validated)
    invoice.sentEmailed or logged as sent
    invoice.submittedHanded to the ASP
    invoice.deliveredAccepted by the buyer's access point
    invoice.reportedAccepted by the FTA (status accepted)
    invoice.rejectedRejected by the ASP, the buyer's access point or the FTA
    invoice.paidPayments cover the full amount
    invoice.overdueThe daily job flagged the invoice as overdue
    invoice.deletedA draft was deleted
    validation.completedAn API-triggered validation finished (validate: true or POST …/validate)
    incoming.created, incoming.validated, incoming.accepted, incoming.rejected, incoming.paidReceived documents
    quote.created, quote.sent, order_confirmation.created, proforma.sent, reminder.created, reminder.sent, document.sentOther documents
    customer.created, product.createdCatalogue
    payment_link.paidA pay-by-link payment was confirmed by the gateway (data.invoiceId, data.paymentId, data.provider)
    bank.transaction_matchedA bank transaction was matched to an invoice or supplier invoice and a payment recorded
    connector.syncedAn accounting connector finished a sync (data.provider, counts, errors)

    GET /webhooks/events returns the same list with descriptions.

    Delivery format

    POST /hooks/einvoicing HTTP/1.1
    Content-Type: application/json
    User-Agent: Webhook-Delivery/1.0
    X-Webhook-Event: invoice.paid
    X-Webhook-Delivery: 7c0d…            (unique per delivery attempt series)
    X-Webhook-Event-Id: 9b1e…            (unique per event; the same for every subscriber)
    X-Webhook-Attempt: 1
    X-Webhook-Signature: t=1700000000,v1=5f1a…
    
    {
      "id": "9b1e…",
      "event": "invoice.paid",
      "createdAt": "2026-09-23T10:15:00Z",
      "organizationId": "…",
      "data": {
        "id": "3f2e…", "invoiceNumber": "INV-0042", "status": "accepted", "paymentStatus": "paid",
        "total": 6100, "paidAmount": 6100, "currency": "AED",
        "buyer": { "id": "…", "name": "Buyer LLC", "trn": "100000000000011" }, "isTest": false
      }
    }
    

    Respond with any 2xx within 10 seconds. Redirects are not followed.

    Verify the signature

    v1 is the hex HMAC-SHA256 of "<t>.<raw body>" with your secret. Reject timestamps older than five minutes to prevent replays.

    import { createHmac, timingSafeEqual } from "node:crypto";
    
    export function verify(secret: string, header: string, rawBody: string): boolean {
    const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
    if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
    const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
    return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
    }
    
    import hmac, hashlib, time
    
    def verify(secret: str, header: str, raw_body: bytes) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      if abs(time.time() - int(parts["t"])) > 300:
          return False
      expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, parts.get("v1", ""))
    

    Use the raw request body, not a re-serialised JSON object.

    Retries and monitoring

    Failed deliveries (non-2xx, timeout, connection error) are retried after at least 1 min, 5 min, 30 min, 2 h and 12 h (a job sends due retries every 5 minutes); after six attempts the delivery is marked failed. A webhook that accumulates 25 exhausted deliveries in a row is paused with a disabledReason; fix the endpoint and set isActive: true to resume.

    RoutePurpose
    GET /webhooks/{id}/deliveries?status=failedDelivery log with attempts, response status, error, duration
    GET /webhooks/{id}/deliveries/{deliveryId}One delivery including its payload
    POST /webhooks/{id}/deliveries/{deliveryId}/retryRetry now
    POST /webhooks/{id}/testSend a webhook.test event and return the result
    POST /webhooks/{id}/rotate-secretNew signing secret

    Deliveries are idempotent from your side when you key on X-Webhook-Event-Id: the same event may be delivered more than once after a retry.

    How delivery runs

    Events are written to an outbox by database triggers the moment the change is committed. A worker pushes them out immediately after API and app actions (submission, sending, imports, payments) and every minute from the scheduler, so a delivery normally arrives within seconds and never later than a minute.