Laravel SDK for API key management and Remonode portal integration. Generate, validate, rotate, and revoke API keys locally — Remonode manages and tracks them centrally.
┌──────────────────────────┐ ┌──────────────────────────┐
│ Your Laravel App │ │ Remonode Portal │
│ │ │ │
│ This package lives here │ │ - Central management │
│ │ │ - Billing/Subscriptions │
│ YOU generate keys: │ ──────► │ - Key metadata sync │
│ pk_... + sk_... │ sync │ - Usage tracking │
│ Stored in YOUR DB │ │ - Audit trail │
│ Validated locally │ │ │
│ │ ◄────── │ Portal can provision │
│ SDK provision endpoint │ call │ keys on your app via │
│ POST /portal/provision │ │ the SDK endpoint │
└──────────────────────────┘ └──────────────────────────┘
│ │
▼ ▼
Your API Routes Paystack Payments
(protected by middleware) (handled by Remonode)
Key principle: Your application generates its own keys. Remonode never generates keys for you. However, the Remonode portal can call your app's SDK endpoint to generate keys on behalf of your users.
- PHP 8.1+
- Laravel 10, 11, 12, or 13
- A Remonode account (required for portal sync features)
Create an account at www.remonode.com before using the portal sync features. The email used in your app must match your Remonode account.
composer require remonode/laravel-sdkphp artisan vendor:publish --tag=remonode-config
php artisan vendor:publish --tag=remonode-migrationsphp artisan migrateThis creates the remonode_api_keys table in your database.
# Local key management (works without Remonode portal)
REMONODE_PK_PREFIX=pk_
REMONODE_SK_PREFIX=sk_
REMONODE_ENVIRONMENT=production
REMONODE_API_KEY_ENFORCEMENT=true
REMONODE_CACHE_ENABLED=true
REMONODE_CACHE_TTL=60
# Optional: Remonode portal connection (for sync, billing, and quota features)
# REMONODE_PORTAL_URL=https://remonode.ng
# REMONODE_PORTAL_KEY=your-portal-secret-key
# REMONODE_APP_UUID=your-app-uuid
# REMONODE_APP_URL=https://yourapp.com
# REMONODE_SYNC_TO_PORTAL=true
# REMONODE_QUOTA_ENFORCEMENT=false
# REMONODE_PORTAL_PROVISION_KEYS=trueNote: The portal connection is optional. The SDK works fully offline for local key management. Only enable portal features if you want to sync metadata, manage billing, or enforce quotas through Remonode.
If you want Remonode to track your app's keys and billing:
- Create an account at www.remonode.com
- Go to Profile page to copy your Portal Key
- Add the portal credentials to your
.env:
# Portal connection (optional)
REMONODE_PORTAL_URL=https://remonode.ng
REMONODE_PORTAL_KEY=your-shared-secret- Register your app (the portal stores your app's URL from
config('app.url')):
use Remonode\SDK\RemonodeFacade as Remonode;
$result = Remonode::register(
appName: 'My Laravel App',
ownerEmail: 'admin@myapp.com',
ownerName: 'John Doe'
);
// Returns:
// - 'mode' => 'local_only' if portal not configured or unreachable
// - 'mode' => 'portal' if connected to Remonode
// - 'success' => false with 'error' if portal is unreachableNote: If the portal is unreachable, registration still succeeds locally. The SDK returns a graceful error response instead of throwing an exception.
When enabled, the Remonode portal can call your app's endpoint to generate API keys on behalf of your users. This allows users to generate keys from the portal UI without leaving the dashboard.
REMONODE_PORTAL_PROVISION_KEYS=trueThe portal authenticates using the same PORTAL_KEY shared secret. The provision endpoint is:
POST /api/v1/remonode/portal/provision
Header: X-Portal-Key: {shared_secret}
Body: {
"email": "user@example.com", // or "user_id": 123
"name": "My API Key", // optional
"environment": "production", // optional
"key_type": "both", // optional: both, public, private
"expires_at": "2027-01-01" // optional
}
How it works:
- Portal admin clicks "Generate Key" on a connected app
- Portal calls your app's
POST /api/v1/remonode/portal/provision - Your app generates keys locally in
remonode_api_keystable - Keys are returned to the portal and stored for management
- User can now use the keys on your API endpoints
The SDK provides two middleware options:
1. Key-type specific middleware (recommended for production):
use Illuminate\Support\Facades\Route;
// Public endpoints — protect with PUBLIC KEY (pk_*)
Route::middleware('remonode.key:pk')->group(function () {
Route::get('/api/v1/public/products', [ProductController::class, 'index']);
Route::get('/api/v1/public/categories', [CategoryController::class, 'index']);
});
// Private endpoints — protect with SECRET KEY (sk_*)
Route::middleware('remonode.key:sk')->group(function () {
Route::get('/api/v1/wallet/balance', [WalletController::class, 'balance']);
Route::post('/api/v1/transfer', [TransferController::class, 'store']);
});
// Both key types accepted (legacy behavior)
Route::middleware('remonode.key:any')->group(function () {
Route::get('/api/v1/mixed', [MixedController::class, 'index']);
});2. Legacy middleware (accepts both key types):
use Remonode\SDK\Http\Middleware\ValidateRemonodeApiKey;
Route::middleware(ValidateRemonodeApiKey::class)->group(function () {
Route::get('/api/v1/data', function () {
$apiKey = request()->get('remonode_api_key');
return response()->json([
'message' => 'Authenticated!',
'key_name' => $apiKey->name,
]);
});
});use Remonode\SDK\RemonodeFacade as Remonode;
$result = Remonode::generate(
userId: $user->id,
name: 'Production API Key'
);
// $result['raw_secret'] = 'sk_live_...' ← SHOW THIS TO USER ONCE
// $result['public_key'] = 'pk_live_...'
// $result['key'] = LocalApiKey model (stored in DB)As a developer, I want to install the package and connect my app.
- Create an account at www.remonode.com
- Install the package:
composer require remonode/laravel-sdk
php artisan vendor:publish --tag=remonode-config
php artisan vendor:publish --tag=remonode-migrations
php artisan migrateEdit .env:
REMONODE_PORTAL_URL=https://remonode.ng
REMONODE_PORTAL_KEY=your-shared-secret
REMONODE_APP_UUID=your-app-uuidTest the connection:
php artisan remonode:test-connectionRegister your app (email must match your Remonode account):
use Remonode\SDK\RemonodeFacade as Remonode;
$result = Remonode::register(
appName: 'My Laravel App',
ownerEmail: 'your-email@example.com', // Must exist on www.remonode.com
ownerName: 'Your Name'
);If keys were generated before connecting to the portal, push them manually:
php artisan remonode:push-keysTesting connection to Remonode portal...
URL: https://remonode.ng
Portal Key: ***configured***
Connection successful!
As a developer, I want to generate API keys locally when users sign up or subscribe.
Keys are generated entirely in your application. No HTTP call to Remonode is required.
use Remonode\SDK\RemonodeFacade as Remonode;
// During user registration or subscription activation
$result = Remonode::generate(
userId: $user->id,
name: 'Mobile App Key',
expiresAt: now()->addYear()->toDateTimeString() // optional
);
// Return the secret key to the user ONCE
return response()->json([
'public_key' => $result['public_key'], // pk_live_abc123...
'secret_key' => $result['raw_secret'], // sk_live_xyz789... (SHOW ONCE)
'key_id' => $result['key']->key_id, // sk_live_abc123def456
]);What happens behind the scenes:
- Package generates
pk_+ 32 random chars (public key) - Package generates
sk_+ 40 random chars (secret key) - Extracts 12-char lookup prefix from the secret key
- Hashes the full secret key with SHA-256
- Generates a unique
key_id(16 random chars) for database lookups - Stores everything in your
remonode_api_keystable - The plaintext secret key is returned exactly once — it is never stored
- If
sync_to_portal=true, key metadata is sent to Remonode - Key appears in Remonode portal dashboard for management and billing
Portal visibility: After generation, the key is visible on the Remonode portal under your connected app. Users can view the public key there, but the secret key is only shown once at generation time.
As a developer, I want only authenticated API consumers to access my endpoints.
The SDK provides two middleware approaches:
Use remonode.key middleware with a parameter to restrict endpoints to specific key types:
use Illuminate\Support\Facades\Route;
// ─── Public endpoints — only PUBLIC KEYS (pk_*) allowed ───
Route::middleware('remonode.key:pk')->group(function () {
Route::get('/api/v1/public/products', [ProductController::class, 'index']);
Route::get('/api/v1/public/categories', [CategoryController::class, 'index']);
});
// ─── Private endpoints — only SECRET KEYS (sk_*) allowed ───
Route::middleware('remonode.key:sk')->group(function () {
Route::get('/api/v1/wallet/balance', [WalletController::class, 'balance']);
Route::post('/api/v1/transfer', [TransferController::class, 'store']);
});
// ─── Admin endpoints — require secret keys ───
Route::middleware('remonode.key:sk')->group(function () {
Route::get('/api/v1/admin/stats', [AdminController::class, 'stats']);
});
// ─── Legacy: both key types accepted ───
Route::middleware('remonode.key:any')->group(function () {
Route::get('/api/v1/mixed', [MixedController::class, 'index']);
});| Middleware | Accepts | Header | Use Case |
|---|---|---|---|
remonode.key:pk |
Public keys only | X-Public-Key |
Public APIs, documentation, catalogs |
remonode.key:sk |
Secret keys only | X-Api-Key |
Private APIs, mutations, billing |
remonode.key:any |
Both | X-Api-Key / X-Public-Key / Bearer |
Migration, mixed access |
API consumer requests:
# Public endpoint (pk_*)
curl -H "X-Public-Key: pk_live_abc123..." \
https://yourapp.com/api/v1/public/products
# Private endpoint (sk_*)
curl -H "X-Api-Key: sk_live_abc123..." \
https://yourapp.com/api/v1/wallet/balanceError responses:
// Wrong key type
{ "success": false, "message": "Public key required for this endpoint." }
// Missing key
{ "success": false, "message": "Missing public key. Provide X-Public-Key header with pk_* key." }The original ValidateRemonodeApiKey middleware accepts both pk_* and sk_* keys:
// In your routes/api.php
use Remonode\SDK\Http\Middleware\ValidateRemonodeApiKey;
Route::middleware(ValidateRemonodeApiKey::class)->group(function () {
Route::get('/api/v1/wallet/balance', [WalletController::class, 'balance']);
Route::post('/api/v1/transfer', [TransferController::class, 'store']);
});Validation flow (both middlewares):
- Read header (
X-Api-Key,X-Public-Key, orAuthorization: Bearer) - For
sk_*: extract 12-char prefix → indexed lookup → constant-time hash compare - For
pk_*: direct lookup bypublic_keycolumn - Check key is active and not expired
- Attach key model to request:
$request->get('remonode_api_key') - Optionally enforce monthly quota (if
REMONODE_QUOTA_ENFORCEMENT=true)
If key is invalid:
{ "success": false, "message": "Invalid API key." }As a user, I want to view, rotate, and revoke my API keys.
The package provides ready-made routes:
// routes/api.php — included automatically by the package
// GET /api/v1/remonode/api-keys → List keys
// POST /api/v1/remonode/api-keys → Generate new key pair
// POST /api/v1/remonode/api-keys/{keyId}/rotate → Rotate key
// POST /api/v1/remonode/api-keys/{keyId}/revoke → Revoke key
// Portal-facing routes (require portal key auth):
// POST /api/v1/remonode/portal/provision → Generate keys for a user
// GET /api/v1/remonode/portal/keys → List keys for a user
// POST /api/v1/remonode/portal/keys/{keyId}/revoke → Revoke a keyAll routes require auth:sanctum middleware.
Generate a new key via API:
curl -X POST https://yourapp.com/api/v1/remonode/api-keys \
-H "Authorization: Bearer your-sanctum-token" \
-H "Content-Type: application/json" \
-d '{"name": "My New Key"}'{
"success": true,
"message": "API key pair generated. Store the secret key securely.",
"data": {
"id": 5,
"key_id": "sk_live_a1b2c3d4e5f6g7h8",
"public_key": "pk_live_mypub...",
"secret_key": "sk_live_mysecretkey...",
"masked_secret": "sk_live_...g7h8",
"name": "My New Key",
"status": "active",
"environment": "production",
"created_at": "2026-08-26T10:00:00.000000Z"
}
}Rotate a compromised key:
use Remonode\SDK\RemonodeFacade as Remonode;
$result = Remonode::rotate($keyId);
// Old key is revoked, new key pair is generated
// $result['raw_secret'] contains the new secret — show it onceRevoke a key:
Remonode::revoke($keyId);Lockout protection: The package prevents revoking a user's last active key pair.
As a developer, I want to receive webhook events when subscriptions change.
Webhooks are automatically routed with HMAC-SHA512 signature verification.
// In your EventServiceProvider, listen for package events:
use Remonode\SDK\Events\RemonodeSubscriptionCreated;
use Remonode\SDK\Events\RemonodeSubscriptionUpdated;
use Remonode\SDK\Events\RemonodeSubscriptionCancelled;
use Remonode\SDK\Events\RemonodePaymentFailed;
protected $listen = [
RemonodeSubscriptionCreated::class => [
\App\Listeners\HandleNewSubscription::class,
],
RemonodeSubscriptionUpdated::class => [
\App\Listeners\HandleSubscriptionUpdate::class,
],
RemonodeSubscriptionCancelled::class => [
\App\Listeners\HandleSubscriptionCancel::class,
],
RemonodePaymentFailed::class => [
\App\Listeners\HandlePaymentFailure::class,
],
];// app/Listeners/HandleNewSubscription.php
use Remonode\SDK\Events\RemonodeSubscriptionCreated;
class HandleNewSubscription
{
public function handle(RemonodeSubscriptionCreated $event): void
{
$email = $event->data['customer']['email'] ?? null;
// Enable API access for this user
// Sync subscription status to your local DB
}
}| Event | When | Your Action |
|---|---|---|
RemonodeSubscriptionCreated |
User subscribes | Enable API access |
RemonodeSubscriptionUpdated |
Payment succeeds | Confirm access active |
RemonodeSubscriptionCancelled |
User cancels | Disable API access |
RemonodePaymentFailed |
Payment fails | Warn user, set grace period |
As a developer, I want to use the key management features without connecting to Remonode.
The package works fully offline. Set REMONODE_SYNC_TO_PORTAL=false and leave portal credentials blank:
REMONODE_SYNC_TO_PORTAL=false
REMONODE_PORTAL_URL=
REMONODE_PORTAL_KEY=All local features work:
- Key generation
- Key validation
- Key rotation/revocation
- Middleware protection
- Key management API
Only Remonode-specific features are disabled:
- Portal registration
- Key metadata sync
- Webhook events from Remonode
Graceful fallback: If REMONODE_PORTAL_KEY is set but the portal is unreachable (network issues, server down), the SDK returns {success: false, mode: "local_only"} with a helpful error message instead of throwing exceptions. This ensures your app never breaks due to portal downtime.
| Key | Env Variable | Default | Description |
|---|---|---|---|
portal_url |
REMONODE_PORTAL_URL |
http://localhost:8006 |
Remonode portal base URL |
portal_key |
REMONODE_PORTAL_KEY |
'' |
Shared secret for portal auth |
app_uuid |
REMONODE_APP_UUID |
'' |
Your app's UUID on Remonode |
timeout |
REMONODE_TIMEOUT |
15 |
HTTP timeout (seconds) |
key_generation.public_prefix |
REMONODE_PK_PREFIX |
pk_ |
Public key prefix |
key_generation.secret_prefix |
REMONODE_SK_PREFIX |
sk_ |
Secret key prefix |
key_generation.public_random_length |
— | 32 |
Random chars in public key |
key_generation.secret_random_length |
— | 40 |
Random chars in secret key |
key_generation.hash_algo |
— | sha256 |
Hash algorithm for secret storage |
key_generation.key_id_length |
— | 16 |
Random chars in key_id lookup |
key_generation.secret_lookup_length |
— | 12 |
Prefix chars for indexed lookup |
environment |
REMONODE_ENVIRONMENT |
production |
Key environment (production/sandbox) |
cache_enabled |
REMONODE_CACHE_ENABLED |
true |
Cache validated keys |
cache_ttl |
REMONODE_CACHE_TTL |
60 |
Cache TTL in minutes |
table |
— | remonode_api_keys |
Database table name |
sync_to_portal |
REMONODE_SYNC_TO_PORTAL |
true |
Auto-sync metadata to Remonode |
enforcement |
REMONODE_API_KEY_ENFORCEMENT |
true |
Master middleware switch |
quota_enforcement |
REMONODE_QUOTA_ENFORCEMENT |
false |
Check monthly quota via portal |
webhook_secret |
REMONODE_WEBHOOK_SECRET |
'' |
HMAC secret for webhook verification |
user_model |
REMONODE_USER_MODEL |
App\Models\User |
Your User model class |
portal_provision_keys |
REMONODE_PORTAL_PROVISION_KEYS |
true |
Allow portal to provision keys via SDK |
exempt_uris |
— | ['api/health', ...] |
Routes that bypass API key validation |
The package registers two middleware aliases automatically:
| Alias | Class | Usage |
|---|---|---|
remonode.key |
ValidateRemonodeKeyType |
Key-type specific (:pk, :sk, :any) |
remonode.portal |
AuthenticatePortalKey |
Portal-to-app auth (validates X-Portal-Key header) |
remonode.api_key |
ValidateRemonodeApiKey |
Legacy (accepts both key types) |
Usage examples:
// Key-type specific (recommended)
Route::middleware('remonode.key:pk')->group(...);
Route::middleware('remonode.key:sk')->group(...);
Route::middleware('remonode.key:any')->group(...);
// Portal-facing routes (for SDK provision endpoint)
Route::middleware('remonode.portal')->group(...);
// Legacy (both key types)
Route::middleware('remonode.api_key')->group(...);
// Or use the class directly
use Remonode\SDK\Http\Middleware\ValidateRemonodeApiKey;
Route::middleware(ValidateRemonodeApiKey::class)->group(...);Note: The
remonode.keymiddleware is registered automatically by the service provider. No manual registration required.
use Remonode\SDK\RemonodeFacade as Remonode;
// ─── Local Key Management ────────────────────────────────────
// Generate keys locally
$result = Remonode::generate($userId, $name, $expiresAt);
// Returns: ['key' => LocalApiKey, 'raw_secret' => 'sk_...', 'public_key' => 'pk_...']
// Validate a raw key against local DB
$key = Remonode::validate($rawKey);
// Returns: LocalApiKey or null
// Rotate a key
$result = Remonode::rotate($keyOrKeyId);
// Revoke a key
Remonode::revoke($keyOrKeyId);
// List all keys for a user
$keys = Remonode::listForUser($userId);
// Find by key_id or public_key
$key = Remonode::findByKeyId('sk_live_abc123...');
$key = Remonode::findByPublicKey('pk_live_xyz789...');
// Check user capabilities
$hasKeys = Remonode::hasActiveKeys($userId);
$canRevoke = Remonode::canRevoke($key);
// ─── Portal Integration (optional) ───────────────────────────
// Register with Remonode (returns local_only if portal unreachable)
$result = Remonode::register('My App', 'admin@myapp.com', 'John');
// Returns: ['success' => true, 'mode' => 'local_only'|'portal', ...]
// Manual sync to portal
Remonode::syncToPortal($key);
// ─── Billing & Quotas (requires portal connection) ───────────
// Check current plan, usage, and quota
$result = Remonode::checkQuota();
// Returns: ['success' => true, 'data' => ['plan' => ..., 'usage' => ..., 'quota' => ...]]
// Get available plans
$result = Remonode::getPlans();
// Returns: ['success' => true, 'data' => ['plans' => [...]]]
// Upgrade the app's plan
$result = Remonode::upgradePlan('starter');
// Returns: ['success' => true, 'data' => ['plan' => ...]]Graceful fallback: All portal-dependent methods (register(), checkQuota(), getPlans(), upgradePlan()) return {success: false, mode: "local_only"} with a helpful message instead of throwing exceptions when the portal is unreachable.
| Column | Type | Description |
|---|---|---|
id |
bigint (PK) | Auto-increment |
user_id |
FK → users | Owner of the key (nullable) |
app_uuid |
string(36) | Remonode app UUID (nullable) |
key_id |
string(100) | Unique lookup ID: sk_live_abc123def456 |
secret_prefix |
string(16) | First 12 random chars for indexed lookup |
public_key |
text | Full public key (plaintext, not secret) |
secret_hash |
string | SHA-256 hash of secret key |
secret_last_four |
string(4) | Last 4 chars for masked display |
name |
string | Human-readable label |
status |
string(20) | active, revoked, rotated, expired |
environment |
string(20) | production or sandbox |
remote_id |
string | Remonode's key UUID (if synced) |
expires_at |
timestamp | Optional expiration |
last_used_at |
timestamp | Last validation time |
created_at/updated_at |
timestamps | — |
deleted_at |
timestamp | Soft delete |
Connected apps are auto-assigned the Free plan on registration. Plans control monthly API call limits:
| Plan | Monthly Requests | Price |
|---|---|---|
| Free | 10,000 | ₦0 |
| Starter | 100,000 | ₦5,000/mo |
| Pro | 1,000,000 | ₦25,000/mo |
| Enterprise | Unlimited | Custom |
To enforce quotas, enable it in your .env:
REMONODE_QUOTA_ENFORCEMENT=trueWhen enabled, the middleware returns 429 with quota details when the limit is exceeded. Developers can upgrade via the Remonode portal.
// Check current plan and usage
$quota = Remonode::checkQuota();
// Returns: ['success' => true, 'data' => ['plan' => ..., 'usage' => ..., 'quota' => ...]]
// List available plans
$plans = Remonode::getPlans();
// Returns: ['success' => true, 'data' => ['plans' => [...]]]
// Upgrade plan (triggers Paystack checkout)
$upgrade = Remonode::upgradePlan('starter');When the SDK registers a new app with the portal, the portal creates a user account and sends a welcome email with:
- Temporary password
- Login link
- Change password link
- Security notice recommending immediate password change
The email is queued via Laravel's mail system. Check app/Mail/PortalWelcomeEmail.php and resources/views/emails/portal-welcome.blade.php for customization.
- Secret keys are never stored in plaintext — only SHA-256 hashes
- Timing-safe comparison via
hash_equals()prevents timing attacks - Prefix-based indexed lookup — fast DB queries, only candidates are hash-compared
- Keys shown once — plaintext secret returned at generation, then discarded
- Lockout protection — cannot revoke last active key pair
- Webhook verification — HMAC-SHA512 signature check on all webhook payloads
secret_hashhidden — never serialized in JSON responses- Portal passwords are random — auto-generated 16-char passwords, never reused
- Welcome email queued — sent asynchronously, failures logged but don't block registration
# Test portal connectivity
php artisan remonode:test-connection
# Push local keys TO the portal (for keys that failed to sync)
php artisan remonode:push-keys
php artisan remonode:push-keys --id=sk_live_abc123 # Push specific key
php artisan remonode:push-keys --user=1 # Push keys for a user
php artisan remonode:push-keys --force # Re-push all keys
# Sync key metadata FROM the portal (pull portal keys into local DB)
php artisan remonode:sync-keys
php artisan remonode:sync-keys --email=user@example.comKeys are generated locally in your app and metadata is synced to the portal:
Generate locally (Remonode::generate)
│
▼
Auto-sync to portal (if sync_to_portal=true)
│
├── ✅ Success → remote_id saved on key
│
└── ❌ Failed → key exists locally, not on portal
│
▼
Run: php artisan remonode:push-keys
| Command | Direction | Purpose |
|---|---|---|
Remonode::generate() |
App → Portal | Auto-sync on generation |
php artisan remonode:push-keys |
App → Portal | Push missed keys manually |
php artisan remonode:sync-keys |
Portal → App | Pull portal keys into local DB |
MIT