@ng-archefusion/angular
v1.1.2
Published
Official Angular SDK for the Archefusion payment orchestration platform
Readme
@ng-archefusion/angular
Official Angular SDK for Archefusion — a payment orchestration platform that intelligently routes transactions through the best available payment gateway (Paystack, Flutterwave, Monnify, Interswitch, and more) using a single integration.
Table of Contents
- Requirements
- Installation
- Architecture
- API Key Setup
- Register the SDK
- Usage
- Auth Interceptor Safety
- Angular Version Compatibility
- Error Reference
- API Reference
- TypeScript Types
- Security Checklist
- Troubleshooting
- Links
Requirements
| Dependency | Version |
|------------|---------|
| @angular/core | >= 12.0.0 |
| @angular/common | >= 12.0.0 |
| rxjs | >= 7.0.0 |
Node.js
>= 18.0.0is required only for server-side webhook verification (Angular Universal / Express). Browser-only apps have no Node.js requirement.
Installation
npm install @ng-archefusion/angularArchitecture
Archefusion API keys are secret keys — there is no separate public/publishable key for browser use. Putting the key in your Angular app means it will be visible in the JavaScript bundle.
The correct flow for a browser SPA is:
Browser (Angular) → Your backend API → Archefusion APIYour Angular app calls your own backend, which holds the secret key and calls Archefusion. The backend returns only what the frontend needs (e.g. the checkout redirect URL).
If you use Angular Universal (SSR), you can call Archefusion directly from the server-rendered side — the key never reaches the browser.
API Key Setup
- Log in to your Archefusion merchant dashboard.
- Navigate to Settings → API Keys.
- Copy your Secret Key — it starts with
af_live_(production) oraf_test_(test mode).
Store it as a server-side environment variable. Never hardcode it or put it in environment.ts.
# .env (server only — never commit this file)
ARCHEFUSION_API_KEY=af_live_YOUR_KEY_HERE
ARCHEFUSION_WEBHOOK_SECRET=whsec_YOUR_WEBHOOK_SECRET_HERERegister the SDK
Registration is only needed if you are calling Archefusion directly from the server (Angular Universal). Browser SPAs that proxy through their own backend do not need this.
Standalone app (Angular 14+)
// app.config.ts
import { provideHttpClient } from '@angular/common/http';
import { provideArchefusion } from '@ng-archefusion/angular';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideArchefusion({ apiKey: process.env['ARCHEFUSION_API_KEY'] }),
],
};NgModule app
// app.module.ts
import { HttpClientModule } from '@angular/common/http';
import { ArchefusionModule } from '@ng-archefusion/angular';
@NgModule({
imports: [
HttpClientModule,
ArchefusionModule.forRoot({ apiKey: process.env['ARCHEFUSION_API_KEY'] }),
],
})
export class AppModule {}Usage
Browser SPA — call your own backend
Your Angular service calls your own API endpoint. No API key in the browser.
// src/app/shared/services/payment.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({ providedIn: 'root' })
export class PaymentService {
constructor(private http: HttpClient) {}
initiate(payload: { orderId: string; amount: number; currency: string; email?: string }) {
return this.http.post<{ redirectUrl: string }>(
`${environment.apiBaseUrl}/api/payments/initiate`,
payload
);
}
verify(paymentId: string) {
return this.http.post<{ status: string }>(
`${environment.apiBaseUrl}/api/payments/${paymentId}/verify`,
{}
);
}
}Your backend receives the request, calls Archefusion with the secret key, and returns the redirect URL:
// backend route (Express / NestJS)
import { PaymentsService } from '@ng-archefusion/angular';
async function initiatePayment(req, res) {
const result = await firstValueFrom(
paymentsService.initiate({
merchantOrderId: req.body.orderId,
amount: req.body.amount,
currency: req.body.currency,
customer: { email: req.body.email },
redirectUrl: req.body.redirectUrl,
})
);
res.json({ redirectUrl: result.data.recommendedGateway.session.redirectUrl });
}Then in your component:
pay(): void {
this.paymentService.initiate({ orderId: `order-${crypto.randomUUID()}`, amount: 5000, currency: 'NGN', email: '[email protected]' })
.subscribe({
next: (res) => window.location.href = res.redirectUrl,
error: (err) => console.error(err),
});
}Angular Universal (SSR) — call Archefusion server-side
Inject PaymentsService directly in a server-side service using PLATFORM_ID to prevent browser execution:
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { PaymentsService, InitiatePaymentRequest } from '@ng-archefusion/angular';
@Injectable({ providedIn: 'root' })
export class SsrPaymentService {
constructor(
private payments: PaymentsService,
@Inject(PLATFORM_ID) private platformId: object,
) {}
initiate(payload: InitiatePaymentRequest) {
if (!isPlatformServer(this.platformId)) {
throw new Error('Payment initiation must run server-side only.');
}
return this.payments.initiate(payload);
}
}Verify a payment
After the customer pays, Archefusion redirects them to your redirectUrl with ?paymentId=xxx appended. Always verify server-side before delivering value — do not trust the status query parameter.
// callback.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { PaymentService } from '@app/shared/services/payment.service';
@Component({
selector: 'app-payment-callback',
standalone: true,
template: `
@if (loading) { <p>Verifying payment...</p> }
@else if (status === 'SUCCESS') { <p>Payment successful!</p> }
@else { <p>Payment did not complete. Status: {{ status }}</p> }
`,
})
export class CallbackComponent implements OnInit {
loading = true;
status = '';
constructor(
private route: ActivatedRoute,
private router: Router,
private paymentService: PaymentService,
) {}
ngOnInit(): void {
const paymentId = this.route.snapshot.queryParamMap.get('paymentId');
if (!paymentId) { this.router.navigate(['/']); return; }
this.paymentService.verify(paymentId).subscribe({
next: (res) => { this.status = res.status; this.loading = false; },
error: () => { this.status = 'ERROR'; this.loading = false; },
});
}
}Payment statuses:
| Status | Meaning |
|--------|---------|
| INITIATED | Payment created, customer not yet redirected. |
| PENDING | Customer at checkout, payment not yet completed. |
| SUCCESS | Payment completed. Safe to deliver value. |
| FAILED | Payment failed or was declined. |
| REFUNDED | Payment was refunded. |
Handle webhooks
Webhooks deliver real-time payment events to your server even if the customer never returns to your app. Use WebhookService to verify the HMAC-SHA256 signature.
Must run server-side (Angular Universal / Express). Never put your webhook secret in browser code.
// server/webhook.handler.ts (Express)
import express from 'express';
import { WebhookService } from '@ng-archefusion/angular';
const app = express();
app.post(
'/webhooks/archefusion',
express.raw({ type: 'application/json' }), // raw body — do NOT parse as JSON first
async (req, res) => {
const webhookService = new WebhookService({ apiKey: '' }); // only secret is used here
const result = await webhookService.verify(
req.headers,
req.body.toString(),
process.env['ARCHEFUSION_WEBHOOK_SECRET']
);
if (!result.valid) return res.status(400).send('Invalid signature');
switch (result.event!.type) {
case 'payment.succeeded': await fulfillOrder(result.event!.data.paymentId); break;
case 'payment.failed': await notifyCustomer(result.event!.data.paymentId); break;
case 'refund.successful': await processRefund(result.event!.data.paymentId); break;
}
res.status(200).send('OK');
}
);Supported events:
| Event | Fires when |
|-------|-----------|
| payment.created | A payment is initiated. |
| payment.updated | A payment status changes. |
| payment.succeeded | A payment completes successfully. |
| payment.failed | A payment fails for any reason. |
| refund.created | A refund is initiated. |
| refund.successful | A refund completes successfully. |
| refund.failed | A refund fails. |
Signature verification rules:
- Always pass the raw request body — do not JSON-parse and re-stringify.
- Respond
200 OKimmediately after signature verification; process asynchronously. - Make your handler idempotent — Archefusion retries failed deliveries. Guard against duplicate processing using the event
id.
Auth Interceptor Safety
If your app has an HTTP interceptor that attaches a user JWT to every outgoing request, it will break any direct SDK calls by overwriting the Authorization header and potentially logging the user out on gateway 401 responses.
Add a gateway URL check to skip both behaviours:
// auth.interceptor.ts
const isArchefusionGateway = req.url.includes('api-gateway.archefusion.com');
// Skip JWT injection for gateway requests
if (!isArchefusionGateway && token && !AuthUtils.isTokenExpired(token)) {
clonedReq = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
}
// Skip logout for gateway 401s
catchError((error: HttpErrorResponse) => {
if (error.status === 401 && !isArchefusionGateway) {
authService.handleUnauthorized();
}
return throwError(() => error);
})If you follow the browser-proxy pattern (your Angular app calls your own backend, not Archefusion directly), your interceptor will never see Archefusion requests and you can skip this entirely.
Angular Version Compatibility
| Angular version | Support | |----------------|---------| | 12, 13 | Supported — NgModule setup; constructor injection in your services | | 14, 15, 16 | Supported — standalone or NgModule | | 17+ | Supported — standalone or NgModule |
The SDK uses constructor-based dependency injection throughout. There are no DI conflicts across Angular versions.
Angular 12 / 13: inject() field initializers are not available. Use constructor injection when consuming the SDK:
@Injectable({ providedIn: 'root' })
export class MyService {
constructor(private payments: PaymentsService) {}
}Error Reference
| HTTP Status | Cause | Fix |
|-------------|-------|-----|
| 400 | Invalid payload — missing required field or bad currency code | Log err.error for details. Check initiate() payload. |
| 401 | Invalid or missing API key | Check the key. If using an interceptor, ensure it is not overwriting the Authorization header. |
| 403 | API key lacks permission | Check key permissions in the dashboard. |
| 404 | paymentId not found | Confirm the ID is correct and belongs to your account. |
| 422 | No eligible gateways for this currency/method | Go to Settings → Gateways and confirm at least one active gateway matches your request. |
| 429 | Rate limited | Slow down requests or contact support. |
| 500 | Archefusion server error | Retry with exponential backoff. |
API Reference
PaymentsService
initiate(payload: InitiatePaymentRequest): Observable<InitiatePaymentResponse>
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| merchantOrderId | string | Yes | Unique order ID. Must be unique per transaction. |
| amount | number | Yes | Amount in the main currency unit (e.g. 1000 = ₦1,000). |
| currency | string | Yes | ISO 4217 code — NGN, GHS, USD, etc. |
| paymentMethod | 'CARD' \| 'TRANSFER' | No | Helps smart routing pick the right gateway. |
| redirectUrl | string | No | Where to send the customer after payment. |
| customer.email | string | No | Required by some gateways. Always recommended. |
| customer.phone | string | No | Required by some gateways (e.g. Interswitch). |
| customerCountry | string | No | ISO 3166-1 alpha-3 (e.g. NGA). Improves cross-border routing. |
| metadata | object | No | Any extra data. Returned in webhook events. |
verify(paymentId: string): Observable<VerifyPaymentResponse>
Verifies the outcome of a payment using the paymentId from initiate() or the redirect URL query param.
WebhookService
verify(headers, rawBody, webhookSecret?): Promise<WebhookVerificationResult>
| Param | Description |
|-------|-------------|
| headers | Raw request headers. |
| rawBody | Unparsed request body string. |
| webhookSecret | Your Archefusion webhook secret. |
Returns { valid: boolean, event?: WebhookEvent, error?: string }.
provideArchefusion(config) / ArchefusionModule.forRoot(config)
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| apiKey | string | Yes | Your Archefusion secret API key. |
| baseUrl | string | No | Override the API base URL. Defaults to https://dev.api-gateway.archefusion.com. |
TypeScript Types
import type {
ArchefusionConfig,
InitiatePaymentRequest,
InitiatePaymentResponse,
VerifyPaymentResponse,
Payment,
PaymentStatus,
PaymentMethod,
WebhookEvent,
WebhookEventType,
WebhookHeaders,
WebhookVerificationResult,
PaymentWebhookEvent,
RefundWebhookEvent,
} from '@ng-archefusion/angular';Security Checklist
- [ ] API key is a server-side environment variable — never in
environment.tsor the browser bundle. - [ ] Payment initiation and verification go through your backend — your Angular frontend calls your own API, not Archefusion directly.
- [ ] Test key in development (
af_test_...), live key in production server environment only. - [ ] Auth interceptor skips gateway requests — see Auth Interceptor Safety.
- [ ] Webhook secret is server-side only — never in browser code.
- [ ] Payments verified server-side before delivering value. Never trust the
statusquery param on the redirect URL. - [ ] Webhook handler is idempotent — guard against duplicate events using the event
id. - [ ] Raw body passed to
webhookService.verify()— do not parse and re-stringify. - [ ] Rotate your API key from the dashboard immediately if it is ever exposed.
Troubleshooting
401 from the gateway logs the user out
Cause: Auth interceptor catches the gateway 401 and calls signOut().
Fix: Add the isArchefusionGateway guard — see Auth Interceptor Safety.
Payment 401 even though the API key looks correct
Cause: Auth interceptor is overwriting the Authorization header with the user's JWT.
Fix: Same as above.
422 — No eligible gateways
Cause: No active gateway on your account matches the currency / paymentMethod you sent.
Fix: Go to Settings → Gateways, confirm a gateway is connected and supports your currency and method. Try omitting paymentMethod to let routing decide.
merchantOrderId already exists
Cause: The same merchantOrderId was sent twice. It must be globally unique.
Fix: Generate a unique ID per transaction:
merchantOrderId: `order-${crypto.randomUUID()}`Redirect URL not working after payment
Cause: The redirectUrl route doesn't exist in your app, or an auth guard is blocking unauthenticated access.
Fix: Confirm the route is registered in app.routes.ts and that the callback path is whitelisted in any auth guard.
