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
| Event | When it fires |
|---|---|
payment.completed | Credit confirmed at merchant's bank (cross-bank: after LyPay; same-bank: instant) |
payment.failed | Payment declined or failed |
payment.expired | Session TTL exceeded without completion |
mandate.created | Recurring mandate activated |
mandate.cancelled | Mandate cancelled by customer or merchant |
mandate.payment_collected | Recurring 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.