Astro
API endpoints and SSR usage for @lumierelabs/daraja.
SSR must be enabled
Astro's API routes (and any route that calls Daraja server-side) require
output: 'server' or output: 'hybrid' in astro.config.mjs, with
individual routes opting in via export const prerender = false; under hybrid
mode. A fully static (output: 'static') site can't make outbound
authenticated calls at request time there's no server to run them on.
Singleton client
import { Daraja } from "@lumierelabs/daraja";
export const daraja = Daraja({
consumerKey: import.meta.env.DARAJA_CONSUMER_KEY,
consumerSecret: import.meta.env.DARAJA_CONSUMER_SECRET,
environment: import.meta.env.PROD ? "production" : "sandbox",
});Add the corresponding entries to src/env.d.ts so import.meta.env is typed:
interface ImportMetaEnv {
readonly DARAJA_CONSUMER_KEY: string;
readonly DARAJA_CONSUMER_SECRET: string;
readonly MPESA_PASSKEY: string;
readonly APP_URL: string;
}Initiating an STK Push
import type { APIRoute } from "astro";
import { daraja } from "../../lib/daraja";
import { DarajaError } from "@lumierelabs/daraja";
export const POST: APIRoute = async ({ request }) => {
const { phoneNumber, amount, orderRef } = await request.json();
try {
const push = await daraja.stkPush({
businessShortCode: "174379",
passkey: import.meta.env.MPESA_PASSKEY,
transactionType: "CustomerPayBillOnline",
amount,
partyA: phoneNumber,
partyB: "174379",
phoneNumber,
callBackURL: `${import.meta.env.APP_URL}/api/callbacks/stk`,
accountReference: orderRef,
});
return new Response(
JSON.stringify({ checkoutRequestId: push.CheckoutRequestID }),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
);
} catch (error) {
const message =
error instanceof DarajaError
? error.message
: "Payment failed to initiate";
return new Response(JSON.stringify({ error: message }), { status: 400 });
}
};
export const prerender = false;STK Push callback handler
import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request }) => {
const payload = await request.json();
const { CheckoutRequestID, ResultCode, ResultDesc, CallbackMetadata } =
payload.Body.stkCallback;
if (ResultCode === 0) {
const items: Array<{ Name: string; Value?: string | number }> =
CallbackMetadata?.Item ?? [];
const get = (name: string) => items.find((i) => i.Name === name)?.Value;
console.log("Payment confirmed", {
checkoutRequestId: CheckoutRequestID,
amount: get("Amount"),
receipt: get("MpesaReceiptNumber"),
});
} else {
console.log("Payment failed", CheckoutRequestID, ResultDesc);
}
return new Response(
JSON.stringify({ ResultCode: 0, ResultDesc: "Accepted" }),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
);
};
export const prerender = false;Calling it from a page during SSR
---
import { daraja } from '../../lib/daraja';
const checkoutRequestId = Astro.url.searchParams.get('crid');
let status = null;
if (checkoutRequestId) {
status = await daraja.stkPushQuery({
businessShortCode: '174379',
passkey: import.meta.env.MPESA_PASSKEY,
checkoutRequestId,
});
}
---
<html lang="en">
<body>
{status && <p>{status.ResultDesc}</p>}
</body>
</html>Every route in this guide needs export const prerender = false; under
Astro's hybrid output mode without it, Astro tries to render the route at
build time, when no phoneNumber/amount body exists yet, and the build
fails or silently produces a static (wrong) response.