Skip to content

Webhooks

Astro sends signed webhook events to your server when payment state changes.

Registering a webhook endpoint

Set webhook_url when creating a payment session, or configure a default in the merchant dashboard.

Signature verification

Every webhook delivery includes an X-OpenWave-Signature header:

X-OpenWave-Signature: sha256=<HMAC-SHA256(raw_body, webhook_secret)>

Always verify this before processing the event. Use constant-time comparison to prevent timing attacks.

typescript
import { WebhookReceiver } from '@neptune.fintech/astro-sdk'

const receiver = new WebhookReceiver({ secret: process.env.ASTRO_WEBHOOK_SECRET! })

receiver.on('payment.completed', (event) => {
  const { session_id, reference, amount } = event.data
  db.orders.markPaid(reference)
})

// Express handler
app.post('/webhooks/astro', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-openwave-signature'] as string
  if (!receiver.verify(req.body.toString(), sig)) return res.sendStatus(401)
  receiver.dispatch(JSON.parse(req.body))
  res.sendStatus(200)
})
dart
final verifier = WebhookVerifier(secret: Platform.environment['ASTRO_WEBHOOK_SECRET']!);

// In your HTTP handler (shelf, etc.)
final rawBody = await request.readAsString();
final sig = request.headers['x-openwave-signature'] ?? '';
verifier.handleRaw(rawBody, sig, {
  'payment.completed': (payload) {
    final ref = (payload['data'] as Map)['reference'];
    db.markPaid(ref);
  },
});
kotlin
val receiver = WebhookReceiver(secret = System.getenv("ASTRO_WEBHOOK_SECRET"))

receiver.on("payment.completed") { payload ->
    val ref = payload.jsonObject["data"]?.jsonObject?.get("reference")?.jsonPrimitive?.content
    db.markPaid(ref)
}

// Ktor route
post("/webhooks/astro") {
    val body = call.receiveText()
    val sig = call.request.headers["X-OpenWave-Signature"] ?: return@post call.respond(401)
    receiver.handle(body, sig)
    call.respond(HttpStatusCode.OK)
}
swift
let verifier = astro.webhookVerifier(secret: ProcessInfo.processInfo.environment["ASTRO_WEBHOOK_SECRET"]!)

// Vapor route
app.post("webhooks", "astro") { req async throws -> HTTPStatus in
    let body = req.body.string ?? ""
    let sig = req.headers.first(name: "X-OpenWave-Signature") ?? ""
    try verifier.handle(rawBody: body, signature: sig, handlers: [
        "payment.completed": { payload in
            let ref = (payload["data"] as? [String: Any])?["reference"] as? String
            await db.markPaid(ref)
        }
    ])
    return .ok
}

Event types

EventWhen it fires
payment.completedCredit confirmed at merchant's bank (cross-bank: after LyPay; same-bank: instant)
payment.failedPayment declined or failed
payment.expiredSession TTL exceeded without completion
mandate.createdRecurring mandate activated
mandate.cancelledMandate cancelled by customer or merchant
mandate.payment_collectedRecurring charge successfully collected

Event payload

json
{
  "event": "payment.completed",
  "api_version": "1.0.0",
  "timestamp": "2026-04-24T06:00:00Z",
  "data": {
    "session_id": "ops_01HZGV...",
    "reference": "order_1042",
    "amount": 50000,
    "currency": "LYD",
    "settlement_type": "lypay",
    "creditor_bank": "andalus"
  }
}

Only fulfil on payment.completed

Never fulfil orders on payment.processing alone. payment.completed fires only after the credit is confirmed at the merchant's bank.

Retries

Astro retries failed webhook deliveries with exponential backoff: 1min → 5min → 30min → 2h → 24h. Return HTTP 2xx to acknowledge.

Built on the OpenWave open standard.