aghfatehi / laravel-saudi-fda
Laravel package for Saudi Food and Drug Authority (SFDA) API integration. SDK for Cosmetics, Drugs, Food & Medical Devices with OAuth2 auto-auth. ربط الهيئة العامة للغذاء والدواء السعودية مع لارافيل - دعم مستحضرات التجميل، الأدوية، المنتجات الغذائية، والأجهزة الطبية
Requires
- php: ^8.1
- ext-curl: *
- ext-json: *
- ext-mbstring: *
- illuminate/console: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^9.0|^10.0|^11.0
README
Saudi FDA (SFDA) API Integration for Laravel
Laravel package for the Saudi Food and Drug Authority public APIs — Cosmetics, Drugs, Food, Medical Devices
By FsoftDev.com — AL-AGHBARI Fatehi
SFDA integration for Laravel — automatic OAuth2 authentication, cosmetics & drug & food & medical device APIs
Table of Contents
- Requirements & Installation
- Configuration
- Quick Start
- Usage
- API Routes
- Error Handling
- Events
- Artisan Commands
- Testing
- Postman Collection
- License
Important: The SFDA API restricts access to Saudi IP addresses only. If your server is outside Saudi Arabia, you must contact SFDA support to add your server IP to their allowed list, or use a Saudi-based server/VPN. Without this, all connection attempts will time out on port 9002.
Requirements & Installation
| Requirement | Version |
|---|---|
| PHP | ^8.1 |
| Laravel | 9.x · 10.x · 11.x · 12.x · 13.x |
| Extensions | json, curl |
composer require aghfatehi/laravel-saudi-fda
Auto-discovery is enabled — no manual service provider registration needed.
Publish the configuration (optional):
php artisan vendor:publish --tag=saudi-fda-config
Configuration
Add to your .env file:
SFDA_CONSUMER_KEY=your_consumer_key SFDA_CONSUMER_SECRET=your_consumer_secret SFDA_ENVIRONMENT=sandbox
All Configuration Options
| Variable | Default | Required | Description |
|---|---|---|---|
SFDA_CONSUMER_KEY |
— | Yes | Your SFDA Consumer Key |
SFDA_CONSUMER_SECRET |
— | Yes | Your SFDA Consumer Secret |
SFDA_ENVIRONMENT |
sandbox |
No | sandbox or production |
SFDA_TOKEN_CACHE_ENABLED |
true |
No | Cache the OAuth2 access token |
SFDA_TOKEN_CACHE_STORE |
file |
No | Cache driver (file, redis, memcached, etc.) |
SFDA_TOKEN_CACHE_KEY |
sfda_access_token |
No | Custom cache key for the token |
SFDA_API_TIMEOUT |
60 |
No | HTTP request timeout in seconds |
SFDA_ROUTES_ENABLED |
true |
No | Enable/disable built-in API routes |
SFDA_ROUTES_PREFIX |
api/saudi-fda |
No | URI prefix for built-in routes |
SFDA_LOGGING_ENABLED |
true |
No | Enable API call logging |
SFDA_LOG_LEVEL |
info |
No | Log level (debug, info, notice, warning, error) |
SFDA_LOG_DATABASE_ENABLED |
false |
No | Log API requests to sfda_api_logs table |
SFDA_LOG_DATABASE_CONNECTION |
— | No | Database connection for logging (defaults to your default DB) |
Override Base URLs (optional)
Each service base URL can be overridden individually:
| Variable | Default |
|---|---|
SFDA_OAUTH_BASE |
https://apis.sfda.gov.sa:9002/v2/oauth |
SFDA_COSMETICS_BASE |
https://apis.sfda.gov.sa:9002/v2/cosmetics |
SFDA_DRUGS_BASE |
https://apis.sfda.gov.sa:9002/v2/DMS |
SFDA_FOOD_BASE |
https://apis.sfda.gov.sa:9002/v2/Food |
SFDA_MEDICAL_DEVICES_BASE |
https://apis.sfda.gov.sa:9002/v2/dwh-md |
Quick Start
# Full health check — config + authentication + API connectivity php artisan saudi-fda:check # View configuration (credentials masked) php artisan saudi-fda:check --config
Using the Facade
use Aghfatehi\SaudiFda\Facades\SaudiFda; SaudiFda::isConfigured(); // bool — credentials present in config SaudiFda::isReady(); // bool — credentials valid, token obtained SaudiFda::environment(); // \Aghfatehi\SaudiFda\Enums\Environment
Dependency Injection
use Aghfatehi\SaudiFda\SaudiFdaClient; class ProductController extends Controller { public function __construct(private SaudiFdaClient $sfda) {} public function show($barcode) { return $this->sfda->cosmetics()->getByBarcode($barcode); } }
Token Storage & Cache
The access token is automatically cached using Laravel's cache system to avoid requesting a new token on every API call.
How it works:
- On first API call, the package requests an OAuth2 token from SFDA
- The token (as an
AccessTokenDTO) is stored in the cache with a TTL ofexpiresIn - 300seconds (5-minute safety margin) - Subsequent calls check the cache first — if a valid
AccessTokenDTOis found, it's reused - If a cached token exists but is expired, or if
forceRefreshis used, a new token is fetched and the cache is updated - If any API call receives a 401 Unauthorized, the package automatically refreshes the token and retries the request once
Cache configuration via .env:
SFDA_TOKEN_CACHE_ENABLED=true # Enable/disable token caching SFDA_TOKEN_CACHE_STORE=file # Cache driver (file, redis, memcached, database) SFDA_TOKEN_CACHE_KEY=sfda_access_token # Cache key name
Example — force refresh token:
use Aghfatehi\SaudiFda\Facades\SaudiFda; // Bypass cache, always get a fresh token $token = SaudiFda::auth()->getAccessToken(true); // Token details $token->accessToken; // string — the Bearer token $token->expiresIn; // int — seconds until expiry (typically 86400) $token->tokenType; // string — "Bearer"
Example — clear cached token manually:
use Illuminate\Support\Facades\Cache; Cache::store(config('saudi-fda.token_cache.store', 'file')) ->forget(config('saudi-fda.token_cache.key', 'sfda_access_token'));
Example — use a different cache store (Redis example):
SFDA_TOKEN_CACHE_STORE=redis
The package stores a serialized AccessTokenDTO object. Any Laravel cache driver that supports serialization works out of the box.
How auto-refresh works:
Request -> 401 Unauthorized -> Package auto-refreshes token -> Retries request -> Succeeds
This happens transparently in ApiClient — the method tokenRefreshCallback is called when a 401 is detected, and the request is retried once.
Usage
All methods use the SaudiFda facade to access the four service groups:
use Aghfatehi\SaudiFda\Facades\SaudiFda; SaudiFda::cosmetics(); // CosmeticsService SaudiFda::drugs(); // DrugService SaudiFda::food(); // FoodService SaudiFda::medicalDevices(); // MedicalDeviceService
Authentication
The package handles OAuth2 Client Credentials automatically — tokens are obtained, cached, and refreshed transparently. If any API call receives a 401 response, the package automatically requests a new token and retries once.
SFDA Endpoint: POST /v2/oauth/accesstoken?grant_type=client_credentials
Auth: HTTP Basic (Consumer Key : Consumer Secret)
Token Expiry: 86400 seconds (24 hours)
use Aghfatehi\SaudiFda\Facades\SaudiFda; // Get token (uses cache if available) $token = SaudiFda::auth()->getAccessToken(); $token->accessToken; // string — the Bearer token $token->expiresIn; // int — seconds until expiry // Force a fresh token (bypass cache) $token = SaudiFda::auth()->getAccessToken(true); // Check credentials validity SaudiFda::auth()->validateCredentials(); // bool
Cosmetics API
Base URL: https://apis.sfda.gov.sa:9002/v2/cosmetics
1. list(array $options = [])
Paginated list of cosmetic products.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page |
int | No | 1 | Page number |
limit |
int | No | — | Results per page |
Keyword |
string | No | — | Filter by keyword |
SaudiFda::cosmetics()->list(['page' => 1, 'limit' => 50]); SaudiFda::cosmetics()->list(['Keyword' => 'cream']); SaudiFda::cosmetics()->list(['page' => 2, 'limit' => 20, 'Keyword' => 'lotion']);
SFDA Endpoint: GET /v2/cosmetics/list?page=&limit=&Keyword=
2. getById(int $productId)
Get a single cosmetic product by its SFDA product ID.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
productId |
int | Yes | — | SFDA product identifier |
SaudiFda::cosmetics()->getById(1495);
SFDA Endpoint: GET /v2/cosmetics/Product_Id/{productID}
3. getByCosmeticNumber(string $cosmeticNumber)
Get a cosmetic product by its registration number.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
cosmeticNumber |
string | Yes | — | Cosmetic registration number (e.g., CN-2023-08203) |
SaudiFda::cosmetics()->getByCosmeticNumber('CN-2023-08203');
SFDA Endpoint: GET /v2/cosmetics/cosmeticNumber/{cosmeticNumber}
4. getByBarcode(string $barcode)
Get a cosmetic product by its barcode (EAN/UPC).
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
barcode |
string | Yes | — | Product barcode |
SaudiFda::cosmetics()->getByBarcode('6281007990215');
SFDA Endpoint: GET /v2/cosmetics/BarCode/{barcode}
5. search(array $options = [])
Advanced search across multiple cosmetic product fields. All parameters are optional — filtered results include only the fields you supply.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
SpecificNameAr |
string | No | — | Arabic specific name |
SpecificName |
string | No | — | English specific name |
BrandName |
string | No | — | Brand name |
barCode |
string | No | — | Barcode |
CosmeticNumber |
string | No | — | Cosmetic registration number |
page |
int | No | 1 | Page number |
limit |
int | No | — | Results per page |
SaudiFda::cosmetics()->search(['BrandName' => 'AVON', 'page' => 1]); SaudiFda::cosmetics()->search(['SpecificNameAr' => 'كريم', 'limit' => 10]); SaudiFda::cosmetics()->search(['barCode' => '6281007990215']);
SFDA Endpoint: GET /v2/cosmetics/search
6. searchByKeyword(string $keyword, int $page = 1)
Search cosmetic products by a free-text keyword with pagination.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
keyword |
string | Yes | — | Search term (goes in URL path) |
page |
int | No | 1 | Page number |
SaudiFda::cosmetics()->searchByKeyword('AVON', 1); SaudiFda::cosmetics()->searchByKeyword('cream', 2);
SFDA Endpoint: GET /v2/cosmetics/search/{keyword}/{page}
7. getImage(string $imageCode)
Get product image data.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
imageCode |
string | Yes | — | Image name/code |
SaudiFda::cosmetics()->getImage('IMG-2023-12345');
SFDA Endpoint: GET /v2/cosmetics/image/{image_code}
Drugs API
Base URL: https://apis.sfda.gov.sa:9002/v2/DMS
1. list(array $options = [])
Paginated list of registered drug products in the Saudi market.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page |
int | No | 1 | Page number |
limit |
int | No | — | Results per page |
SaudiFda::drugs()->list(['page' => 1, 'limit' => 100]); SaudiFda::drugs()->list(['page' => 5]);
SFDA Endpoint: GET /v2/DMS/drug/list?page=&limit=
Sample Response:
{
"data": [
{
"registerNumber": "21-37-10",
"tradeName": "ORELOX 100MG TABLETS",
"scientificName": "CEFPODOXIME",
"atcCode1": "J01DD14",
"strength": "100",
"price": "30.80",
"pharmaceuticalForm": { "nameEn": "Tablet" },
"marketingStatus": { "nameEn": "Marketed" },
"legalStatus": { "nameEn": "Prescription" },
"company": { "nameEn": "SANOFI WINTHROP INDUSTRIE" }
}
],
"currentPage": 1,
"pageCount": 791,
"pageSize": 15,
"rowCount": 11856
}
Food API
Base URL: https://apis.sfda.gov.sa:9002/v2/Food
1. list(array $options = [])
Paginated list of food products.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page |
int | No | 1 | Page number |
limit |
int | No | — | Results per page |
SaudiFda::food()->list(['page' => 1, 'limit' => 50]);
SFDA Endpoint: GET /v2/Food/product/list/{page}?limit=
2. getById(int $productId)
Get a food product by its SFDA ID.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
productId |
int | Yes | — | SFDA product identifier |
SaudiFda::food()->getById(1449070);
SFDA Endpoint: GET /v2/Food/product/id/{id}
3. getByReferenceNumber(string $referenceNumber)
Get a food product by its reference number.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
referenceNumber |
string | Yes | — | Reference number (e.g., P-3-N-200621-107719) |
SaudiFda::food()->getByReferenceNumber('P-3-N-200621-107719');
SFDA Endpoint: GET /v2/Food/product/referencenumber/{referenceNumber}
4. getByBarcode(string $barcode)
Get a food product by its barcode.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
barcode |
string | Yes | — | Product barcode |
SaudiFda::food()->getByBarcode('50254156');
SFDA Endpoint: GET /v2/Food/product/barcode/{barcode}
5. search(array $options = [])
Search food products by keyword.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
keyword |
string | Yes | — | Search term |
page |
int | No | 1 | Page number |
SaudiFda::food()->search(['keyword' => 'chocolate', 'page' => 1]); SaudiFda::food()->search(['keyword' => 'milk', 'page' => 2]);
SFDA Endpoint: GET /v2/Food/product/search/{keyword}/{page}
6. getImage(string $imageCode)
Get food product image data.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
imageCode |
string | Yes | — | Image name/code |
SaudiFda::food()->getImage('FOOD-IMG-12345');
SFDA Endpoint: GET /v2/Food/image/{image_code}
Medical Devices API
Base URL: https://apis.sfda.gov.sa:9002/v2/dwh-md
The Medical Devices API is split into three categories.
Low Risk Devices
listLowRisk(array $options = [])
Paginated list of low-risk medical devices.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page |
int | No | 1 | Page number |
limit |
int | No | — | Results per page |
SaudiFda::medicalDevices()->listLowRisk(['page' => 1]); SaudiFda::medicalDevices()->listLowRisk(['page' => 2, 'limit' => 10]);
SFDA Endpoint: GET /v2/dwh-md/Lowrisk/list/{page}?limit=
getLowRiskProduct(?int $lowRiskId = null, ?int $productId = null, ?string $accountNumber = null, ?string $registrationNumber = null, ?string $crNumber = null)
Get a low-risk device by any combination of identifiers. At least one parameter should be provided.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
lowRiskId |
int | No | null | Low Risk ID |
productId |
int | No | null | Product ID |
accountNumber |
string | No | null | Account number |
registrationNumber |
string | No | null | Registration/license number |
crNumber |
string | No | null | Commercial Registration (CR) number |
SaudiFda::medicalDevices()->getLowRiskProduct(lowRiskId: 123); SaudiFda::medicalDevices()->getLowRiskProduct(registrationNumber: 'LIC-123'); SaudiFda::medicalDevices()->getLowRiskProduct(crNumber: 'CR-456');
SFDA Endpoint: GET /v2/dwh-md/Lowrisk/Product?LowRiskID=&productID=&AccountNumber=&RegistrationNumber=&CrNumber=
searchLowRisk(string $keyword, int $page = 1)
Search low-risk devices by keyword.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
keyword |
string | Yes | — | Search term |
page |
int | No | 1 | Page number |
SaudiFda::medicalDevices()->searchLowRisk('face mask', 1);
SFDA Endpoint: GET /v2/dwh-md/Lowrisk/search/{keyword}/{page}
GHTF Devices
listGHTF(array $options = [])
Paginated list of GHTF devices.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page |
int | No | 1 | Page number |
limit |
int | No | — | Results per page |
SaudiFda::medicalDevices()->listGHTF(['page' => 1]);
SFDA Endpoint: GET /v2/dwh-md/GHTF/list/{page}?limit=
getGHTFProduct(?int $propertiesId = null, ?int $mdId = null, ?string $referenceNumber = null, ?string $accountNumber = null, ?string $deviceNumber = null, ?string $crNumber = null)
Get a GHTF device by any combination of identifiers.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
propertiesId |
int | No | null | Properties ID |
mdId |
int | No | null | MD ID |
referenceNumber |
string | No | null | Reference number |
accountNumber |
string | No | null | Account number |
deviceNumber |
string | No | null | Device/license number |
crNumber |
string | No | null | Commercial Registration number |
SaudiFda::medicalDevices()->getGHTFProduct(propertiesId: 456); SaudiFda::medicalDevices()->getGHTFProduct(deviceNumber: 'LIC-456');
SFDA Endpoint: GET /v2/dwh-md/GHTF/Product?PropertiesId=&MDId=&ReferenceNumber=&AccountNumber=&DeviceNumber=&CrNumber=
getGHTFAccessory(int $propertiesId)
Get GHTF device accessory details.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
propertiesId |
int | Yes | — | Properties ID of the accessory |
SaudiFda::medicalDevices()->getGHTFAccessory(11);
SFDA Endpoint: GET /v2/dwh-md/GHTF/Accessory/id/{PropertiesId}
searchGHTF(string $keyword, int $page = 1)
Search GHTF devices by keyword.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
keyword |
string | Yes | — | Search term |
page |
int | No | 1 | Page number |
SaudiFda::medicalDevices()->searchGHTF('hospital bed', 1);
SFDA Endpoint: GET /v2/dwh-md/GHTF/search/{keyword}/{page}
TFA Devices
listTFA(array $options = [])
Paginated list of TFA devices.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page |
int | No | 1 | Page number |
limit |
int | No | — | Results per page |
SaudiFda::medicalDevices()->listTFA(['page' => 1]);
SFDA Endpoint: GET /v2/dwh-md/TFA/list/{page}?limit=
getTFAAccessory(int $propertiesId)
Get TFA device accessory details.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
propertiesId |
int | Yes | — | Properties ID of the accessory |
SaudiFda::medicalDevices()->getTFAAccessory(11);
SFDA Endpoint: GET /v2/dwh-md/TFA/Accessory/id/{PropertiesId}
searchTFA(string $keyword, int $page = 1)
Search TFA devices by keyword.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
keyword |
string | Yes | — | Search term |
page |
int | No | 1 | Page number |
SaudiFda::medicalDevices()->searchTFA('ultrasound', 1);
SFDA Endpoint: GET /v2/dwh-md/TFA/search/{keyword}/{page}
API Routes
The package registers built-in API routes under /api/saudi-fda (configurable via SFDA_ROUTES_PREFIX). All routes resolve the SaudiFdaClient via Laravel's service container.
| Method | Endpoint | Description | Route Name |
|---|---|---|---|
GET |
/api/saudi-fda/status |
Package health check | saudi-fda.status |
POST |
/api/saudi-fda/auth/token |
Get OAuth2 access token | saudi-fda.auth.token |
GET |
/api/saudi-fda/cosmetics |
List cosmetics (query: page, limit, Keyword) |
saudi-fda.cosmetics.list |
GET |
/api/saudi-fda/cosmetics/{id} |
Get cosmetic by product ID | saudi-fda.cosmetics.by-id |
GET |
/api/saudi-fda/cosmetics/number/{cosmeticNumber} |
Get cosmetic by registration number | saudi-fda.cosmetics.by-number |
GET |
/api/saudi-fda/cosmetics/barcode/{barcode} |
Get cosmetic by barcode | saudi-fda.cosmetics.by-barcode |
POST |
/api/saudi-fda/cosmetics/search |
Advanced cosmetics search | saudi-fda.cosmetics.search |
GET |
/api/saudi-fda/drugs |
List drugs (query: page, limit) |
saudi-fda.drugs.list |
GET |
/api/saudi-fda/food |
List food products | saudi-fda.food.list |
GET |
/api/saudi-fda/food/{id} |
Get food by ID | saudi-fda.food.by-id |
POST |
/api/saudi-fda/food/search |
Search food products | saudi-fda.food.search |
GET |
/api/saudi-fda/medical-devices/low-risk |
List Low Risk devices | saudi-fda.medical-devices.low-risk |
GET |
/api/saudi-fda/medical-devices/ghtf |
List GHTF devices | saudi-fda.medical-devices.ghtf |
GET |
/api/saudi-fda/medical-devices/tfa |
List TFA devices | saudi-fda.medical-devices.tfa |
To disable routes, set SFDA_ROUTES_ENABLED=false in your .env.
Error Handling
Every API method throws one of two exception types:
| Exception | When |
|---|---|
AuthenticationException |
Invalid or missing credentials |
SaudiFdaException |
API error (network, rate limit, 4xx/5xx, timeout) |
use Aghfatehi\SaudiFda\Exceptions\SaudiFdaException; use Aghfatehi\SaudiFda\Exceptions\AuthenticationException; try { $products = SaudiFda::cosmetics()->list(); } catch (AuthenticationException $e) { // Check SFDA_CONSUMER_KEY and SFDA_CONSUMER_SECRET report($e); } catch (SaudiFdaException $e) { // Network error, rate limit, or SFDA server error report($e); }
Database Logging
Every API request/response can be stored in the sfda_api_logs table for auditing, debugging, and analytics.
Enable database logging in .env:
SFDA_LOG_DATABASE_ENABLED=true SFDA_LOG_DATABASE_CONNECTION=mysql # optional, defaults to default DB connection
Create the table:
php artisan vendor:publish --tag=saudi-fda-migrations php artisan migrate
What gets logged:
| Column | Type | Description |
|---|---|---|
service |
string | API service name (cosmetics, drugs, food, medical_devices) |
endpoint |
string | API endpoint called |
method |
string | HTTP method (GET) |
http_code |
int | HTTP status code |
request_payload |
json | Request parameters (masked for sensitive data) |
response_payload |
json | API response data (masked for sensitive data) |
error_message |
text | Error message if the request failed |
duration_ms |
float | Request duration in milliseconds |
ip_address |
string | Client IP address |
created_at |
timestamp | When the request was made |
Query logs with Eloquent:
use Aghfatehi\SaudiFda\Models\SaudiFdaApiLog; // Recent failed requests $failures = SaudiFdaApiLog::whereNotNull('error_message') ->latest() ->take(10) ->get(); // Slow requests (> 2 seconds) $slow = SaudiFdaApiLog::where('duration_ms', '>', 2000) ->latest() ->get(); // Requests by service $cosmeticsLogs = SaudiFdaApiLog::where('service', 'cosmetics') ->whereDate('created_at', today()) ->get();
Sensitive data masking: When database logging is enabled, the package automatically masks credentials, tokens, and authorization headers in the logged payloads (e.g., Ejmb****).
Events
| Event | Fired When | Payload |
|---|---|---|
ApiRequestSucceeded |
Any API request succeeds | Endpoint + duration |
ApiRequestFailed |
Any API request fails | Endpoint + response data |
Artisan Commands
# Full health check — config + authentication + API connectivity php artisan saudi-fda:check # Test authentication only php artisan saudi-fda:check --auth # View current configuration (credentials masked) php artisan saudi-fda:check --config
Testing
vendor/bin/phpunit
The package includes PHPUnit tests for:
- Facade resolution
- Singleton service instances
- Configuration checks
- Authentication errors
CI: GitHub Actions runs tests across PHP 8.1–8.4 x Laravel 9–13 (26 matrix combinations).
Postman Collection
The repository includes a complete Postman collection: SFDA-API-Postman.json
Features:
- All 24 SFDA API endpoints with response examples
- Pre-request script for automatic OAuth2 token acquisition
- Test scripts that validate responses and handle 401 token expiry
- Uses environment variables for credentials (never hardcoded)
How to use:
- Postman -> Import -> Select
SFDA-API-Postman.json - Click Environment -> Add (or edit an existing environment)
- Add these Environment variables:
SFDA_CONSUMER_KEY= your consumer keySFDA_CONSUMER_SECRET= your consumer secret
- Make your first request — the token is fetched automatically
License
MIT — Created by AL-AGHBARI Fatehi — FsoftDev.com