laravel-nafith maintained by mahmoud-hamed
Laravel Nafith
Laravel Nafith is a Laravel package for integrating with the Nafith Electronic Promissory Note (Sanad) platform.
This Nafith Laravel package handles OAuth2 authentication, sanad creation, sanad closing, status sync, and HMAC-SHA256 webhook verification. It also provides a database-backed tracking model that works with any Eloquent model, including models that use string or UUID primary keys.
Features
- Laravel package for Nafith API integration
- Nafith sanad creation and closing workflows
- Signed webhook verification for Nafith callbacks
- Status sync command for pending and approved sanads
- Database tracking with polymorphic Eloquent relations
- Encrypted storage for sensitive debtor and payload data
Why This Package
- Speeds up Nafith integration in Laravel 11 and Laravel 12 apps
- Keeps webhook handling and signature verification in one place
- Gives you a reusable Nafith service layer instead of one-off API code
- Works well for Saudi fintech and promissory note workflows
Requirements
- PHP 8.2+
- Laravel 11.x | 12.x
- A Nafith merchant account (sandbox or production)
Installation
1. Require the package
# From Packagist
composer require mahmoud-hamed/laravel-nafith
# From a local path (development)
composer require mahmoud-hamed/laravel-nafith --repositories='[{"type":"path","url":"../laravel-nafith"}]'
Laravel's auto-discovery registers the service provider automatically.
2. Publish config and migration
php artisan vendor:publish --tag=nafith-config
php artisan vendor:publish --tag=nafith-migrations
This creates:
config/nafith.php-- all API credentials and settingsdatabase/migrations/xxxx_create_nafith_sanads_table.php-- the tracking table
3. Run the migration
php artisan migrate
4. Add environment variables
Add these to your .env:
NAFITH_BASE_URL=https://sandbox.nafith.sa/
NAFITH_CLIENT_ID=your-client-id
NAFITH_CLIENT_SECRET=your-client-secret
NAFITH_SIGN_SECRET=your-sign-secret
NAFITH_SCOPE=read write
NAFITH_SIGNATURE_HOST=nafith.sa
NAFITH_TOKEN_CACHE_KEY=nafith_access_token
NAFITH_TOKEN_TTL=3500
NAFITH_CITY_OF_ISSUANCE=1
NAFITH_CITY_OF_PAYMENT=1
NAFITH_APPROVAL_WINDOW_HOURS=6
NAFITH_MAX_APPROVE_DURATION=360
NAFITH_CURRENCY=SAR
NAFITH_TIMEOUT=30
NAFITH_RETRY_TIMES=1
NAFITH_RETRY_SLEEP=500
NAFITH_WEBHOOK_URI=nafith/callback
NAFITH_WEBHOOK_PREFIX=
NAFITH_LOG_SENSITIVE_DATA=false
5. Add log channels (recommended)
Add these to your config/logging.php under the 'channels' array:
'nafith' => [
'driver' => 'daily',
'path' => storage_path('logs/nafith.log'),
'level' => env('LOG_NAFITH_LEVEL', 'info'),
'days' => 14,
],
'nafith_webhook' => [
'driver' => 'daily',
'path' => storage_path('logs/nafith-webhook.log'),
'level' => env('LOG_NAFITH_WEBHOOK_LEVEL', 'info'),
'days' => 14,
],
Sensitive payload data is redacted from logs by default. Only enable NAFITH_LOG_SENSITIVE_DATA=true for short-lived local debugging.
Quick Start
Creating a Sanad
use MahmoudHamed\Nafith\Services\NafithService;
$nafithService = app(NafithService::class);
$sanad = $nafithService->createSanad([
'debtor_national_id' => '1000000001',
'debtor_phone' => '+966500000000',
'debtor_name' => 'Ahmad Ali',
'amount' => 30000.00,
'currency' => 'SAR',
'reference_id' => 'ORDER-12345',
'nafithable_type' => App\Models\Order::class,
'nafithable_id' => 12345,
]);
// $sanad is a MahmoudHamed\Nafith\Models\NafithSanad instance
echo $sanad->sanad_number; // e.g. "1010062627658168"
echo $sanad->status; // "pending"
Closing a Sanad
$nafithService->closeSanad($sanad);
// Automatically sends CLOSED if approved, CANCELLED_BY_CREDITOR otherwise
Syncing Status from Nafith API
$freshSanad = $nafithService->syncStatus($sanad);
// Returns updated model if status changed, null if unchanged
Running the Artisan Command
# Sync all pending/approved sanads from the Nafith API
php artisan nafith:sync-statuses
# Process in smaller chunks
php artisan nafith:sync-statuses --chunk=50
Scheduling the Sync Command
Add to routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('nafith:sync-statuses')->everyFiveMinutes();
Webhook Setup
The package automatically registers a webhook endpoint at:
POST {your-domain}/nafith/callback
Configure the URI and middleware in config/nafith.php:
'webhook_uri' => 'nafith/callback', // The path
'webhook_prefix' => '', // Optional prefix (e.g. 'api')
'webhook_middleware' => ['throttle:60,1'], // Public endpoint protection
Webhook Flow
- Nafith sends a POST to your webhook URL
- The package verifies the HMAC-SHA256 signature
- Parses the payload and normalizes the status
- Dispatches
NafithWebhookReceivedevent - The default listener updates the
NafithSanadrecord in the database
Listening for Webhooks
The default listener handles database updates automatically. To add custom behavior (e.g., send notifications), listen to the event:
use MahmoudHamed\Nafith\Events\NafithWebhookReceived;
// In your EventServiceProvider or via Event::listen()
Event::listen(NafithWebhookReceived::class, function (NafithWebhookReceived $event) {
$data = $event->data;
// $data['status'] -- normalized status (e.g. 'approved')
// $data['sanad_number'] -- the Sanad number
// $data['group_id'] -- the Sanad group ID
// $data['reference_id'] -- your reference ID
// $data['payload'] -- the full raw webhook payload
if ($data['status'] === 'approved') {
// Send notification, update order status, etc.
}
});
Using the NafithSanad Model
The NafithSanad model uses a polymorphic nafithable relation, so it works with any Eloquent model.
Sensitive debtor fields and stored payload snapshots are encrypted at rest and hidden from JSON serialization by default.
Adding the Relationship to Your Model
use MahmoudHamed\Nafith\Models\NafithSanad;
class Order extends Model
{
public function nafithSanads()
{
return $this->morphMany(NafithSanad::class, 'nafithable');
}
}
Querying Sanads
use MahmoudHamed\Nafith\Models\NafithSanad;
use MahmoudHamed\Nafith\NafithStatus;
// All pending sanads
$sanads = NafithSanad::pending()->get();
// All expired pending sanads (past approval window)
$sanads = NafithSanad::expiredPending()->get();
// Sanads for a specific model
$sanads = NafithSanad::forModel(App\Models\Order::class, $orderId)->get();
// Check status
$sanad->isNafithApproved(); // true/false
$sanad->isNafithPending(); // true/false
$sanad->isExpired(); // true/false
$sanad->canClose(); // true/false
Getting the Parent Model
$sanad = NafithSanad::find(1);
$parentModel = $sanad->nafithable; // Returns the related Order, Bond, etc.
Using the Low-Level API Client
If you need direct API access without the service layer:
use MahmoudHamed\Nafith\Nafith;
$nafith = app(Nafith::class);
// Create a Sanad
$response = $nafith->createSingleSanad([
'debtor' => ['national_id' => '1000000001'],
'city_of_issuance' => '1',
'debtor_phone_number' => '+966500000000',
'total_value' => 30000,
'currency' => 'SAR',
'max_approve_duration' => 360,
'reference_id' => 'MY-REF-123',
'sanad' => [
['due_type' => 'upon request', 'total_value' => 30000, 'reference_id' => 'SANAD-1'],
],
]);
// Close a Sanad
$response = $nafith->closeSanad('group-id-123', 'closed');
// Get status
$response = $nafith->getSanadStatus('group-id-123');
// Verify a webhook signature
$valid = $nafith->verifyWebhook($signature, $timestamp, $rawBody, $path);
// Normalize any status string
$normalized = $nafith->normalizeStatus('accept'); // returns 'approved'
NafithStatus Enum
use MahmoudHamed\Nafith\NafithStatus;
NafithStatus::PENDING; // 'pending'
NafithStatus::APPROVED; // 'approved'
NafithStatus::REJECTED_BY_DEBTOR; // 'rejected_by_debtor'
NafithStatus::NO_RESPONSE; // 'no_response'
NafithStatus::CANCELLED_BY_CREDITOR; // 'cancelled_by_creditor'
NafithStatus::CLOSED; // 'closed'
// Helpers
NafithStatus::PENDING->label(); // English label
NafithStatus::PENDING->labelAr(); // Arabic label
NafithStatus::PENDING->color(); // Bootstrap color
NafithStatus::PENDING->isActive(); // true (pending/approved are active)
NafithStatus::PENDING->isFinal(); // false
NafithStatus::PENDING->canApprove(); // true
NafithStatus::PENDING->canReject(); // true
NafithStatus::PENDING->canCancel(); // true
// From string
NafithStatus::fromString('approved'); // NafithStatus::APPROVED
NafithStatus::fromString(null); // null
Architecture
./
src/
Nafith.php -- Core API client (OAuth, HTTP, signatures)
NafithException.php -- Exception with HTTP status + body
NafithStatus.php -- Backed string enum
NafithServiceProvider.php -- Package bootstrap
Models/NafithSanad.php -- Polymorphic tracking model
Services/NafithService.php -- High-level lifecycle service
Http/Controllers/
NafithCallbackController.php -- Webhook endpoint
Console/Commands/
SyncNafithStatuses.php -- Artisan sync command
Events/
NafithWebhookReceived.php -- Webhook event
config/nafith.php -- Publishable config
database/migrations/
create_nafith_sanads_table.php -- Publishable migration
routes/webhook.php -- Auto-registered route
License
MIT