Modern, Smart, and Standardized Integration for Delivery Management
Universal V2 Protocol - Compatible with ALL V2 Systems
Version 2.0 - Production ReadyThis Quick Start assumes you're already registered with Jenni Logistics.
If you don't have your system_code and credentials yet:
{
"username": "user",
"password": "pass"
}
{
"shipment_number": "001",
"receiver_name": "أحمد",
"amount_iqd": 50000
}
{
"shipment_numbers":
["001"]
}
| # | API Name | Method | Endpoint | Auth Required | Mode | Purpose |
|---|---|---|---|---|---|---|
| 1 | Login | POST | /v2/auth/login |
❌ No | Receive/Push | Authenticate & get JWT token |
| 2 | Refresh Token | POST | /v2/auth/refresh |
🔑 Refresh Token | Receive/Push | Renew expired token |
| 3 | Create Shipments | POST | /v2/shipments/create |
✅ Yes | Receive In You → Jenni | Bulk create (max 100) |
| 4 | Query Shipments | POST | /v2/shipments/query |
✅ Yes | Receive In Track in Jenni | Query by IDs/Numbers (max 100) OR paginated list of all user's shipments |
| 5 | Update Status | POST | /v2/shipments/update-status |
✅ Yes | Receive In You → Jenni | Postpone, Return, Deliver, etc. |
| 6 | Edit Shipment | PUT | /v2/shipments/edit |
✅ Yes | Receive In Edit in Jenni | Edit shipment details (partial update) |
| 7 | Generate Stickers | POST | /v2/shipments/stickers |
✅ Yes | Receive/Push | Generate PDF stickers for printing (max 100) |
| 8 | Return Reasons | GET | /v2/reference/return-reasons |
✅ Yes | Receive/Push | List of return reasons (Download: Excel/JSON) |
| 9 | Postponed Reasons | GET | /v2/reference/postponed-reasons |
✅ Yes | Receive/Push | List of postponed reasons (Download: Excel/JSON) |
| 10 | Governorates | GET | /v2/reference/governorates |
❌ No | Receive/Push | List of governorate codes (Download: Excel/JSON) - Public Geographic Data |
| 11 | Cities | GET | /v2/reference/cities |
❌ No | Receive/Push | List of cities with pagination (Download: Excel/JSON) - Public Geographic Data |
| 12 | Available Actions | GET | /v2/reference/available-actions |
✅ Yes | Receive/Push | Actions available for a shipment - Requires ownership verification |
| 13 | Action Codes | GET | /v2/reference/action-codes |
❌ No | Receive/Push | Complete list of professional action codes - Public Reference |
| 14 | Order Statistics | GET | /v2/statistics/orders-by-status |
✅ Yes | Receive In Your stats in Jenni | Count of orders grouped by status |
| 15 | Orders in Process | GET | /v2/orders/in-process |
✅ Yes | Receive In Your orders in Jenni | List all in-process orders with pagination |
| 16 | Delete Shipment | DELETE | /v2/orders/{shipment_id} |
✅ Yes | Receive In Delete from Jenni | Delete shipment by ID. Only shipments in initial stages (SORTING_CENTER, NEW_CUSTOMER_SHIPMENTS) and initial steps (NEW_ORDER_TO_PRINT, NEW_ORDER_TO_PICKUP, NEW_WITH_PA) can be deleted. Ownership verified. |
| 17 | Orders by Step | GET | /v2/orders/by-step |
✅ Yes | Receive In Filter from Jenni | Get orders filtered by step status |
| 18 | Get Countries | GET | /v2/reference/countries |
❌ No | Receive/Push | List of countries |
| 19 | Get My Stores | GET | /v2/merchants/my-stores |
✅ Yes | Management | Get merchant's stores list (Master Customer) |
| 20 | Create Store | POST | /v2/stores/create |
✅ Yes | Management | Create new store with auto-generated password |
| 21 | Create Merchant | POST | /v2/merchant-management/create |
✅ Yes | Management | Create new merchant (AGGREGATOR role) |
| 22 | Update Merchant | PUT | /v2/merchant-management/update |
✅ Yes | Management | Update merchant info (AGGREGATOR role) |
| 23 | 📤 Push Shipments | POST | /v2/shipments/create |
✅ Yes | On YOUR Server ⚠️ Jenni → You: Implement on your server | Receive shipments FROM Jenni - You must implement POST /v2/shipments/create on YOUR server to receive shipments |
| 24 | 📥 Status Updates (Push Out) | POST | /v2/push/update-status |
✅ Yes | On Jenni Server ⚠️ You → Jenni: Send updates to Jenni server | Send status updates to Jenni - You call this endpoint on Jenni server to report delivery status changes |
| 25 | Create Ticket | POST | /v2/tickets/create |
✅ Yes | Receive/Push | Create support ticket |
| 26 | Get My Tickets | GET | /v2/tickets/by-user |
✅ Yes | Receive/Push | List user tickets with pagination |
| 27 | Search Tickets | POST | /v2/tickets/search |
✅ Yes | Receive/Push | Search tickets with filters |
| 28 | Get Ticket Statuses | GET | /v2/tickets/statuses |
✅ Yes | Receive/Push | List all ticket statuses |
| 29 | Get Ticket Departments | GET | /v2/tickets/departments |
✅ Yes | Receive/Push | List all ticket departments |
| 30 | Get Ticket Causes | GET | /v2/tickets/causes |
✅ Yes | Receive/Push | List all ticket causes |
| 31 | Get Ticket History | GET | /v2/tickets/{ticket_id}/history |
✅ Yes | Receive/Push | Get ticket conversation history |
| 32 | Get Initial Ticket Status | GET | /v2/tickets/initial-status |
✅ Yes | Receive/Push | Get initial status for new tickets |
| 33 | Upload Ticket Attachment | POST | /v2/tickets/{ticket_id}/attachment |
✅ Yes | Receive/Push | Upload file attachment to ticket |
| 34 | Get Payment Info | GET | /v2/payments/{payment_id} |
✅ Yes | Receive/Push | Get payment manifest details with all shipments (V2 mapped properties: snake_case, mapped stages/steps, governorate names, balance info) |
| 35 | Get Settled Payments | GET | /v2/payments/settled |
✅ Yes | Receive/Push | List settled payments with pagination |
| 36 | Get Return Info | GET | /v2/returns/{return_id} |
✅ Yes | Receive/Push | Get RTO manifest details with all shipments (V2 mapped properties: snake_case, mapped stages/steps, governorate names) |
| 37 | Get Settled Returns | GET | /v2/returns/settled |
✅ Yes | Receive/Push | List settled returns with pagination |
Bookmark this section for quick access to the most common API patterns and field requirements.
# Login
POST https://jenni.alzaeemexp.com/api/v2/auth/login
{
"username": "your_username",
"password": "your_password"
}
# Use Token
Authorization: Bearer {token}
# Refresh
POST https://jenni.alzaeemexp.com/api/v2/auth/refresh
Headers: Authorization: Bearer {refreshToken}
POST https://jenni.alzaeemexp.com/api/v2/shipments/create
{
"system_code": "YOUR_CODE",
"shipments": [{
"shipment_number": "SHIP001",
"external_shipment_id": "12345",
"receiver_name": "أحمد علي",
"receiver_phone_1": "07901234567",
"governorate_code": "BGD",
"city": "الكرادة",
"amount_iqd": 50000
}]
}
external_shipment_id is mandatory (sender's shipment ID for tracking). Jenni Logistics uses this ID when sending status updates back to you.
shipment_id from response - you'll need it to update shipment status later.
POST https://jenni.alzaeemexp.com/api/v2/shipments/query
{
"shipment_numbers": ["SHIP001", "SHIP002"]
}
# OR by IDs: { "shipment_ids": [12345, 12346] }
# OR paginated: { "page": 0, "page_size": 20 }
# Max 100 per request (specific lookup)
# Max 100 per page (paginated mode)
POST https://jenni.alzaeemexp.com/api/v2/shipments/update-status
# Postpone
{
"shipment_id": 12345,
"action": "POSTPONED",
"postponed_reason": "العميل غير موجود",
"postponed_reason_en": "Customer not available",
"postponed_reason_ku": "کڕیار بەردەست نییە",
"postponed_date_id": 1 // 1=tomorrow, 2=in 2 days, 3=in 3 days
}
# Return
{
"shipment_id": 12345,
"action": "RETURN_TO_STORE",
"return_reason": "العنوان خاطئ",
"return_reason_en": "Wrong address",
"return_reason_ku": "ناونیشان هەڵەیە"
}
# Delivered
{
"shipment_id": 12345,
"action": "SUCCESSFUL_DELIVERY"
}
These requirements apply to POST /v2/shipments/update-status endpoint (Receive In Mode)
| Action | Required Fields | Optional Fields | Example |
|---|---|---|---|
POSTPONED |
postponed_reasonpostponed_date_id
|
note |
postponed_date_id: 1 (غداً) |
RETURN_TO_STORERETURNED_WITH_AGENT |
return_reason |
return_quantitynote
|
return_quantity for partial returns |
TREATED |
treated_message |
note |
Min 3 characters |
SUCCESSFUL_DELIVERY |
- |
image_urlnote
|
No extra fields needed |
SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE |
new_amount_iqd ORnew_amount_usd
|
image_urlnote
|
At least one amount required |
PARTIAL_DELIVERY |
- |
return_quantitynew_amount_iqdnote
|
Partial delivery/exchange |
| Code | English | Arabic |
|---|---|---|
BGD |
Baghdad | بغداد |
BAS |
Basra | البصرة |
NIN |
Nineveh | نينوى |
ARB |
Erbil | أربيل |
NJF |
Najaf | النجف |
| Code | English | Arabic |
|---|---|---|
KRK |
Kirkuk | كركوك |
ANA |
Anbar | الأنبار |
KAR |
Karbala | كربلاء |
DHI |
Dhi Qar | ذي قار |
MAY |
Maysan | ميسان |
Get complete list: View All Governorates
07XXXXXXXXXshipment_number/reference/cities to validate city names/available-actions before updating statusGet complete lists in Excel or JSON format:
To keep API responses concise and efficient, we use standardized abbreviations for common terms. Here's your complete reference guide.
| Term | Abbreviation |
|---|---|
| Pickup Agent | PA |
| Delivery Agent | DA |
| Midmile Agent | MA |
| Term | Abbreviation |
|---|---|
| Warehouse | WH |
| Sorting Center | SC |
| Return To Origin / Returned Orders | RTO |
| Out For Delivery | OFD |
All step codes now use shortened, standardized abbreviations for better readability:
| Step Code | Arabic Name | Description |
|---|---|---|
IN_SC |
داخل مركز الفرز | Shipment received in sorting center |
PRINT_MANIFEST_DA |
طباعة المنفيست لمندوبين التوصيل | Delivery manifest printed for courier |
NEW_WITH_PA |
شحنات جديدة مع مندوب الأستلام | New shipment with pickup agent |
DELIVERY_REATTEMPT |
إعادة محاولة التوصيل | Rescheduled for delivery retry |
RTO_CONFIRMED |
راجع مؤكد | Return confirmed by merchant |
POSTPONED_CONFIRMED |
مؤجل مؤكد | Postponement confirmed by merchant |
PENDING_DELIVERY_APPROVAL |
بانتظار موافقة التسليم | Awaiting delivery confirmation from recipient |
REJECTED_PRICE_CHANGE |
مرفوض تغيير السعر | Price change request refused |
OFD |
قيد التوصيل | Out for delivery with courier |
RTO_WITH_DA |
راجع عند المندوب | Returned shipment with delivery agent |
POSTPONED |
مؤجل | Delivery postponed by recipient request |
RTO_WH |
راجع كلي في المخزن | Shipment has been fully returned to warehouse |
RTO_ARCHIVED |
أرشيف شحنات راجعة | Returned shipment archived |
PARTIALLY_DELIVERED |
تسليم جزئيا أو أستبدال | Shipment partially delivered or item replaced |
DELIVERED_PRICE_CHANGED |
سلمت مع تغيير المبلغ | Delivered with modified amount |
FORCE_DELIVERY |
واصل أجباري | Mandatory delivery required |
DELIVERED |
سلمت بنجاح | Delivered successfully to recipient |
DELIVERED_ARCHIVED |
أرشيف المسلم بنجاح | Successfully delivered shipment archived |
RTO_FROM_BRANCH |
استلام الراجع من الفروع | Returned shipment being collected from branch |
RTO_IN_TRANSIT_WH |
رواجع الفروع في المخزن | Returned shipment in warehouse awaiting branch pickup |
BRANCH_PRINT_MANIFEST |
طباعة منفيست للفروع | Inter-branch manifest printed |
RTO_READY_FOR_BRANCH |
رواجع جاهزة للتسليم للفروع | Returned shipments ready for branch transfer |
WITH_MA |
شحنات في الطريق للفروع | Shipment with midmile agent |
NEW_IN_TRANSIT |
شحنات جديدة بين فرعين | Shipment in transit between branches |
NEW_ORDER_TO_PRINT |
جاهز للطبع | Ready for sticker printing |
NEW_ORDER_TO_PICKUP |
جاهز للنقل | Shipment ready for courier pickup |
All return-related steps start with RTO_
RTO_CONFIRMEDRTO_WITH_DARTO_WHRTO_ARCHIVEDRTO_FROM_BRANCHRTO_IN_TRANSIT_WHRTO_READY_FOR_BRANCHDelivery-related steps are clear:
OFD - Out For DeliveryDELIVERED - SuccessDELIVERED_PRICE_CHANGEDDELIVERED_ARCHIVEDPARTIALLY_DELIVEREDFORCE_DELIVERYNew orders and processing:
NEW_WITH_PA - With Pickup AgentIN_SC - In Sorting CenterNEW_ORDER_TO_PRINTNEW_ORDER_TO_PICKUPNEW_IN_TRANSIT - Between BranchesWITH_MA - With Midmile Agentstep_name (abbreviated like OFD, IN_SC) and step_name_ar (Arabic) for displayThe API automatically handles both old and new names for backward compatibility:
OUT_FOR_DELIVERY → OFDIN_SORTING_CENTER → IN_SCRETURNED_WITH_AGENT → RTO_WITH_DADELIVERED_SUCCESSFULLY → DELIVERED
Update your systems to use the new abbreviated names for:
• Shorter API responses (less bandwidth)
• Faster parsing
• Industry-standard naming
• Better readability in logs
You CANNOT use this API without registering with Jenni Logistics technical team first.
Before you continue reading this documentation, you need to contact Jenni Logistics technical team to:
system_code (your company identifier)Your current API base URL is:
https://jenni.alzaeemexp.com/apiAll API endpoints should use this base URL
The V2 Integration API is a modern, RESTful API designed for seamless integration with external delivery management systems. It provides standardized endpoints with clean property naming conventions and global standardization.
All V2 features work with ANY V2-compatible system - whether it's Jenni Logistics-to-Jenni Logistics, Jenni Logistics-to-External, or External-to-Jenni Logistics integration. Features like retry mechanism, bidirectional tracking, sender information, and smart lookup are available for ALL V2 integrations, not limited to specific systems.
YOUR_DOMAIN/v2/push/update-status including agent GPS locationtracking_number, sender_merchant_id)IN_SC, DELIVERED)merchant_settlement instead of customer_payment)
IMPORTANT: About system_code
❌ DO NOT ask: "What is my system_code?" or "Can you give me a system_code?"
✅ YOU must provide: Your own system_code when registering. This is YOUR unique identifier that YOU choose.
How it works:
system_code (e.g., YOUR_COMPANY_NAME)📖 Please read the section below about "What is system_code?" before proceeding.
You MUST register with Jenni Logistics technical team before you can start integration. This guide explains what information you need to provide and how the registration process works.
system_code?
system_code is your unique company identifier in Jenni Logistics system.
It's like your "username" for API access and is used in every API request to identify your company.
ECOMMERCE_STORE_01 - E-commerce websiteABC_DELIVERY_CO - Delivery companyPHARMACY_CHAIN_IRAQ - Pharmacy chainMARKETPLACE_APP - Mobile marketplace⚠️ CRITICAL: Who Provides system_code?
YOU provide the system_code, NOT Jenni Logistics!
YOUR_COMPANY_NAME)Security Note:
Your system_code is NOT a secret. It's just an identifier (like a username) that identifies your company in the system.
Security comes from your authentication credentials: username and password that you use to login and receive access tokens.
These credentials are what protect your API access, not the system_code itself.
Choose which integration mode suits your business:
You send shipments → Jenni Logistics delivers
Jenni Logistics sends shipments → You deliver
Username and password authentication with dynamic tokens
Gather the following information before contacting Jenni Logistics technical team:
| Information | Required? | Description & Example |
|---|---|---|
| Company Name | Required |
Your official company name
Example: ABC Electronics Store
|
| system_code | Required |
Your proposed unique identifier (uppercase, underscores only)
Example:
ABC_ELECTRONICS_STORE
|
| Integration Type | Required |
Receive In (you send to Jenni Logistics) OR Push Out (Jenni Logistics sends to you)
Example: Receive In
|
| Your System Authentication (for Webhook Updates) | Conditional |
REQUIRED if:
You MUST provide:
When you provide your Domain/URL for webhook updates, you MUST also provide authentication credentials that Jenni Logistics will use to authenticate with YOUR API endpoint when sending status updates. Choose one:
Example for TOKEN:
YOUR_STATIC_TOKEN
Example for LOGIN: Username: jenni_api_user, Password: your_secure_password
|
| Your Domain/URL | Conditional |
REQUIRED for:
⚠️ IMPORTANT:
If you want automatic webhook status updates sent to your system, you MUST provide:
If you do NOT provide these, automatic webhook updates will NOT be sent to your system. You will need to manually query shipment status using POST /v2/shipments/query instead.
How It Works:
Jenni Logistics will use the Domain/URL and authentication credentials you provide to send HTTP POST requests to your endpoint /v2/push/update-status whenever shipment status changes.
This is different from the credentials Jenni Logistics provides you for accessing Jenni Logistics API.
Example:
https://yourstore.com or https://api.yourstore.com
Jenni Logistics will automatically append endpoints:
• Push Out: /v2/shipments/create
• Receive In Webhook: /v2/push/update-status
|
| Contact Person | Required |
Technical contact name, email, and phone
Example: Ahmed Ali, ahmed@yourstore.com, +964 790 123 4567
|
| Expected Volume | Optional |
Estimated daily/monthly shipment count
Example: 200 shipments/day, 6,000 shipments/month
|
Send your registration request with all required information to:
Copy and fill this template for your registration request:
Subject: V2 API Integration Registration Request - [Your Company Name]
Dear Jenni Logistics Technical Team,
We would like to register for V2 API integration.
COMPANY INFORMATION:
- Company Name: [Your Company Name]
- Integration API Domain: https://jenni.alzaeemexp.com/api
- system_code: [YOUR_SYSTEM_CODE]
- Business Type: [E-commerce / Delivery Service / Marketplace / etc.]
INTEGRATION DETAILS:
- Integration Type: [Receive In / Push Out]
- Expected Daily Volume: [Number] shipments per day
WEBHOOK (Optional - only if you want automatic status updates):
- Domain/URL: [https://yourstore.com]
- Authentication Method: [TOKEN / LOGIN]
- If TOKEN: Token: [YOUR_STATIC_TOKEN]
- If LOGIN: Username: [YOUR_API_USERNAME], Password: [YOUR_API_PASSWORD]
CONTACT:
- Name: [Your Name]
- Email: [your.email@company.com]
- Phone: [+964 XXX XXX XXXX]
Thank you,
[Your Name]
Within 1-3 business days, Jenni Logistics technical team will confirm your registration and provide you with:
system_code - Your confirmed unique identifier (that YOU provided)username - Your login username (you should already have this from Jenni Logistics)password - Your password (you should already have this from Jenni Logistics)Note: Your username and password credentials are provided by Jenni Logistics before registration. You will use these credentials to login and receive access tokens for API calls.
Start with testing in the following order:
Ready to Start?
Once testing is successful, you can gradually increase volume and move to production!
Usually 1-3 business days. Urgent requests can be processed within 24 hours - mention urgency in your email.
Contact the sales team for pricing information. Technical integration setup is typically included in service packages.
Not recommended after going live as it requires reconfiguration. Choose wisely! Test systems can be changed easily.
Yes! Large companies often have separate codes for different divisions (e.g., COMPANY_WEBSITE, COMPANY_APP, COMPANY_WHOLESALE).
Only if:
POST /v2/shipments/query instead.
Absolutely! Request test credentials first. Test with small volumes, then request production credentials when ready.
Before starting, you must choose the right integration type for your needs:
Choose this if:
Choose this if:
YOUR_DOMAIN/v2/push/update-statusNot Sure? See the Integration Modes section for detailed comparison and real-world examples.
V2 Integration API supports two distinct integration patterns based on which system initiates the shipment creation. Choose the appropriate mode based on your business workflow.
Jenni → Your System
/v2/shipments/create on YOUR server/v2/push/update-status on JenniYour System → Jenni
/v2/auth/login on Jenni/v2/shipments/create on Jenni/v2/shipments/queryPOST /v2/shipments/create
On YOUR server
POST /v2/push/update-status
On Jenni Logistics server
/v2/shipments/create
On YOUR server - Receive shipments from Jenni Logistics
is_dlv_agent. Real-time push, no polling needed!
/v2/push/update-status
On Jenni Logistics server - Send status updates to Jenni Logistics
postponed_reason + postponed_date_idreturn_reasonnoteis_supports_v2 |
TRUE |
is_v2_push_url |
Your endpoint URL Example: https://your-system.com/v2/shipments/create |
is_auth_method |
TOKEN or LOGIN |
is_dlv_agent |
Delivery agent ID in Jenni |
External delivery company receives orders from Jenni Logistics
Branch or collection center receives shipments from main system
Logistics partner receives specific shipments
POST /v2/shipments/create
On Jenni Logistics server
POST /v2/push/update-status
On YOUR server (automatic)
POST /v2/shipments/query
Check status anytime
Automatic Status Updates from Jenni Logistics:
YOUR_DOMAIN/v2/push/update-status (on YOUR server)POST /v2/shipments/query for manual checks anytimePOST /v2/push/update-status on YOUR server to receive these updatesRequired Configuration:
In Jenni Logistics system settings, configure:
https://yourstore.com
Jenni Logistics will send updates to: YOUR_DOMAIN/v2/push/update-status
/v2/push/update-status
Receive automatic real-time status updates from Jenni Logistics (includes agent GPS location)
See Scenario 8 in Real-World Scenarios for complete implementation example
/v2/shipments/create
Create new shipments in Jenni
/v2/shipments/query
Query shipment status from Jenni Logistics
/v2/shipments/update-status
Update shipment status (if needed)
/v2/shipments/edit
Edit shipment information
system_code |
Your unique code in Jenni Example: ECOMMERCE_STORE_01 |
authentication |
JWT Token from /v2/auth/login |
webhook_url |
(Optional) Your domain Jenni Logistics sends updates to: YOUR_DOMAIN/v2/push/update-status |
E-commerce website sends its orders to Jenni Logistics
Mobile application or online marketplace
Point of sale system for merchants
Warehouse management system
| Feature |
Mode 1: Push Out Jenni → Your System |
Mode 2: Receive In Your System → Jenni |
|---|---|---|
| Who creates shipments? | Jenni creates | You create |
| Who delivers? | You deliver | Jenni delivers |
| Your role | Delivery service provider | Merchant / Customer |
| Server implementation needed? | YES - Receive endpoint | NO - Just call APIs |
| Authentication flow | Jenni authenticates to you | You authenticate to Jenni Logistics |
| Configuration in Jenni |
is_supports_v2 = TRUEis_v2_push_url = YOUR_URLis_dlv_agent = AGENT_ID
|
system_code = YOUR_CODEtokenToreceiveCases = YOUR_TOKENis_rcv_agent = AGENT_ID
|
| Main APIs you use |
Implement /receiveCall /update-status
|
Call /createCall /query
|
| Status updates flow | You → Jenni | Jenni → You (webhook or polling) |
| Technical complexity | Medium - Need server endpoint | Low - Just REST API calls |
A webhook is an automatic notification system where Jenni Logistics sends real-time status updates directly to YOUR server whenever a shipment status changes. This eliminates the need for continuous polling (repeated API calls to check status).
❌ Your server asks Jenni Logistics every 5 minutes: "Any updates?"
Wastes resources, delays, not real-time
✅ Jenni Logistics tells YOUR server instantly: "Status changed!"
Real-time, efficient, no delays
You can always use POST /v2/shipments/query instead
You call: POST /v2/shipments/create
Agent picks up, delivers, or updates status
POST to: YOUR_DOMAIN/v2/push/update-status
Update database, send customer SMS/Email
Real-time status on your website/app
POST YOUR_DOMAIN/v2/push/update-status
This endpoint receives automatic notifications from Jenni Logistics
Example:
https://yourstore.com
https://api.yourstore.com
Jenni Logistics will automatically send updates to: YOUR_DOMAIN/v2/push/update-status
When a shipment status changes, Jenni Logistics will send a POST request to your endpoint with this JSON body:
Both shipment_number and shipment_id are ALWAYS included in every webhook update.
You can use either field to identify and match the shipment in your database:
shipment_number - Use this if you match orders by tracking number/order numbershipment_id - Use this if you store Jenni's shipment ID (more reliable, always unique){
"system_code": "YOUR_SYSTEM_CODE",
"updates": [
{
"shipment_number": "ORD-2024-001",
"shipment_id": 98765,
"external_id": "MYSTORE-ORDER-12345",
"action_code": "SUCCESSFUL_DELIVERY",
"current_step": "DELIVERED",
"current_step_ar": "سلمت بنجاح",
"current_stage": "DELIVERED",
"current_stage_ar": "الواصل",
"governorate_code": "BGD",
"governorate_name": "BAGHDAD",
"note": "Delivered successfully to customer",
"agent_latitude": 33.3152,
"agent_longitude": 44.3661,
"amount_iqd": 50000,
"amount_usd": 0,
"quantity_delivered": 2,
"quantity_returned": 0,
"postponed_reason": null,
"postponed_reason_en": null,
"postponed_reason_ku": null,
"postponed_date_id": null,
"return_reason": null,
"return_reason_en": null,
"return_reason_ku": null
}
]
}
Both shipment_number and shipment_id are ALWAYS available in every webhook request.
You can use either one to identify and match the shipment in your database:
shipment_number: The tracking number you originally sent to Jenni Logistics (recommended for matching with your order database)shipment_id: Jenni's internal unique shipment ID (always unique, more reliable if you store it)external_id: Only available in bidirectional integrations (Jenni-to-Jenni) - your original order ID| Field | Type | Description |
|---|---|---|
system_code |
String | Your unique identifier - verify this matches YOUR code for security |
shipment_number |
String | ALWAYS available: The tracking number you sent to Jenni Logistics. Use this to find order in YOUR database by order number/tracking code. |
shipment_id |
Integer (Long) | ALWAYS available: Jenni's internal unique shipment ID. More reliable than shipment_number (always unique). Use this if you store Jenni's shipment_id in your database. |
external_id |
String | Only in bidirectional integrations: Your original order ID (if you provided external_shipment_id during creation). Available only when shipment came from another Jenni system. |
action_code |
String | What happened: SUCCESSFUL_DELIVERY, POSTPONED, RETURNED_WITH_AGENT, etc. |
current_step |
String | Current status in English (e.g., "DELIVERED", "OFD", "POSTPONED") |
current_step_ar |
String | Current status in Arabic (e.g., "سلمت بنجاح", "قيد التوصيل", "مؤجل") |
current_stage |
String | Delivery stage: SORTING_CENTER, WITH_AGENT, DELIVERED, RTO |
governorate_code |
String | Governorate code (BGD, BAS, NIN, etc.) |
note |
String | Additional note from agent or system (may be null) |
agent_latitude |
Double | Agent's GPS latitude when action was taken (may be null) |
agent_longitude |
Double | Agent's GPS longitude when action was taken (may be null) |
amount_iqd |
Double | Shipment amount in IQD (may change if customer negotiates) |
quantity_delivered |
Integer | Number of items delivered (for partial deliveries) |
quantity_returned |
Integer | Number of items returned (for partial returns) |
postponed_reason |
String | Only present for POSTPONED action: Reason for postponement (e.g., "العميل غير موجود") |
postponed_reason_en |
String | Only present for POSTPONED action: English description (e.g., "Customer not available") |
postponed_reason_ku |
String | Only present for POSTPONED action: Kurdish description (e.g., "کڕیار بەردەست نییە") |
postponed_date_id |
Integer | Only present for POSTPONED action: 0=no date, 1=today/tomorrow, 2=in 2 days, 3=in 3+ days |
return_reason |
String | Only present for RETURN actions: Reason for return (e.g., "رفض الاستلام") |
return_reason_en |
String | Only present for RETURN actions: English description (e.g., "Customer refused") |
return_reason_ku |
String | Only present for RETURN actions: Kurdish description (e.g., "کڕیار ڕەتکردەوە") |
// server.js - Express.js Example
const express = require('express');
const app = express();
app.use(express.json());
// This endpoint receives automatic status updates from Jenni Logistics
app.post('/v2/push/update-status', async (req, res) => {
try {
console.log('📬 Received webhook from Jenni Logistics:', req.body);
const { system_code, updates } = req.body;
// 1. Verify system_code for security
if (system_code !== 'YOUR_SYSTEM_CODE') {
console.error('❌ Invalid system_code:', system_code);
return res.status(401).json({
success: false,
message: 'Invalid system code'
});
}
// 2. Process each update
for (const update of updates) {
// Both shipment_number and shipment_id are ALWAYS available
// Use shipment_number to match by your order number, or shipment_id for more reliable unique matching
const orderIdentifier = update.shipment_number; // or use: update.shipment_id
const jenniShipmentId = update.shipment_id; // Always available - Jenni's unique ID
console.log(`\n🔄 Processing update for order: ${orderIdentifier} (Jenni ID: ${jenniShipmentId})`);
console.log(` Action: ${update.action_code}`);
console.log(` Status: ${update.current_step} (${update.current_step_ar})`);
// Optional: Log GPS location
if (update.agent_latitude && update.agent_longitude) {
console.log(` 📍 Agent Location: ${update.agent_latitude}, ${update.agent_longitude}`);
}
// 3. Update YOUR database
// You can use either shipment_number or shipment_id to find the order
await updateOrderStatus({
orderNumber: update.shipment_number, // Use this if you match by order number
jenniShipmentId: update.shipment_id, // Use this if you store Jenni's shipment_id
status: update.current_step,
statusArabic: update.current_step_ar,
action: update.action_code,
note: update.note,
agentLat: update.agent_latitude,
agentLng: update.agent_longitude,
amountIQD: update.amount_iqd,
updatedAt: new Date()
});
// 4. Send customer notification based on action
if (update.action_code === 'SUCCESSFUL_DELIVERY') {
await sendCustomerSMS(
update.shipment_number,
'Your order has been delivered successfully! 🎉'
);
} else if (update.action_code === 'POSTPONED') {
const reason = update.postponed_reason_en || update.postponed_reason || 'Unknown reason';
await sendCustomerSMS(
update.shipment_number,
`Delivery postponed: ${reason}. Will contact you soon.`
);
} else if (update.action_code === 'RETURNED_WITH_AGENT') {
const reason = update.return_reason_en || update.return_reason || 'Unknown reason';
await sendCustomerSMS(
update.shipment_number,
`Order returned: ${reason}`
);
}
console.log(` ✅ Order ${update.shipment_number} updated successfully`);
}
// 5. IMPORTANT: Respond with success (Jenni expects this)
res.json({
success: true,
message: `Successfully processed ${updates.length} update(s)`,
received_count: updates.length
});
} catch (error) {
console.error('❌ Error processing webhook:', error);
res.status(500).json({
success: false,
message: error.message
});
}
});
// Helper function: Update database
async function updateOrderStatus(data) {
// Your database logic here (MySQL, MongoDB, etc.)
console.log('💾 Updating database:', data);
// Example with MySQL
// await db.query(
// 'UPDATE orders SET status = ?, status_ar = ?, note = ?, updated_at = NOW() WHERE order_number = ?',
// [data.status, data.statusArabic, data.note, data.orderNumber]
// );
}
// Helper function: Send SMS notification
async function sendCustomerSMS(orderNumber, message) {
// Your SMS service logic (Twilio, local SMS gateway, etc.)
console.log('📱 Sending SMS for order:', orderNumber, '-', message);
}
app.listen(3000, () => {
console.log('✅ Server listening on port 3000');
console.log('📬 Webhook endpoint ready: POST /v2/push/update-status');
});
// app/Http/Controllers/WebhookController.php - Laravel Example
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Order;
use App\Services\SMSService;
use Illuminate\Support\Facades\Log;
class WebhookController extends Controller
{
public function handleJenniWebhook(Request $request)
{
try {
Log::info('📬 Received webhook from Jenni Logistics', $request->all());
$systemCode = $request->input('system_code');
$updates = $request->input('updates', []);
// 1. Verify system_code for security
if ($systemCode !== 'YOUR_SYSTEM_CODE') {
Log::error('❌ Invalid system_code: ' . $systemCode);
return response()->json([
'success' => false,
'message' => 'Invalid system code'
], 401);
}
// 2. Process each update
foreach ($updates as $update) {
// Both shipment_number and shipment_id are ALWAYS available
// Use shipment_number to match by your order number, or shipment_id for more reliable unique matching
$orderNumber = $update['shipment_number']; // Your tracking number
$jenniShipmentId = $update['shipment_id']; // Always available - Jenni's unique ID
$action = $update['action_code'];
$status = $update['current_step'];
$statusAr = $update['current_step_ar'];
Log::info("🔄 Processing update for order: {$orderNumber} (Jenni ID: {$jenniShipmentId})");
Log::info(" Action: {$action}");
Log::info(" Status: {$status} ({$statusAr})");
// 3. Update database
// You can use either shipment_number or shipment_id to find the order
$order = Order::where('order_number', $orderNumber)->first();
// Alternative: $order = Order::where('jenni_shipment_id', $jenniShipmentId)->first();
if ($order) {
$order->update([
'jenni_status' => $status,
'jenni_status_ar' => $statusAr,
'jenni_action' => $action,
'jenni_note' => $update['note'] ?? null,
'agent_latitude' => $update['agent_latitude'] ?? null,
'agent_longitude' => $update['agent_longitude'] ?? null,
'amount_iqd' => $update['amount_iqd'] ?? $order->amount_iqd,
'updated_at' => now()
]);
// 4. Send customer notification
$this->sendCustomerNotification($order, $action, $update);
Log::info(" ✅ Order {$orderNumber} updated successfully");
} else {
Log::warning(" ⚠️ Order {$orderNumber} not found in database");
}
}
// 5. Return success response
return response()->json([
'success' => true,
'message' => 'Successfully processed ' . count($updates) . ' update(s)',
'received_count' => count($updates)
]);
} catch (\Exception $e) {
Log::error('❌ Error processing webhook: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
private function sendCustomerNotification($order, $action, $update)
{
$smsService = new SMSService();
switch ($action) {
case 'SUCCESSFUL_DELIVERY':
$smsService->send(
$order->customer_phone,
'Your order has been delivered successfully! 🎉'
);
break;
case 'POSTPONED':
$reason = $update['postponed_reason_en'] ?? $update['postponed_reason'] ?? 'Unknown reason';
$smsService->send(
$order->customer_phone,
"Delivery postponed: {$reason}. Will contact you soon."
);
break;
case 'RETURNED_WITH_AGENT':
$reason = $update['return_reason_en'] ?? $update['return_reason'] ?? 'Unknown reason';
$smsService->send(
$order->customer_phone,
"Order returned: {$reason}"
);
break;
}
}
}
// routes/web.php or routes/api.php
Route::post('/v2/push/update-status', [WebhookController::class, 'handleJenniWebhook']);
# app.py - Flask Example
from flask import Flask, request, jsonify
import logging
from datetime import datetime
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route('/v2/push/update-status', methods=['POST'])
def handle_jenni_webhook():
try:
data = request.get_json()
logging.info(f'📬 Received webhook from Jenni Logistics: {data}')
system_code = data.get('system_code')
updates = data.get('updates', [])
# 1. Verify system_code for security
if system_code != 'YOUR_SYSTEM_CODE':
logging.error(f'❌ Invalid system_code: {system_code}')
return jsonify({
'success': False,
'message': 'Invalid system code'
}), 401
# 2. Process each update
for update in updates:
# Both shipment_number and shipment_id are ALWAYS available
# Use shipment_number to match by your order number, or shipment_id for more reliable unique matching
order_number = update.get('shipment_number') # Your tracking number
jenni_shipment_id = update.get('shipment_id') # Always available - Jenni's unique ID
action = update.get('action_code')
status = update.get('current_step')
status_ar = update.get('current_step_ar')
logging.info(f'\n🔄 Processing update for order: {order_number} (Jenni ID: {jenni_shipment_id})')
logging.info(f' Action: {action}')
logging.info(f' Status: {status} ({status_ar})')
# Optional: Log GPS location
if update.get('agent_latitude') and update.get('agent_longitude'):
logging.info(f' 📍 Agent Location: {update["agent_latitude"]}, {update["agent_longitude"]}')
# 3. Update database
# You can use either shipment_number or shipment_id to find the order
update_order_status({
'order_number': order_number, # Use this if you match by order number
'jenni_shipment_id': jenni_shipment_id, # Use this if you store Jenni's shipment_id
'status': status,
'status_arabic': status_ar,
'action': action,
'note': update.get('note'),
'agent_lat': update.get('agent_latitude'),
'agent_lng': update.get('agent_longitude'),
'amount_iqd': update.get('amount_iqd'),
'updated_at': datetime.now()
})
# 4. Send customer notification
if action == 'SUCCESSFUL_DELIVERY':
send_customer_sms(
order_number,
'Your order has been delivered successfully! 🎉'
)
elif action == 'POSTPONED':
reason = update.get('postponed_reason_en') or update.get('postponed_reason') or 'Unknown reason'
send_customer_sms(
order_number,
f'Delivery postponed: {reason}. Will contact you soon.'
)
elif action == 'RETURNED_WITH_AGENT':
reason = update.get('return_reason_en') or update.get('return_reason') or 'Unknown reason'
send_customer_sms(
order_number,
f'Order returned: {reason}'
)
logging.info(f' ✅ Order {order_number} updated successfully')
# 5. Return success response
return jsonify({
'success': True,
'message': f'Successfully processed {len(updates)} update(s)',
'received_count': len(updates)
})
except Exception as e:
logging.error(f'❌ Error processing webhook: {str(e)}')
return jsonify({
'success': False,
'message': str(e)
}), 500
def update_order_status(data):
"""Update order in database"""
logging.info(f'💾 Updating database: {data}')
# Your database logic here (SQLAlchemy, pymongo, etc.)
# Example with SQLAlchemy:
# order = Order.query.filter_by(order_number=data['order_number']).first()
# if order:
# order.status = data['status']
# order.status_ar = data['status_arabic']
# order.note = data['note']
# order.updated_at = data['updated_at']
# db.session.commit()
def send_customer_sms(order_number, message):
"""Send SMS to customer"""
logging.info(f'📱 Sending SMS for order: {order_number} - {message}')
# Your SMS service logic here
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3000)
logging.info('✅ Server listening on port 3000')
logging.info('📬 Webhook endpoint ready: POST /v2/push/update-status')
// WebhookController.java - Spring Boot Example
package com.yourcompany.controller;
import com.yourcompany.service.OrderService;
import com.yourcompany.service.SMSService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/v2")
@RequiredArgsConstructor
@Slf4j
public class WebhookController {
private final OrderService orderService;
private final SMSService smsService;
@PostMapping("/push/update-status")
public ResponseEntity
Here's what the webhook body looks like for different delivery scenarios:
{
"system_code": "YOUR_SYSTEM_CODE",
"updates": [{
"shipment_number": "ORD-2024-001",
"action_code": "SUCCESSFUL_DELIVERY",
"current_step": "DELIVERED",
"current_step_ar": "سلمت بنجاح",
"current_stage": "DELIVERED",
"agent_latitude": 33.3152,
"agent_longitude": 44.3661,
"amount_iqd": 50000,
"note": "Delivered to customer successfully"
}]
}
{
"system_code": "YOUR_SYSTEM_CODE",
"updates": [{
"shipment_number": "ORD-2024-001",
"action_code": "POSTPONED",
"current_step": "POSTPONED",
"current_step_ar": "مؤجل",
"postponed_reason": "العميل غير موجود",
"postponed_reason_en": "Customer not available",
"postponed_reason_ku": "کڕیار بەردەست نییە",
"postponed_date_id": 1,
"agent_latitude": 33.3152,
"agent_longitude": 44.3661,
"note": "Customer not available - will retry tomorrow"
}]
}
Note: Values 1-3 are calculated based on the postponed date compared to today's date in Baghdad timezone.
{
"system_code": "YOUR_SYSTEM_CODE",
"updates": [{
"shipment_number": "ORD-2024-001",
"action_code": "RETURNED_WITH_AGENT",
"current_step": "RTN_WITHAGENT",
"current_step_ar": "راجع عند المندوب",
"current_stage": "RTO",
"return_reason": "رفض الاستلام",
"return_reason_en": "Customer refused",
"return_reason_ku": "کڕیار ڕەتکردەوە",
"agent_latitude": 33.3152,
"agent_longitude": 44.3661,
"note": "Customer refused to accept"
}]
}
{
"system_code": "YOUR_SYSTEM_CODE",
"updates": [{
"shipment_number": "ORD-2024-001",
"action_code": "PARTIAL_DELIVERY",
"current_step": "PARTIAL_DELIVERY",
"current_step_ar": "تسليم جزئي",
"quantity_delivered": 2,
"quantity_returned": 1,
"amount_iqd": 30000,
"note": "Delivered 2 items, 1 item returned"
}]
}
{
"system_code": "YOUR_SYSTEM_CODE",
"updates": [{
"shipment_number": "ORD-2024-001",
"action_code": "SUCCESSFUL_DELIVERY",
"current_step": "DELIVERED_PRICE_CHANGED",
"current_step_ar": "سلمت مع تغيير السعر",
"amount_iqd": 40000,
"note": "Customer negotiated - accepted 40,000 IQD instead of 50,000"
}]
}
You can test your webhook endpoint using cURL before going live:
curl -X POST "https://yourstore.com/v2/push/update-status" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"system_code": "YOUR_SYSTEM_CODE",
"updates": [{
"shipment_number": "TEST-001",
"action_code": "SUCCESSFUL_DELIVERY",
"current_step": "DELIVERED",
"current_step_ar": "سلمت بنجاح",
"current_stage": "DELIVERED",
"agent_latitude": 33.3152,
"agent_longitude": 44.3661,
"amount_iqd": 50000,
"note": "Test delivery"
}]
}'
{
"success": true,
"message": "Successfully processed 1 update(s)",
"received_count": 1
}
Possible Causes:
/v2/push/update-status)Solution: Contact Jenni Logistics technical team to verify configuration and check server logs
Possible Causes:
Solution: Add comprehensive error logging and handle null values safely
Possible Causes:
Solution: Implement idempotency - check if update already processed before saving to database
Best Practices:
system_code{ "success": true } IMMEDIATELYsystem_code for security?? null or defaults)This section provides complete, working examples for both integration modes. Each example includes authentication, error handling, and real-world scenarios.
Your Role: Delivery company that receives orders from Jenni Logistics and delivers them
What You Need to Do:
/v2/shipments/create endpoint on YOUR server to receive shipments from Jenni LogisticsPOST /v2/push/update-status// server.js - Express.js Example
const express = require('express');
const app = express();
app.use(express.json());
// ==========================================
// STEP 1: Implement endpoint to receive from Jenni Logistics
// ==========================================
app.post('/v2/shipments/create', async (req, res) => {
console.log('📥 Received shipments from Jenni Logistics');
try {
// 1. Validate authentication
const authToken = req.headers.authorization?.replace('Bearer ', '');
if (!authToken || !isValidJenniToken(authToken)) {
return res.status(401).json({
success: false,
message: 'Invalid authentication token'
});
}
// 2. Extract shipments from request
const { system_code, shipments } = req.body;
console.log(`System: ${system_code}, Shipments: ${shipments.length}`);
// 3. Process and save shipments to YOUR database
const acceptedShipments = [];
const rejectedShipments = [];
for (const shipment of shipments) {
try {
// Check if shipment already exists in your database
if (await shipmentExists(shipment.shipment_id)) {
rejectedShipments.push({
shipment_number: shipment.shipment_number,
shipment_id: shipment.shipment_id,
airway_bill_number: shipment.airway_bill_number || null,
reason: "Shipment already exists in your system",
error_code: "DUPLICATE_SHIPMENT",
field: "shipment_number"
});
continue;
}
// Save shipment to your database
const savedShipment = await saveToDatabase(shipment);
// Assign shipment to your delivery driver
await assignToYourDriver(savedShipment);
acceptedShipments.push({
shipment_number: shipment.shipment_number,
shipment_id: shipment.shipment_id,
external_id: savedShipment.id.toString(),
airway_bill_number: shipment.airway_bill_number || null
});
} catch (error) {
rejectedShipments.push({
shipment_number: shipment.shipment_number,
shipment_id: shipment.shipment_id || null,
airway_bill_number: shipment.airway_bill_number || null,
reason: error.message,
error_code: "PROCESSING_ERROR",
field: null
});
}
}
// 4. Return response to Jenni Logistics (REQUIRED FORMAT)
res.json({
success: acceptedShipments.length > 0,
message: acceptedShipments.length > 0
? `${acceptedShipments.length} shipment(s) processed successfully`
: "All shipments failed to process",
timestamp: new Date().toISOString(),
accepted_shipments: acceptedShipments,
rejected_shipments: rejectedShipments,
summary: {
total_requested: shipments.length,
accepted_count: acceptedShipments.length,
rejected_count: rejectedShipments.length
}
});
} catch (error) {
console.error('❌ Error receiving shipments:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
timestamp: new Date().toISOString()
});
}
});
// Helper functions
function isValidJenniToken(token) {
// Validate token from Jenni Logistics
// Check against your configured token or use JWT validation
return token === process.env.JENNI_AUTH_TOKEN;
}
async function saveToDatabase(shipment) {
// Save shipment to your database
const saved = await db.shipments.create({
shipment_number: shipment.shipment_number,
receiver_name: shipment.receiver_name,
receiver_phone: shipment.receiver_phone_1,
address: shipment.address,
amount: shipment.amount_iqd,
status: 'PENDING',
created_at: new Date()
});
return saved;
}
async function assignToYourDriver(shipment) {
// Your logic to assign shipment to your delivery driver
const driver = await findAvailableDriver(shipment.governorate);
await db.assignments.create({
shipment_id: shipment.id,
driver_id: driver.id,
assigned_at: new Date()
});
}
app.listen(3000, () => {
console.log('✅ Server running on port 3000');
console.log('📡 Waiting for shipments from Jenni Logistics...');
});
// statusUpdater.js - Send updates to Jenni Logistics server
const axios = require('axios');
const JENNI_API_URL = 'https://jenni-api.com/api';
const JENNI_USERNAME = 'your_username';
const JENNI_PASSWORD = 'your_password';
let authToken = null;
let tokenExpiry = null;
// ==========================================
// STEP 2: Login to Jenni Logistics and get token
// ==========================================
async function loginToJenni() {
try {
const response = await axios.post(`${JENNI_API_URL}/v2/auth/login`, {
username: JENNI_USERNAME,
password: JENNI_PASSWORD
});
authToken = response.data.token;
tokenExpiry = Date.now() + (response.data.expires_in * 1000);
console.log('✅ Logged in to Jenni Logistics successfully');
return authToken;
} catch (error) {
console.error('❌ Login failed:', error.response?.data || error.message);
throw error;
}
}
// Ensure valid token
async function ensureValidToken() {
if (!authToken || Date.now() > (tokenExpiry - 5 * 60 * 1000)) {
await loginToJenni();
}
}
// ==========================================
// STEP 3: Send status update to Jenni Logistics
// ==========================================
async function sendStatusUpdateToJenni(shipmentNumber, action, details = {}) {
await ensureValidToken();
try {
const payload = {
system_code: 'YOUR_SYSTEM_CODE',
updates: [{
shipment_number: shipmentNumber,
action_code: action,
timestamp: new Date().toISOString(),
...details
}]
};
console.log(`📤 Sending ${action} update for ${shipmentNumber}`);
const response = await axios.post(
`${JENNI_API_URL}/v2/push/update-status`,
payload,
{
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
}
);
console.log('✅ Status update sent successfully');
return response.data;
} catch (error) {
console.error('❌ Failed to send status update:', error.response?.data || error.message);
throw error;
}
}
// ==========================================
// USAGE EXAMPLES
// ==========================================
// Example 1: Successful Delivery
async function markAsDelivered(shipmentNumber, driverName) {
return await sendStatusUpdateToJenni(
shipmentNumber,
'SUCCESSFUL_DELIVERY',
{
location: {
latitude: 33.312805,
longitude: 44.361488,
address: 'Baghdad, Al-Karrada'
},
proof_of_delivery: {
images: [
'https://your-cdn.com/proof/12345.jpg'
],
receiver_name: 'Ahmed Mohammed',
delivery_time: new Date().toISOString()
},
notes: `Delivered by driver: ${driverName}`
}
);
}
// Example 2: Postponed Delivery
async function markAsPostponed(shipmentNumber, reason, postponedDateId) {
return await sendStatusUpdateToJenni(
shipmentNumber,
'POSTPONED',
{
postponed_reason: reason,
postponed_reason_en: 'Customer not available',
postponed_reason_ku: 'کڕیار بەردەست نییە',
postponed_date_id: postponedDateId, // ✅ REQUIRED: 1=tomorrow, 2=2 days, 3=3+ days
notes: 'Will retry delivery tomorrow'
}
);
}
// Example 3: Return to Store
async function markAsReturned(shipmentNumber, returnReason) {
return await sendStatusUpdateToJenni(
shipmentNumber,
'RETURNED_WITH_AGENT',
{
return_reason: returnReason,
return_reason_en: 'Customer refused',
return_reason_ku: 'کڕیار ڕەتکردەوە',
notes: 'Customer refused to receive'
}
);
}
// Example 4: Partial Delivery
async function markAsPartialDelivery(shipmentNumber, deliveredQty, returnedQty) {
return await sendStatusUpdateToJenni(
shipmentNumber,
'PARTIAL_DELIVERY',
{
is_partial: true,
quantity_delivered: deliveredQty,
quantity_returned: returnedQty,
partial_return_action: 'RETURN_TO_STORE',
notes: `Delivered ${deliveredQty} items, returned ${returnedQty} items`
}
);
}
// ==========================================
// AUTOMATED STATUS SYNC
// ==========================================
async function syncAllPendingUpdates() {
console.log('🔄 Starting status sync...');
// Get all shipments with pending status updates from your database
const pendingUpdates = await db.status_updates.findAll({
where: { synced_to_jenni: false }
});
for (const update of pendingUpdates) {
try {
await sendStatusUpdateToJenni(
update.shipment_number,
update.action,
update.details
);
// Mark as synced
await db.status_updates.update(
{ synced_to_jenni: true },
{ where: { id: update.id } }
);
} catch (error) {
console.error(`Failed to sync ${update.shipment_number}:`, error.message);
// Will retry in next sync
}
}
console.log('✅ Status sync completed');
}
// Run sync every 5 minutes
setInterval(syncAllPendingUpdates, 5 * 60 * 1000);
// Export functions
module.exports = {
sendStatusUpdateToJenni,
markAsDelivered,
markAsPostponed,
markAsReturned,
markAsPartialDelivery,
syncAllPendingUpdates
};
// workflow.js - Complete workflow example
const { markAsDelivered, markAsPostponed, markAsReturned } = require('./statusUpdater');
// Simulate driver delivery process
async function driverDeliveryWorkflow(shipmentNumber) {
console.log(`\n🚚 Driver starting delivery for: ${shipmentNumber}`);
try {
// 1. Driver picks up shipment from warehouse
console.log('📦 Shipment picked up from warehouse');
// 2. Driver attempts delivery
console.log('🚗 Driver on the way to customer...');
await sleep(2000); // Simulate travel time
// 3. Simulate different outcomes
const outcome = Math.random();
if (outcome < 0.7) {
// 70% chance: Successful delivery
console.log('✅ Customer received the shipment');
await markAsDelivered(shipmentNumber, 'Driver Ali');
console.log('📤 Status updated to Jenni Logistics: DELIVERED');
} else if (outcome < 0.85) {
// 15% chance: Customer not available
console.log('⏰ Customer not available');
await markAsPostponed(shipmentNumber, 'Customer not available');
console.log('📤 Status updated to Jenni Logistics: POSTPONED');
} else {
// 15% chance: Customer refused
console.log('❌ Customer refused delivery');
await markAsReturned(shipmentNumber, 'Customer refused');
console.log('📤 Status updated to Jenni Logistics: RETURNED');
}
} catch (error) {
console.error('❌ Error in delivery workflow:', error.message);
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Test the workflow
driverDeliveryWorkflow('SHIP001');
# server.py - Flask Example
from flask import Flask, request, jsonify
from datetime import datetime
import os
app = Flask(__name__)
# ==========================================
# STEP 1: Implement endpoint to receive from Jenni Logistics
# ==========================================
@app.route('/v2/shipments/create', methods=['POST'])
def receive_shipments():
print('📥 Received shipments from Jenni Logistics')
try:
# 1. Validate authentication
auth_header = request.headers.get('Authorization', '')
token = auth_header.replace('Bearer ', '')
if not token or not is_valid_jenni_token(token):
return jsonify({
'success': False,
'message': 'Invalid authentication token'
}), 401
# 2. Extract shipments from request
data = request.get_json()
system_code = data.get('system_code')
shipments = data.get('shipments', [])
print(f'System: {system_code}, Shipments: {len(shipments)}')
# 3. Process and save shipments
accepted_shipments = []
rejected_shipments = []
for shipment in shipments:
try:
# Check for duplicates
if is_duplicate_shipment(shipment['shipment_number'], shipment['shipment_id']):
rejected_shipments.append({
'shipment_number': shipment['shipment_number'],
'shipment_id': shipment['shipment_id'],
'airway_bill_number': shipment.get('airway_bill_number'),
'reason': 'Shipment number already exists',
'error_code': 'DUPLICATE_SHIPMENT',
'field': 'shipment_number'
})
continue
# Save to database
saved_shipment = save_shipment_to_database(shipment)
# Assign to driver
assign_to_driver(saved_shipment)
accepted_shipments.append({
'shipment_number': shipment['shipment_number'],
'shipment_id': shipment['shipment_id'],
'external_id': str(saved_shipment['id']),
'airway_bill_number': shipment.get('airway_bill_number')
})
except Exception as e:
rejected_shipments.append({
'shipment_number': shipment['shipment_number'],
'shipment_id': shipment.get('shipment_id'),
'airway_bill_number': shipment.get('airway_bill_number'),
'reason': str(e),
'error_code': 'PROCESSING_ERROR',
'field': None
})
# 4. Return response (REQUIRED FORMAT)
return jsonify({
'success': len(accepted_shipments) > 0,
'message': f'{len(accepted_shipments)} shipment(s) processed successfully' if accepted_shipments else 'All shipments failed to process',
'timestamp': datetime.utcnow().isoformat() + 'Z',
'accepted_shipments': accepted_shipments,
'rejected_shipments': rejected_shipments,
'summary': {
'total_requested': len(shipments),
'accepted_count': len(accepted_shipments),
'rejected_count': len(rejected_shipments)
}
})
except Exception as error:
print(f'❌ Error receiving shipments: {error}')
return jsonify({
'success': False,
'message': 'Internal server error',
'timestamp': datetime.utcnow().isoformat() + 'Z'
}), 500
def is_valid_jenni_token(token):
"""Validate token from Jenni Logistics"""
return token == os.getenv('JENNI_AUTH_TOKEN')
def save_shipment_to_database(shipment):
"""Save shipment to your database"""
# Your database logic here
saved = {
'id': 12345,
'shipment_number': shipment['shipment_number'],
'receiver_name': shipment['receiver_name'],
'status': 'PENDING'
}
return saved
def assign_to_driver(shipment):
"""Assign shipment to available driver"""
# Your assignment logic here
pass
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3000)
print('✅ Server running on port 3000')
print('📡 Waiting for shipments from Jenni Logistics...')
# status_updater.py - Send updates to Jenni Logistics server
import requests
from datetime import datetime, timedelta
JENNI_API_URL = 'https://jenni-api.com/api'
JENNI_USERNAME = 'your_username'
JENNI_PASSWORD = 'your_password'
auth_token = None
token_expiry = None
# ==========================================
# STEP 2: Login to Jenni Logistics
# ==========================================
def login_to_jenni():
global auth_token, token_expiry
try:
response = requests.post(
f'{JENNI_API_URL}/v2/auth/login',
json={
'username': JENNI_USERNAME,
'password': JENNI_PASSWORD
}
)
response.raise_for_status()
data = response.json()
auth_token = data['token']
token_expiry = datetime.now() + timedelta(seconds=data['expires_in'])
print('✅ Logged in to Jenni Logistics successfully')
return auth_token
except Exception as error:
print(f'❌ Login failed: {error}')
raise
def ensure_valid_token():
"""Ensure token is valid"""
global auth_token, token_expiry
if not auth_token or datetime.now() > (token_expiry - timedelta(minutes=5)):
login_to_jenni()
# ==========================================
# STEP 3: Send status update
# ==========================================
def send_status_update(shipment_number, action, details=None):
ensure_valid_token()
if details is None:
details = {}
try:
payload = {
'system_code': 'YOUR_SYSTEM_CODE',
'updates': [{
'shipment_number': shipment_number,
'action_code': action,
'timestamp': datetime.utcnow().isoformat() + 'Z',
**details
}]
}
print(f'📤 Sending {action} update for {shipment_number}')
response = requests.post(
f'{JENNI_API_URL}/v2/push/update-status',
json=payload,
headers={
'Authorization': f'Bearer {auth_token}',
'Content-Type': 'application/json'
}
)
response.raise_for_status()
print('✅ Status update sent successfully')
return response.json()
except Exception as error:
print(f'❌ Failed to send status update: {error}')
raise
# ==========================================
# USAGE EXAMPLES
# ==========================================
def mark_as_delivered(shipment_number, driver_name):
"""Mark shipment as delivered"""
return send_status_update(
shipment_number,
'SUCCESSFUL_DELIVERY',
{
'location': {
'latitude': 33.312805,
'longitude': 44.361488,
'address': 'Baghdad, Al-Karrada'
},
'proof_of_delivery': {
'images': ['https://your-cdn.com/proof/12345.jpg'],
'receiver_name': 'Ahmed Mohammed',
'delivery_time': datetime.utcnow().isoformat() + 'Z'
},
'notes': f'Delivered by driver: {driver_name}'
}
)
def mark_as_postponed(shipment_number, reason, postponed_date_id):
"""Mark shipment as postponed"""
return send_status_update(
shipment_number,
'POSTPONED',
{
'postponed_reason': reason,
'postponed_reason_en': 'Customer not available',
'postponed_reason_ku': 'کڕیار بەردەست نییە',
'postponed_date_id': postponed_date_id, # ✅ REQUIRED: 1=tomorrow, 2=2 days, 3=3+ days
'notes': 'Will retry delivery tomorrow'
}
)
def mark_as_returned(shipment_number, return_reason):
"""Mark shipment as returned"""
return send_status_update(
shipment_number,
'RETURNED_WITH_AGENT',
{
'return_reason': return_reason,
'return_reason_en': 'Customer refused',
'return_reason_ku': 'کڕیار ڕەتکردەوە',
'notes': 'Customer refused to receive'
}
)
# Example usage
if __name__ == '__main__':
# Successful delivery
mark_as_delivered('SHIP001', 'Driver Ali')
# Postponed delivery
mark_as_postponed('SHIP002', 'Customer not available')
# Returned delivery
mark_as_returned('SHIP003', 'Customer refused')
<?php
// receive.php - Receive shipments from Jenni Logistics
header('Content-Type: application/json');
// ==========================================
// STEP 1: Implement /receive endpoint
// ==========================================
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
error_log('📥 Received shipments from Jenni Logistics');
try {
// 1. Validate authentication
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
$token = str_replace('Bearer ', '', $authHeader);
if (empty($token) || !isValidJenniToken($token)) {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Invalid authentication token'
]);
exit;
}
// 2. Extract shipments
$data = json_decode(file_get_contents('php://input'), true);
$systemCode = $data['system_code'] ?? '';
$shipments = $data['shipments'] ?? [];
error_log("System: $systemCode, Shipments: " . count($shipments));
// 3. Process shipments
$acceptedShipments = [];
$rejectedShipments = [];
foreach ($shipments as $shipment) {
try {
// Check if shipment already exists in your database
if (shipmentExists($shipment['shipment_id'])) {
$rejectedShipments[] = [
'shipment_number' => $shipment['shipment_number'],
'shipment_id' => $shipment['shipment_id'],
'airway_bill_number' => $shipment['airway_bill_number'] ?? null,
'reason' => 'Shipment already exists in your system',
'error_code' => 'DUPLICATE_SHIPMENT',
'field' => 'shipment_number'
];
continue;
}
// Save shipment to your database
$savedShipment = saveToDatabase($shipment);
// Assign shipment to your delivery driver
assignToYourDriver($savedShipment);
$acceptedShipments[] = [
'shipment_number' => $shipment['shipment_number'],
'shipment_id' => $shipment['shipment_id'],
'external_id' => (string)$savedShipment['id'],
'airway_bill_number' => $shipment['airway_bill_number'] ?? null
];
} catch (Exception $e) {
$rejectedShipments[] = [
'shipment_number' => $shipment['shipment_number'],
'shipment_id' => $shipment['shipment_id'] ?? null,
'airway_bill_number' => $shipment['airway_bill_number'] ?? null,
'reason' => $e->getMessage(),
'error_code' => 'PROCESSING_ERROR',
'field' => null
];
}
}
// 4. Return response (REQUIRED FORMAT)
echo json_encode([
'success' => count($acceptedShipments) > 0,
'message' => count($acceptedShipments) > 0
? count($acceptedShipments) . ' shipment(s) processed successfully'
: 'All shipments failed to process',
'timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
'accepted_shipments' => $acceptedShipments,
'rejected_shipments' => $rejectedShipments,
'summary' => [
'total_requested' => count($shipments),
'accepted_count' => count($acceptedShipments),
'rejected_count' => count($rejectedShipments)
]
]);
} catch (Exception $error) {
error_log('❌ Error receiving shipments: ' . $error->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Internal server error',
'timestamp' => gmdate('Y-m-d\TH:i:s\Z')
]);
}
}
function isValidJenniToken($token) {
return $token === getenv('JENNI_AUTH_TOKEN');
}
function saveToDatabase($shipment) {
// Save shipment to your database
// Return saved shipment with ID
return [
'id' => 12345,
'shipment_number' => $shipment['shipment_number'],
'status' => 'PENDING'
];
}
function assignToYourDriver($shipment) {
// Your logic to assign shipment to your delivery driver
}
?>
<?php
// StatusUpdater.php - Send updates to Jenni Logistics server
class JenniStatusUpdater {
private $apiUrl = 'https://jenni-api.com/api'; // Jenni Logistics server URL
private $username;
private $password;
private $authToken = null;
private $tokenExpiry = null;
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
// ==========================================
// Login to Jenni Logistics server
// ==========================================
public function login() {
$ch = curl_init($this->apiUrl . '/v2/auth/login');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'username' => $this->username,
'password' => $this->password
]));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception('Login failed: ' . $response);
}
$data = json_decode($response, true);
$this->authToken = $data['token'];
$this->tokenExpiry = time() + $data['expires_in'];
error_log('✅ Logged in to Jenni Logistics successfully');
return $this->authToken;
}
private function ensureValidToken() {
if (!$this->authToken || time() > ($this->tokenExpiry - 300)) {
$this->login();
}
}
// ==========================================
// STEP 3: Send status update
// ==========================================
public function sendStatusUpdate($shipmentNumber, $action, $details = []) {
$this->ensureValidToken();
$payload = [
'system_code' => 'YOUR_SYSTEM_CODE',
'updates' => [[
'shipment_number' => $shipmentNumber,
'action_code' => $action,
'timestamp' => gmdate('Y-m-d\TH:i:s\Z')
] + $details]
];
error_log("📤 Sending $action update for $shipmentNumber");
$ch = curl_init($this->apiUrl . '/v2/push/update-status');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->authToken
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception('Failed to send status update: ' . $response);
}
error_log('✅ Status update sent successfully');
return json_decode($response, true);
}
// ==========================================
// USAGE EXAMPLES
// ==========================================
public function markAsDelivered($shipmentNumber, $driverName) {
return $this->sendStatusUpdate(
$shipmentNumber,
'SUCCESSFUL_DELIVERY',
[
'location' => [
'latitude' => 33.312805,
'longitude' => 44.361488,
'address' => 'Baghdad, Al-Karrada'
],
'proof_of_delivery' => [
'images' => ['https://your-cdn.com/proof/12345.jpg'],
'receiver_name' => 'Ahmed Mohammed',
'delivery_time' => gmdate('Y-m-d\TH:i:s\Z')
],
'notes' => "Delivered by driver: $driverName"
]
);
}
public function markAsPostponed($shipmentNumber, $reason, $postponedDateId) {
return $this->sendStatusUpdate(
$shipmentNumber,
'POSTPONED',
[
'postponed_reason' => $reason,
'postponed_reason_en' => 'Customer not available',
'postponed_reason_ku' => 'کڕیار بەردەست نییە',
'postponed_date_id' => $postponedDateId, // ✅ REQUIRED: 1=tomorrow, 2=2 days, 3=3+ days
'notes' => 'Will retry delivery tomorrow'
]
);
}
public function markAsReturned($shipmentNumber, $returnReason) {
return $this->sendStatusUpdate(
$shipmentNumber,
'RETURNED_WITH_AGENT',
[
'return_reason' => $returnReason,
'return_reason_en' => 'Customer refused',
'return_reason_ku' => 'کڕیار ڕەتکردەوە',
'notes' => 'Customer refused to receive'
]
);
}
}
// Example usage
$updater = new JenniStatusUpdater('your_username', 'your_password');
// Successful delivery
$updater->markAsDelivered('SHIP001', 'Driver Ali');
// Postponed delivery
$updater->markAsPostponed('SHIP002', 'Customer not available');
// Returned delivery
$updater->markAsReturned('SHIP003', 'Customer refused');
?>
// ShipmentReceiveController.java - Spring Boot Example
@RestController
@RequestMapping("/v2/shipments")
@Slf4j
public class ShipmentReceiveController {
@Autowired
private ShipmentService shipmentService;
// ==========================================
// STEP 1: Implement /receive endpoint
// ==========================================
@PostMapping("/receive")
public ResponseEntity<ReceiveResponse> receiveShipments(
@RequestHeader("Authorization") String authHeader,
@RequestBody ReceiveRequest request) {
log.info("📥 Received shipments from Jenni Logistics");
try {
// 1. Validate authentication
String token = authHeader.replace("Bearer ", "");
if (!isValidJenniToken(token)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ReceiveResponse.error("Invalid authentication token"));
}
// 2. Extract shipments
String systemCode = request.getSystemCode();
List<Shipment> shipments = request.getShipments();
log.info("System: {}, Shipments: {}", systemCode, shipments.size());
// 3. Process shipments
List<Map<String, Object>> acceptedShipments = new ArrayList<>();
List<Map<String, Object>> rejectedShipments = new ArrayList<>();
for (Shipment shipment : shipments) {
try {
// Check if shipment already exists in your database
if (shipmentService.shipmentExists(
shipment.getShipmentId())) {
Map<String, Object> rejected = new HashMap<>();
rejected.put("shipment_number", shipment.getShipmentNumber());
rejected.put("shipment_id", shipment.getShipmentId());
rejected.put("airway_bill_number", shipment.getAirwayBillNumber());
rejected.put("reason", "Shipment number already exists");
rejected.put("error_code", "DUPLICATE_SHIPMENT");
rejected.put("field", "shipment_number");
rejectedShipments.add(rejected);
continue;
}
// Save to database
Shipment saved = shipmentService.saveShipment(shipment);
// Assign shipment to your delivery driver
shipmentService.assignToYourDriver(saved);
// Add to accepted
Map<String, Object> accepted = new HashMap<>();
accepted.put("shipment_number", shipment.getShipmentNumber());
accepted.put("shipment_id", shipment.getShipmentId());
accepted.put("external_id", String.valueOf(saved.getId()));
accepted.put("airway_bill_number", shipment.getAirwayBillNumber());
acceptedShipments.add(accepted);
} catch (Exception e) {
Map<String, Object> rejected = new HashMap<>();
rejected.put("shipment_number", shipment.getShipmentNumber());
rejected.put("shipment_id", shipment.getShipmentId());
rejected.put("airway_bill_number", shipment.getAirwayBillNumber());
rejected.put("reason", e.getMessage());
rejected.put("error_code", "PROCESSING_ERROR");
rejected.put("field", null);
rejectedShipments.add(rejected);
}
}
// 4. Return response (REQUIRED FORMAT)
Map<String, Object> response = new HashMap<>();
response.put("success", !acceptedShipments.isEmpty());
response.put("message", !acceptedShipments.isEmpty()
? String.format("%d shipment(s) processed successfully", acceptedShipments.size())
: "All shipments failed to process");
response.put("timestamp", Instant.now().toString());
response.put("accepted_shipments", acceptedShipments);
response.put("rejected_shipments", rejectedShipments);
response.put("summary", Map.of(
"total_requested", shipments.size(),
"accepted_count", acceptedShipments.size(),
"rejected_count", rejectedShipments.size()
));
return ResponseEntity.ok(response);
} catch (Exception error) {
log.error("❌ Error receiving shipments", error);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ReceiveResponse.error("Internal server error"));
}
}
private boolean isValidJenniToken(String token) {
String expectedToken = System.getenv("JENNI_AUTH_TOKEN");
return token != null && token.equals(expectedToken);
}
}
// JenniStatusUpdater.java - Send updates to Jenni Logistics server
@Service
@Slf4j
public class JenniStatusUpdater {
private static final String JENNI_API_URL = "https://jenni-api.com/api";
@Value("${jenni.username}")
private String username;
@Value("${jenni.password}")
private String password;
private String authToken;
private Instant tokenExpiry;
private final RestTemplate restTemplate;
public JenniStatusUpdater(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
// ==========================================
// STEP 2: Login to Jenni Logistics
// ==========================================
public void login() {
try {
LoginRequest loginRequest = new LoginRequest(username, password);
ResponseEntity<LoginResponse> response = restTemplate.postForEntity(
JENNI_API_URL + "/v2/auth/login",
loginRequest,
LoginResponse.class
);
LoginResponse data = response.getBody();
this.authToken = data.getToken();
this.tokenExpiry = Instant.now().plusSeconds(data.getExpiresIn());
log.info("✅ Logged in to Jenni Logistics successfully");
} catch (Exception e) {
log.error("❌ Login failed", e);
throw new RuntimeException("Login failed", e);
}
}
private void ensureValidToken() {
if (authToken == null || Instant.now().isAfter(tokenExpiry.minusSeconds(300))) {
login();
}
}
// ==========================================
// STEP 3: Send status update
// ==========================================
public StatusUpdateResponse sendStatusUpdate(
String shipmentNumber,
String action,
Map<String, Object> details) {
ensureValidToken();
try {
Map<String, Object> update = new HashMap<>();
update.put("shipment_number", shipmentNumber);
update.put("action_code", action);
update.put("timestamp", Instant.now().toString());
update.putAll(details);
Map<String, Object> payload = Map.of(
"system_code", "YOUR_SYSTEM_CODE",
"updates", List.of(update)
);
log.info("📤 Sending {} update for {}", action, shipmentNumber);
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(authToken);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(payload, headers);
ResponseEntity<StatusUpdateResponse> response = restTemplate.postForEntity(
JENNI_API_URL + "/v2/push/update-status",
request,
StatusUpdateResponse.class
);
log.info("✅ Status update sent successfully");
return response.getBody();
} catch (Exception e) {
log.error("❌ Failed to send status update", e);
throw new RuntimeException("Failed to send status update", e);
}
}
// ==========================================
// USAGE EXAMPLES
// ==========================================
public StatusUpdateResponse markAsDelivered(String shipmentNumber, String driverName) {
Map<String, Object> details = Map.of(
"location", Map.of(
"latitude", 33.312805,
"longitude", 44.361488,
"address", "Baghdad, Al-Karrada"
),
"proof_of_delivery", Map.of(
"images", List.of("https://your-cdn.com/proof/12345.jpg"),
"receiver_name", "Ahmed Mohammed",
"delivery_time", Instant.now().toString()
),
"notes", "Delivered by driver: " + driverName
);
return sendStatusUpdate(shipmentNumber, "SUCCESSFUL_DELIVERY", details);
}
public StatusUpdateResponse markAsPostponed(String shipmentNumber, String reason, Integer postponedDateId) {
Map<String, Object> details = Map.of(
"postponed_reason", reason,
"postponed_reason_en", "Customer not available",
"postponed_reason_ku", "کڕیار بەردەست نییە",
"postponed_date_id", postponedDateId, // ✅ REQUIRED: 1=tomorrow, 2=2 days, 3=3+ days
"notes", "Will retry delivery tomorrow"
);
return sendStatusUpdate(shipmentNumber, "POSTPONED", details);
}
public StatusUpdateResponse markAsReturned(String shipmentNumber, String returnReason) {
Map<String, Object> details = Map.of(
"return_reason", returnReason,
"return_reason_en", "Customer refused",
"return_reason_ku", "کڕیار ڕەتکردەوە",
"notes", "Customer refused to receive"
);
return sendStatusUpdate(shipmentNumber, "RETURNED_WITH_AGENT", details);
}
}
| Configuration Field | Value | Description |
|---|---|---|
is_supports_v2 |
TRUE | Enable V2 Push API |
is_v2_push_url |
https://your-server.com/v2/shipments/create | Your endpoint URL to receive shipments from Jenni Logistics |
is_auth_method |
TOKEN or LOGIN | How Jenni Logistics authenticates to your server |
is_request_connection_token |
your_secret_token | Static token (if using TOKEN auth) |
is_dlv_agent |
Agent/Driver ID in Jenni | Which agent triggers the push |
Follow this step-by-step workflow to successfully integrate with our V2 API.
system_code (company identifier)username and password)POST https://jenni.alzaeemexp.com/api/v2/auth/login
{
"username": "your_username",
"password": "your_password"
}
Response:
{
"token": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi..."
}
Download and cache these lists locally for validation:
GET /v2/reference/governorates?download=excelGET /v2/reference/cities?download=excelGET /v2/reference/return-reasons?download=excelGET /v2/reference/postponed-reasons?download=excelPOST https://jenni.alzaeemexp.com/api/v2/shipments/create
Headers: Authorization: Bearer {token}
{
"system_code": "YOUR_CODE",
"shipments": [
{
"shipment_number": "SHIP001",
"receiver_name": "Ahmed Ali",
"receiver_phone_1": "07901234567",
"governorate_code": "BGD",
"city": "Al-Karrada",
"amount_iqd": 50000
// ... other fields
}
]
}
shipment_id for each accepted shipment
shipment_id in your database - you'll need it when updating shipment status or tracking shipments.
POST https://jenni.alzaeemexp.com/api/v2/shipments/query
Headers: Authorization: Bearer {token}
{
"shipment_numbers": ["SHIP001", "SHIP002"]
}
Response includes:
- Current status (current_step)
- Delivery agent info
- Settlement info
- Full history
Step 6a: Check available actions first
GET https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=SHIP001
Headers: Authorization: Bearer {token}
Step 6b: Perform the action
POST https://jenni.alzaeemexp.com/api/v2/shipments/update-status
Headers: Authorization: Bearer {token}
{
"shipment_id": 12345,
"action": "POSTPONED",
"postponed_reason": "العميل غير موجود",
"postponed_reason_en": "Customer not available",
"postponed_reason_ku": "کڕیار بەردەست نییە",
"postponed_date_id": 1
}
# Order counts by status
GET https://jenni.alzaeemexp.com/api/v2/statistics/orders-by-status
# Orders in process
GET https://jenni.alzaeemexp.com/api/v2/orders/in-process
JWT token-based authentication with 24h access + 30-day refresh tokens
Professional, standardized action codes (SUCCESSFUL_DELIVERY, RETURN_TO_STORE, etc.)
26 step statuses + 40+ action codes mapped to international conventions
Full Arabic + English responses with RTL/LTR support
Create up to 100 shipments or query up to 100 in a single API call. Pagination supports 100 per page.
Detailed validation with specific error codes and field-level rejection reasons
Real-time validation for 18 governorates and 100+ cities across Iraq
Accept payments in IQD and USD with automatic conversion tracking
Access governorates, cities, reasons, and action codes (some public, some protected)
Export reference data in Excel or JSON formats for offline use
Support for partial delivery/returns with quantity and amount tracking
Live API tester, 5 language examples (cURL, JS, Python, Java, PHP)
Full V2 support for any two systems with external_shipment_id tracking and status synchronization
Immediate retry (1s-4s) + Persistent retry (15min-60min) for all V2 integrations
4-tier priority search for all V2 systems: shipment_id → external_id → sentToCaseId → receipt number
Track sender_name and sender_phone from any V2 source system for complete origin details
Push Out Mode means Jenni Logistics pushes shipments to your delivery system. This involves two-way communication:
Important: The two APIs below run on different servers:
YOUR_DOMAIN/v2/shipments/createJENNI_DOMAIN/v2/push/update-statusis_dlv_agent,
Jenni Logistics automatically sends them to your endpoint (is_v2_push_url) in real-time. No manual triggering required!
Purpose: Jenni Logistics automatically sends shipments to this endpoint on YOUR server when they are assigned to your delivery agents.
is_v2_push_url to your domain (e.g., https://yourstore.com or https://yourstore.com/api)/v2/shipments/create to your configured URLYOUR_DOMAIN/v2/shipments/createPOST YOUR_DOMAIN/v2/shipments/create
Content-Type: application/json
Authorization: Bearer {token}
// Token is either static (is_request_connection_token)
// or dynamic (from LOGIN authentication)
{
"system_code": "YOUR_SYSTEM_CODE",
"auth_method": "TOKEN", // or "LOGIN"
"static_token": "your-token-here", // if TOKEN method
"username": "user", // if LOGIN method
"password": "pass", // if LOGIN method
"shipments": [
{
"shipment_id": 12345, // ✅ Jenni Logistics case ID - Use for matching
"shipment_number": "SHIP-001", // Receipt number (can be duplicate!)
"external_shipment_id": "12345", // ✅ For Jenni-to-Jenni: same as shipment_id
"receiver_name": "أحمد علي",
"receiver_phone_1": "07901234567",
"governorate_code": "BGD", // Global code
"city_name": "الكرادة", // For non-Jenni systems
"city": "الكرادة", // ✅ For Jenni-to-Jenni compatibility
"address": "شارع 14، بناية 5",
"amount_iqd": 50000,
"amount_usd": 0,
"quantity": 2,
"note": "Handle with care",
"sender_name": "متجر الإلكترونيات",
"sender_phone": "07701234567",
"fragile": false
}
]
}
Jenni now sends BOTH field formats for compatibility:
external_shipment_id = shipment_id (for receiving Jenni Logistics instance)city = city_name (both formats sent)external_shipment_idshipment_id and city_name as beforeResult: Jenni Logistics Push Out format is now 100% compatible with Jenni Logistics Receive In format!
Your endpoint MUST return the following format EXACTLY:
total_requested, accepted_count, rejected_count{
"success": true,
"message": "1 shipment processed successfully",
"timestamp": "2025-11-03T05:15:51Z",
"accepted_shipments": [
{
"shipment_number": "SHIP-001",
"shipment_id": 12345,
"external_id": "EXT-12345", // Your internal shipment ID (optional)
"airway_bill_number": "AWB-789" // Optional
}
],
"rejected_shipments": [
{
"shipment_number": "94777598",
"shipment_id": null,
"airway_bill_number": null,
"reason": "Shipment number already exists",
"error_code": "DUPLICATE_SHIPMENT",
"field": "shipment_number"
}
],
"summary": {
"total_requested": 2,
"accepted_count": 1,
"rejected_count": 1
}
}
| Field | Type | Description |
|---|---|---|
success |
Boolean | true if at least one shipment was accepted, false if all rejected |
message |
String | Human-readable status message (e.g., "1 shipment processed successfully") |
timestamp |
String | ISO 8601 timestamp (e.g., "2025-11-03T05:15:51Z") |
accepted_shipments |
Array | REQUIRED: List of successfully processed shipments. Each item should include shipment_number, shipment_id (if available), and optionally external_id (your internal ID) |
rejected_shipments |
Array | REQUIRED: List of rejected shipments. Each item must include shipment_number, error_code, reason, and field (field that caused error) |
summary |
Object | REQUIRED: Summary statistics with total_requested, accepted_count, rejected_count |
@RestController
@RequestMapping("/v2")
public class JenniIntegrationController {
@PostMapping("/shipments/create")
public ResponseEntity<Map<String, Object>> receiveShipments(
@RequestBody Map<String, Object> request,
@RequestHeader("Authorization") String authHeader) {
// 1. Validate authentication token
if (!isValidToken(authHeader)) {
return ResponseEntity.status(401).body(Map.of("error", "Invalid token"));
}
// 2. Extract shipments from request
List<Map<String, Object>> shipments = (List) request.get("shipments");
List<Map<String, Object>> acceptedShipments = new ArrayList<>();
List<Map<String, Object>> rejectedShipments = new ArrayList<>();
int successCount = 0;
int failedCount = 0;
for (Map<String, Object> shipment : shipments) {
try {
// ✅ CRITICAL: Use shipment_id for safe matching
Long shipmentId = Long.valueOf(shipment.get("shipment_id").toString());
String receiptNo = (String) shipment.get("shipment_number");
// Check if shipment already exists in your database
if (shipmentExistsInDatabase(shipmentId, receiptNo)) {
rejectedShipments.add(Map.of(
"shipment_number", receiptNo,
"shipment_id", shipmentId,
"airway_bill_number", shipment.get("airway_bill_number") != null
? shipment.get("airway_bill_number").toString() : null,
"reason", "Shipment already exists in your system",
"error_code", "DUPLICATE_SHIPMENT",
"field", "shipment_number"
));
failedCount++;
continue;
}
// Save shipment to your database
String externalId = saveToDatabase(shipment);
// Add to accepted shipments
Map<String, Object> accepted = new HashMap<>();
accepted.put("shipment_number", receiptNo);
accepted.put("shipment_id", shipmentId);
accepted.put("external_id", externalId); // Your internal ID
accepted.put("airway_bill_number", shipment.get("airway_bill_number"));
acceptedShipments.add(accepted);
successCount++;
} catch (Exception e) {
Long shipmentId = shipment.get("shipment_id") != null
? Long.valueOf(shipment.get("shipment_id").toString()) : null;
rejectedShipments.add(Map.of(
"shipment_number", shipment.get("shipment_number").toString(),
"shipment_id", shipmentId,
"airway_bill_number", shipment.get("airway_bill_number") != null
? shipment.get("airway_bill_number").toString() : null,
"reason", e.getMessage(),
"error_code", "PROCESSING_ERROR",
"field", null
));
failedCount++;
}
}
// Build response in REQUIRED format
Map<String, Object> response = new HashMap<>();
response.put("success", successCount > 0);
response.put("message", successCount > 0
? String.format("%d shipment(s) processed successfully", successCount)
: "All shipments failed to process");
response.put("timestamp", Instant.now().toString());
response.put("accepted_shipments", acceptedShipments);
response.put("rejected_shipments", rejectedShipments);
response.put("summary", Map.of(
"total_requested", shipments.size(),
"accepted_count", successCount,
"rejected_count", failedCount
));
return ResponseEntity.ok(response);
}
}
Purpose: Your system calls this endpoint on Jenni Logistics server to send status updates back when delivery status changes.
{
"system_code": "YOUR_SYSTEM_CODE",
"auth_method": "TOKEN",
"static_token": "your-token-here",
"updates": [
{
"shipment_id": 12345, // ✅ BEST: Use shipment_id from original push
"shipment_number": "SHIP-001", // Optional: receipt number
"action_code": "SUCCESSFUL_DELIVERY", // Global action code
"timestamp": "2025-10-31T15:30:00Z",
"note": "Delivered successfully",
// === POSTPONED Action ===
// ✅ REQUIRED fields for POSTPONED action:
"postponed_reason": "العميل غير موجود", // Required for POSTPONED
"postponed_reason_en": "Customer not available", // Optional: English description
"postponed_reason_ku": "کڕیار بەردەست نییە", // Optional: Kurdish description
"postponed_date_id": 1, // Required: 1=tomorrow, 2=in 2 days, 3=3+ days
// === RETURN Actions (RETURNED_WITH_AGENT, RETURN_TO_STORE) ===
// ✅ REQUIRED fields for return actions:
"return_reason": "رفض الاستلام", // Required for any RETURN action
"return_reason_en": "Customer refused", // Optional: English description
"return_reason_ku": "کڕیار ڕەتکردەوە", // Optional: Kurdish description
// === REATTEMPT/TREATMENT ===
// ✅ For DELIVERY_REATTEMPT, use 'note' field for treatment details
"note": "تم التواصل مع العميل - موعد جديد غداً",
// === OTHER Optional Fields ===
"new_amount_iqd": 45000, // For AMOUNT_CHANGE actions
"proof_image_url": "https://cdn.example.com/proof.jpg",
"delivery_latitude": 33.3152,
"delivery_longitude": 44.3661,
// === Partial Delivery ===
"is_partial": false,
"quantity_delivered": 2,
"quantity_returned": 0,
"partial_has_returned": true,
"partial_return_action": "RETURN_TO_STORE"
}
]
}
// When your driver updates delivery status
async function sendStatusToJenni(shipmentId, actionCode) {
const request = {
system_code: "YOUR_SYSTEM_CODE",
auth_method: "TOKEN",
static_token: "your-token-here",
updates: [{
shipment_id: shipmentId, // ✅ Use the ID we received from Jenni Logistics
action_code: actionCode, // e.g., "SUCCESSFUL_DELIVERY"
timestamp: new Date().toISOString(),
note: "Status updated by driver"
}]
};
const response = await fetch('https://jenni-api.com/v2/push/update-status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${YOUR_TOKEN}`
},
body: JSON.stringify(request)
});
const result = await response.json();
console.log('Status update result:', result);
}
Use these standardized codes when sending status updates:
| Action Code | Arabic / العربية | When to Use | Required Fields |
|---|---|---|---|
SUCCESSFUL_DELIVERY |
سُلمت بنجاح | Shipment delivered successfully to customer | None |
POSTPONED |
مؤجل | Delivery postponed to another date |
postponed_reason postponed_date_id 1=tomorrow, 2=2 days, 3=3+ days |
RETURNED_WITH_AGENT |
راجع عند المندوب | Shipment returned, still with delivery agent | return_reason |
RETURN_TO_STORE |
راجع للمخزن | Shipment returned to warehouse/store | return_reason |
DELIVERY_REATTEMPT |
إعادة محاولة التوصيل | Rescheduled for another delivery attempt | note Use note field for treatment/follow-up details |
PARTIAL_DELIVERY |
تسليم جزئي | Some items delivered, some returned |
is_partial=true quantity_delivered quantity_returned partial_return_action |
SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE |
سُلمت بتغيير مبلغ | Delivered with different amount (negotiated) | new_amount_iqd |
CUSTOMER_UNREACHABLE |
العميل غير متاح | Cannot contact customer | None |
WRONG_ADDRESS |
عنوان خاطئ | Address is incorrect or incomplete | None |
Best for: Simple integrations with fixed token
Admin Configuration Required: Contact your system administrator to configure static token authentication for your integration.
Authorization: Bearer {token}Best for: Secure integrations with token refresh
Admin Configuration Required: Contact your system administrator to configure LOGIN authentication with username/password for your integration.
shipment_id for matching - it's uniqueshipment_id in response, not receipt numberV2 Integration API includes an automatic retry mechanism with exponential backoff to ensure reliable delivery of shipments and status updates for ALL V2-compatible systems (Jenni, external systems, or any V2 integration) even when network issues occur.
When ANY V2 system sends requests (shipments or status updates), it automatically retries failed requests:
V2 systems will automatically retry these errors:
V2 systems will NOT retry these errors:
| Setting | Default Value | Description |
|---|---|---|
Max Attempts |
3 retries | Maximum number of immediate retry attempts |
Initial Delay |
1 second | Delay before first retry |
Backoff Strategy |
Exponential (2x) | Each retry waits longer: 1s → 2s → 4s |
Max Delay |
30 seconds | Maximum delay between retries |
Connection Timeout |
5 seconds | Maximum time to establish connection |
Read Timeout |
30 seconds | Maximum time to wait for response |
When immediate retries fail, Jenni Logistics saves the update to database for long-term retry:
| Setting | Value | Description |
|---|---|---|
Max Attempts |
3 attempts | Maximum number of persistent retry attempts |
First Retry |
15 minutes | First retry scheduled 15 minutes after failure |
Second Retry |
30 minutes | Second retry (if first fails) after 30 minutes |
Third Retry |
60 minutes | Final retry (if second fails) after 60 minutes |
Scheduled Job |
Every 5 minutes | Background job checks for pending retries |
Expiry |
24 hours | Updates older than 24 hours marked as expired |
Total Window |
~2 hours | All 3 attempts complete within ~2 hours |
15:30:00 | Attempt 1/3 - Sending 5 shipments to YOUR_SYSTEM
15:30:05 | ⚠️ Network timeout - Will retry in 1 second
15:30:06 | Attempt 2/3 - Retrying same request
15:30:08 | ✅ Success! All 5 shipments delivered
Total time: 8 seconds (transparent to user)
16:00:00 | Attempt 1/3 - Sending 10 shipments
16:00:02 | ⚠️ 503 Service Unavailable - Will retry in 1 second
16:00:03 | Attempt 2/3 - Retrying same request
16:00:05 | ⚠️ 503 Service Unavailable - Will retry in 2 seconds
16:00:07 | Attempt 3/3 - Retrying same request
16:00:09 | ✅ Success! All 10 shipments delivered
Total time: 9 seconds (all immediate retries exhausted but succeeded)
14:00:00 | Status Update: Shipment #12345 DELIVERED
14:00:01 | Immediate Retry 1/3: Connection refused
14:00:02 | Immediate Retry 2/3: Connection refused
14:00:04 | Immediate Retry 3/3: Connection refused
14:00:07 | ⚠️ All immediate retries failed
14:00:07 | ✅ Saved to persistent retry queue
14:00:07 | 📅 Scheduled for persistent retry at 14:15
--- 15 minutes later ---
14:15:00 | Persistent Retry 1/3: Attempt to send status update
14:15:01 | ⚠️ Connection refused - Will retry at 14:45
--- 30 minutes later ---
14:45:00 | Persistent Retry 2/3: Attempt to send status update
14:45:02 | ✅ Success! Status update delivered
Total persistent retry window: 45 minutes (your system recovered)
10:00:00 | Status Update: Shipment #67890 RETURNED
10:00:01 | Immediate retries failed (3/3)
10:00:01 | ✅ Saved to pending updates table
10:15:00 | Persistent Retry 1/3: Connection refused
10:45:00 | Persistent Retry 2/3: Connection refused
11:45:00 | Persistent Retry 3/3: Connection refused
11:45:01 | ❌ All persistent retries exhausted
11:45:01 | 📋 Status: FAILED (requires manual intervention)
11:45:01 | 🔔 Alert sent to administrator
Note: Update remains in database for 24 hours before marked as EXPIRED
17:00:00 | Attempt 1/3 - Sending 5 shipments
17:00:01 | ❌ 401 Unauthorized - No retry (fix authentication)
Total time: 1 second (immediate failure - authentication errors are NOT retried)
shipment_idTo handle potential duplicate requests safely, implement idempotency checking:
@PostMapping("/shipments/create")
public ResponseEntity<?> receiveShipments(@RequestBody ShipmentRequest request) {
List<Result> results = new ArrayList<>();
for (Shipment shipment : request.getShipments()) {
Long shipmentId = shipment.getShipment_id();
// ✅ CRITICAL: Check if already processed (using cache/redis)
if (isAlreadyProcessed(shipmentId)) {
log.info("Shipment {} already processed (duplicate request)", shipmentId);
results.add(Result.success(shipmentId, "ALREADY_PROCESSED"));
continue;
}
try {
// Process and save shipment to your database
String externalId = processAndSaveShipment(shipment);
// ✅ Mark as processed in cache (with 7-day TTL for cleanup)
markAsProcessed(shipmentId, externalId, Duration.ofDays(7));
results.add(Result.success(shipmentId, externalId));
} catch (Exception e) {
results.add(Result.failure(shipmentId, e.getMessage()));
}
}
return ResponseEntity.ok(results);
}
If your system has rate limits, return proper 429 response:
@PostMapping("/shipments/create")
public ResponseEntity<?> receiveShipments(@RequestBody ShipmentRequest request) {
// Check if rate limit exceeded for this system
String systemCode = request.getSystem_code();
if (isRateLimitExceeded(systemCode)) {
log.warn("Rate limit exceeded for system: {}", systemCode);
// Return 429 - Jenni Logistics will automatically retry with backoff
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.header("Retry-After", "60") // Suggest retry after 60 seconds
.body(Map.of(
"error", "RATE_LIMIT_EXCEEDED",
"message", "Too many requests. Please retry after 60 seconds.",
"retry_after_seconds", 60
));
}
// Process shipments normally
return processAllShipments(request);
}
| Issue | Symptom | Solution |
|---|---|---|
| Slow Response | Timeouts and retries | Optimize database queries, add caching, increase server capacity |
| High Duplicate Rate | Same shipments processed multiple times | Implement idempotency checking with cache |
| Frequent 5xx Errors | High retry rate | Fix server errors, add monitoring, increase stability |
| Auth Failures | Immediate failures, no retries | Check token validity, verify authentication configuration |
Authenticate user and receive JWT access token and refresh token.
{
"username": "john.doe",
"password": "yourSecurePassword123"
}
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Refresh your JWT token before expiration.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Include the token in the Authorization header as Bearer
{token} for all subsequent API calls.
Create shipments in bulk (maximum 100 shipments per request).
shipment_number AND external_shipment_id fieldsstore_id is optionalmerchant_id is required{
"system_code": "YOUR_COMPANY_REGISTERED_NAME",
"shipments": [
{
"shipment_number": "SHIP001",
"external_shipment_id": "12345",
"airway_bill_number": "AWB789",
"merchant_id": null,
"store_id": null,
"receiver_name": "Ahmed Ali",
"receiver_phone_1": "07901234567",
"receiver_phone_2": "07801234567",
"governorate_code": "BGD",
"city": "Al-Karrada",
"address": "Street 10, Building 5, Floor 2",
"amount_iqd": 50000.0,
"amount_usd": 0,
"quantity": 1,
"is_proof_of_delivery": true,
"is_fragile": false,
"have_return_item": false,
"is_special_case": false,
"product_info": "Mobile Phone - Samsung Galaxy",
"note": "Please call before delivery",
"receiver_latitude": 33.3152,
"receiver_longitude": 44.3661
}
]
}
{
"success": true,
"message": "Request processed with 2 accepted and 1 rejected",
"timestamp": "2024-01-15T10:30:00Z",
"accepted_shipments": [
{
"shipment_number": "SHIP001",
"shipment_id": 12345,
"airway_bill_number": "AWB789",
"status": "created",
"created_at": "2024-01-15T10:30:00Z"
},
{
"shipment_number": "SHIP002",
"shipment_id": 12346,
"airway_bill_number": "AWB790",
"status": "created",
"created_at": "2024-01-15T10:30:05Z"
}
],
"rejected_shipments": [
{
"shipment_number": "SHIP003",
"airway_bill_number": "AWB791",
"reason": "Duplicate shipment number already exists in the system",
"error_code": "DUPLICATE_SHIPMENT",
"field": "shipment_number"
}
],
"summary": {
"total_requested": 3,
"accepted_count": 2,
"rejected_count": 1
}
}
| Field | Type | Required | Description |
|---|---|---|---|
| system_code | String | Required | Your registered company/system name provided during registration |
| shipment_number | String | Required | Unique shipment tracking number (your internal tracking number) |
| external_shipment_id | String | Required |
⚠️ CRITICAL for tracking: Sender's original shipment ID.
Jenni Logistics will use this ID when sending status updates back to you.
Important Note: If you use your shipment ID as the tracking number,
you MUST provide the same value in both
shipment_number AND external_shipment_id fields.
This ensures proper bidirectional tracking when Jenni Logistics sends status updates to your system.
|
| airway_bill_number | String | Optional | External reference number (not used for search) |
| merchant_id | Integer | Required* | Merchant ID (required for AGGREGATOR users) |
| store_id | Integer | Optional | Store ID (optional for MERCHANT users) |
| receiver_name | String | Optional | Receiver full name |
| receiver_phone_1 | String | Required | Primary phone number (format: 07XXXXXXXXX) |
| receiver_phone_2 | String | Optional | Secondary phone number |
| sender_name | String | Optional | Sender/Merchant name |
| sender_phone | String | Optional | Sender contact phone number |
| governorate_code | String | Required | Governorate code (e.g., "BAG" for Baghdad) |
| city | String | Required | City/District name |
| address | String | Optional | Detailed address |
| amount_iqd | Double | Required | Amount in Iraqi Dinar |
| amount_usd | Double | Optional | Amount in USD (default: 0) |
| quantity | Integer | Optional | Number of items (default: 1) |
| proof_of_delivery | Boolean | Optional | Require proof of delivery (default: false) |
| fragile | Boolean | Optional | Is shipment fragile? (default: false) |
| have_return_item | Boolean | Optional | Indicates if the shipment contains return items (used for reverse logistics). Default: false |
| product_info | String | Optional | Product description |
| note | String | Optional | Special instructions or notes |
| receiver_latitude | Double | Optional | Receiver location latitude (GPS coordinates) |
| receiver_longitude | Double | Optional | Receiver location longitude (GPS coordinates) |
Use these standardized governorate codes in your API requests:
| Code | Governorate (EN) | Governorate (AR) |
|---|---|---|
ANB |
ANBAR | الأنبار |
ARB |
ERBIL | أربيل |
BAS |
BASRAH | البصرة |
BBL |
BABYLON | بابل |
BGD |
BAGHDAD | بغداد |
DHI |
DHI_QAR | ذي قار |
View complete list in the Governorate Codes Reference section below.
Purpose: Query shipments with three flexible modes - by specific IDs/Numbers, or paginated list of all user's shipments.
Security: Returns only shipments owned by the authenticated user (Master Customer, Delivery Agent, Pickup Agent, or Aggregator).
shipment_ids (Case IDs) - max 100shipment_numbers (tracking numbers) - max 100page & page_size (max 100 per page){
"shipment_ids": [12345, 12346, 12347]
}
{
"shipment_numbers": ["SHIP001", "SHIP002", "SHIP003"]
}
{
"page": 0,
"page_size": 20
}
{
"success": true,
"message": "Query completed successfully",
"timestamp": "2024-01-15T10:30:00Z",
"shipments": [
{
"shipment_id": 12345,
"shipment_number": "SHIP001",
"airway_bill_number": "AWB789",
"current_branch_name": "Main Branch",
"receiver_name": "Ahmed Ali",
"receiver_phone_1": "07901234567",
"governorate_name": "Baghdad",
"city": "Al-Karrada",
"amount_iqd": 50000.0,
"current_step": "IN_SC",
"current_step_ar": "داخل مركز الفرز",
"return_reason": null,
"return_reason_en": null,
"return_reason_ku": null,
"postponed_reason": null,
"postponed_reason_en": null,
"postponed_reason_ku": null,
"postponed_date": null,
"sender_merchant_id": 100,
"sender_merchant_name": "ABC Trading",
"sender_merchant_phone": "07701234567",
"sender_store_id": 3235,
"sender_store_name": "ABC Electronics Branch",
"sender_store_phone": "07801234567",
"delivery_agent_name": "Ali Hassan",
"delivery_agent_phone": "07901111111",
"pickup_agent_name": "Mohammed Karim",
"pickup_agent_phone": "07902222222",
"shipment_cost": 15000.0,
"is_fragile": false,
"have_return_item": false,
"is_partial_return": false,
"quantity_delivered": 1,
"quantity_returned": 0,
"partial_return_stage": null,
"partial_return_step": null,
"partial_return_step_ar": null,
"partial_return_note": null,
"image_url": "https://jenni.alzaeemexp.com/api/api/v1.0/case-image/get-image/img123.jpg",
"shipment_history": [
{
"history_id": 101,
"step_name": "RECEIVED",
"step_name_ar": "مستلم",
"branch_name": "Main Branch",
"enter_date": "2024-01-15 08:00:00",
"handler_name": "System",
"note": null
},
{
"history_id": 102,
"step_name": "IN_SC",
"step_name_ar": "داخل مركز الفرز",
"branch_name": "Main Branch",
"enter_date": "2024-01-15 09:00:00",
"handler_name": "John Doe",
"note": null
}
],
"partial_history": [
{
"history_id": 201,
"step_name": "PARTIAL_DELIVERY",
"step_name_ar": "تسليم جزئي",
"branch_name": "Main Branch",
"enter_date": "2024-01-15 11:00:00",
"handler_name": "Ali Hassan",
"note": "Partial delivery: 2 out of 3 items delivered"
}
],
"treatment_history": [
{
"treatment_date": "2024-01-15 09:15:00",
"treatment_type": "TREATED",
"treatment_description": "تم التواصل مع العميل وتأكيد الموعد",
"treated_by": "Call Center Agent"
}
]
},
{
"shipment_id": 12348,
"shipment_number": "SHIP004",
"receiver_name": "Sara Ahmed",
"receiver_phone_1": "07903456789",
"amount_iqd": 75000.0,
"is_fragile": false,
"have_return_item": true,
"current_step": "DELIVERED",
"current_step_ar": "سلمت",
"return_reason": null,
"return_reason_en": null,
"return_reason_ku": null,
"postponed_reason": null,
"postponed_reason_en": null,
"postponed_reason_ku": null,
"postponed_date": null,
"is_partial_return": true,
"quantity_delivered": 2,
"quantity_returned": 1,
"partial_return_stage": "RTO",
"partial_return_step": "RTO_IN_SC",
"partial_return_step_ar": "راجع في مركز الفرز",
"partial_return_note": "الجزء الراجع موجود في فرع: Baghdad Branch",
"sender_store_name": "Tech Store",
"sender_store_phone": "07801122334",
"delivery_agent_name": "Hussein Ali",
"delivery_agent_phone": "07905555555"
}
],
"not_found": ["SHIP002", "SHIP003"],
"pagination": null,
"summary": {
"total_requested": 4,
"found_count": 2,
"not_found_count": 2
}
}
For Paginated Mode (MODE 3), response includes pagination metadata:
{
"success": true,
"message": "Query completed successfully",
"timestamp": "2024-01-15T10:30:00Z",
"shipments": [...],
"not_found": [],
"pagination": {
"current_page": 0,
"page_size": 20,
"total_records": 150,
"total_pages": 8,
"has_next_page": true,
"has_previous_page": false
},
"summary": {
"total_requested": 20,
"found_count": 20,
"not_found_count": 0
}
}
not_found list for items not found or access denied
{
"success": true,
"message": "Query completed successfully",
"timestamp": "2025-11-02T10:00:00Z",
"shipments": [...], // Array of shipments
"not_found": [],
"pagination": {
"current_page": 0,
"page_size": 20,
"total_records": 150,
"total_pages": 8,
"has_next_page": true,
"has_previous_page": false
},
"summary": {
"total_requested": 20,
"found_count": 20,
"not_found_count": 0
}
}
shipment_history - All status changes (RECEIVED → IN_SC → OFD → DELIVERED)partial_history - Same structure as shipment_history, but specifically for partial delivery records (empty [] if no partial deliveries)treatment_history - Call center treatments and customer communications📌 Note: Both shipment_history and partial_history use identical data structure (ShipmentHistoryDto) with fields: history_id, step_code, step_name_ar, branch_name, enter_date, handler_name, note
is_partial_return - Boolean flag (true/false)quantity_delivered - Quantity successfully deliveredquantity_returned - Quantity returnedpartial_return_stage - Current stage of returned portion (mapped to global name)partial_return_step - Current step of returned portion (global name)partial_return_step_ar - Step name in Arabicpartial_return_note - Contains branch name where partial return is located (e.g., "الجزء الراجع موجود في فرع: Baghdad Branch")Update shipment status with various actions (postpone, return, delivered, etc.).
postponed_reason + postponed_date_id
(1=tomorrow, 2=in 2 days, 3=in 3 days)
return_reason +
optional return_quantity for partial returns
treated_message required (minimum 3 characters)new_amount_iqd or
new_amount_usd required
/v2/reference/postponed-reasons
and /v2/reference/return-reasons (but you can use any text)
{
"shipment_id": 12345,
"action": "POSTPONED",
"postponed_reason": "العميل غير موجود",
"postponed_reason_en": "Customer not available",
"postponed_reason_ku": "کڕیار بەردەست نییە",
"postponed_date_id": 2,
"note": "سيتم التواصل بعد يومين"
}
Example for RETURN action:
{
"shipment_id": 12346,
"action": "RETURNED_WITH_AGENT",
"return_reason": "رفض الاستلام",
"return_reason_en": "Customer refused",
"return_reason_ku": "کڕیار ڕەتکردەوە",
"note": "العميل رفض المنتج"
}
{
"success": true,
"message": "Shipment status updated successfully",
"timestamp": "2025-10-28T10:30:00Z",
"shipment_id": 12345,
"shipment_number": "SHIP001",
"action_performed": "POSTPONED",
"new_status": "POSTPONED",
"new_status_ar": "مؤجل",
"updated_at": "2025-10-28T10:30:00Z"
}
| Field | Type | Required | Description |
|---|---|---|---|
| shipment_number | String | Optional* | Shipment tracking number (use this OR shipment_id) |
| shipment_id | Long | Optional* | Shipment ID (use this OR shipment_number) |
| action | String | Required | Action code (POSTPONED, RETURN_TO_STORE, SUCCESSFUL_DELIVERY, etc.) - See Action Codes Reference |
| postponed_reason | String | Required* | Postponed reason TEXT (required if action=POSTPONED) |
| postponed_date_id | Integer | Required* | ✅ Required for POSTPONED action. Values:
|
| return_reason | String | Required* | Return reason TEXT (required if action=RETURN_TO_STORE or RETURNED_WITH_AGENT) |
| return_quantity | Integer | Optional | Optional - For PARTIAL RETURNS only. Specify number of items being returned (must be >= 1). If not provided, all items are returned. |
| treated_message | String | Required* | Treatment message (required if action=TREATED) |
| new_amount_iqd | Double | Optional | New amount in IQD (for amount change actions) |
| new_amount_usd | Double | Optional | New amount in USD (for amount change actions) |
| image_url | String | Optional | Proof image URL |
| note | String | Optional | Additional notes |
| Action Code | Description | Required Fields |
|---|---|---|
POSTPONED |
Postpone delivery | postponed_reason + postponed_date_id |
RETURN_TO_STORE |
Return to warehouse | return_reason |
RETURNED_WITH_AGENT |
Return with agent | return_reason |
SUCCESSFUL_DELIVERY |
Delivered successfully | None (Optional: image_url, note) |
SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE |
Delivered with amount change | new_amount_iqd or new_amount_usd |
PARTIAL_DELIVERY |
Partial delivery/exchange | Optional: return_quantity, new_amount_iqd |
TREATED |
Customer follow-up/treatment | treated_message (min 3 characters) |
PRICE_CHANGE_APPROVED |
Accept requested price changes | None |
| For complete list of all 40+ action codes, see Action Codes Reference section | ||
✅ You can use any text for
return_reason field.
📝 Recommended reasons (active in system):
هاتف الزبون مغلق
الزبون رفض الطلب
الزبون لم يطلب
الزبون لم يرد
سيستلم الطلب لاحقا من المحل
القياس ليس المطلوب
البضاعة ليست هي المطلوبة
الزبون مستلم سابقا
الزبون غلق الهاتف بعد الأتفاق
هاتف الزبون غير داخل بالخدمة
تحويل الى غير محافظة
الزبون لا يرد بعد الاتفاق
الزبون حاظر المندوب
رقم الزبون خطأ
الطلب ملغي
الطلب مكرر
راجع بسبب اختلاف السعر
الزبون لا يرد بعد سماع المكالمة
الزبون لا يرد ليومين
الزبون لا يرد بعد التأجيل
تحويل الى غير محطة
rtn_active=false) for admin
review.
✅ You can use any text for
postponed_reason field.
📝 Recommended reasons (active in system):
تم تغيير العنوان من قبل الزبون
تم التأجيل لرغبة الزبون
مسافر ولا يوجد من يستلم
تحويل الى غير منطقة
المتجر يريد تأجيل الطلب
مؤجل بسبب فحص الطلبيات
مؤجل ليلا لرغبة الزبون
مؤجل بالاتفاق مع الزبون
تحويل الى مندوب اخر
post_active=false) for admin
review.
{
"shipment_number": "SHIP002",
"action": "RETURN_TO_STORE",
"return_reason": "العنوان خاطئ",
"note": "العميل أعطى عنوان خاطئ"
}
{
"shipment_number": "SHIP003",
"action": "TREATED",
"treated_message": "العميل سيتواصل معنا غداً لاستلام الشحنة"
}
{
"shipment_number": "SHIP004",
"action": "SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE",
"new_amount_iqd": 45000,
"note": "العميل دفع 45000 بدلاً من 50000"
}
Reference data includes governorates, cities, return reasons, postponed reasons, and action codes. Download these lists once and cache them locally for validation and dropdown menus in your application.
Get list of all active governorates. No authentication required (public geographic data).
# Get JSON
GET https://jenni.alzaeemexp.com/api/v2/reference/governorates
# Download Excel
GET https://jenni.alzaeemexp.com/api/v2/reference/governorates?download=excel
# Download JSON
GET https://jenni.alzaeemexp.com/api/v2/reference/governorates?download=json
{
"success": true,
"message": "Governorates retrieved successfully",
"timestamp": "2025-10-30T10:00:00Z",
"data": [
{
"code": "BGD",
"name_en": "Baghdad",
"name_ar": "بغداد"
},
{
"code": "BAS",
"name_en": "Basra",
"name_ar": "البصرة"
}
]
}
Get list of cities/districts with pagination. Filter by governorate code (optional). No authentication required (public geographic data).
| Parameter | Type | Required | Description |
|---|---|---|---|
| governorate_code | String | Optional | Filter cities by governorate (e.g., "BGD") |
| page | Integer | Optional | Page number (default: 1) |
| size | Integer | Optional | Page size (default: 20, max: 100) |
| search | String | Optional | Search city name (partial match) |
| download | String | Optional | Download format: "excel" or "json" |
# All cities (paginated)
GET https://jenni.alzaeemexp.com/api/v2/reference/cities?page=1&size=50
# Cities for Baghdad only
GET https://jenni.alzaeemexp.com/api/v2/reference/cities?governorate_code=BGD
# Search cities
GET https://jenni.alzaeemexp.com/api/v2/reference/cities?search=الكرادة
# Download all cities (Excel)
GET https://jenni.alzaeemexp.com/api/v2/reference/cities?download=excel
Get list of active return reasons (actual text to use in update-status API). Requires authentication.
{
"success": true,
"message": "Return reasons retrieved successfully",
"timestamp": "2025-10-30T10:00:00Z",
"data_type": "return_reasons",
"total_count": 3,
"data": [
{
"text": "العنوان خاطئ",
"active": true,
"return_reason_en": "Wrong address",
"return_reason_ku": "ناونیشان هەڵەیە"
},
{
"text": "العميل غير موجود",
"active": true,
"return_reason_en": "Customer not available",
"return_reason_ku": "کڕیار بەردەست نییە"
},
{
"text": "رفض الاستلام",
"active": true,
"return_reason_en": "Customer refused",
"return_reason_ku": "کڕیار ڕەتکردەوە"
}
]
}
text value directly.
"return_reason": "العنوان خاطئ"
Get list of active postponed reasons (actual text to use in update-status API). Requires authentication.
{
"success": true,
"message": "Postponed reasons retrieved successfully",
"timestamp": "2025-10-30T10:00:00Z",
"data_type": "postponed_reasons",
"total_count": 2,
"data": [
{
"text": "العميل غير موجود",
"active": true,
"postponed_reason_en": "Customer not available",
"postponed_reason_ku": "کڕیار بەردەست نییە"
},
{
"text": "العميل مشغول",
"active": true,
"postponed_reason_en": "Customer busy",
"postponed_reason_ku": "کڕیار سەرقاڵە"
}
]
}
text value directly.
"postponed_reason": "العميل غير موجود"
Get actions that are currently available for a specific shipment (based on its current status).
| Parameter | Type | Required | Description |
|---|---|---|---|
| shipment_number | String | Optional* | Shipment tracking number (use this OR shipment_id) |
| shipment_id | Long | Optional* | Shipment ID (use this OR shipment_number) |
GET https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=SHIP001
Headers: Authorization: Bearer {token}
{
"success": true,
"message": "Available actions retrieved successfully for MASTERCUSTOMER",
"timestamp": "2025-10-30T10:00:00Z",
"data": [
{
"action_code": "POSTPONED",
"action_name_en": "Postponed by customer request",
"action_name_ar": "مؤجل",
"description": "Postpone delivery to another date",
"required_fields": ["postponed_reason", "postponed_date_id"]
},
{
"action_code": "RETURN_TO_STORE",
"action_name_en": "Return to warehouse",
"action_name_ar": "راجع للمخزن",
"description": "Return shipment to warehouse",
"required_fields": ["return_reason"]
},
{
"action_code": "SUCCESSFUL_DELIVERY",
"action_name_en": "Successfully delivered",
"action_name_ar": "تسليم بنجاح",
"description": "Mark shipment as delivered",
"required_fields": []
}
],
"metadata": {
"shipment_id": 12345,
"shipment_number": "SHIP001",
"current_step": "OFD",
"current_step_ar": "قيد التوصيل",
"current_step_description": "Out for delivery - Agent on the way",
"current_stage": "WITH_AGENT",
"current_stage_ar": "عند المندوب",
"user_rank": "MASTERCUSTOMER"
}
}
The metadata object provides context about the shipment and current user (all values are mapped names, not internal codes):
shipment_id: Unique shipment ID (most accurate identifier)shipment_number: Receipt number (can be duplicated)current_step: Abbreviated step name (e.g., OFD, IN_SC, DELIVERED)current_step_ar: Arabic step name (e.g., قيد التوصيل)current_step_description: Full English description of current stepcurrent_stage: Stage name (SORTING_CENTER, RTO, DELIVERED, INTER_BRANCH_TRANSFER, WITH_AGENT, NEW_CUSTOMER_SHIPMENTS)current_stage_ar: Arabic stage name (المخزن، الراجع، الواصل، النقل بين الفروع، عند المندوب، شحنات جديدة عند العميل)user_rank: Your account type (MASTERCUSTOMER, DLVAGENT, PICKUPAGENT, AGGREGATOR, etc.)required_fields/v2/shipments/update-status| Reference Data | API Endpoint | Download Options | Use Case |
|---|---|---|---|
|
Governorates
Public |
/v2/reference/governorates |
Validate governorate_code before creating shipments |
|
|
Cities
Public |
/v2/reference/cities |
Validate city name and get city ID |
|
|
Return Reasons
Auth Required |
/v2/reference/return-reasons |
|
Show dropdown in UI for return actions |
|
Postponed Reasons
Auth Required |
/v2/reference/postponed-reasons |
|
Show dropdown in UI for postpone actions |
|
Action Codes
Public |
/v2/reference/action-codes |
Complete list of all possible actions with descriptions |
// Fetch governorates
async function loadGovernorates() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/reference/governorates');
const data = await response.json();
const select = document.getElementById('governorate-select');
data.data.forEach(gov => {
const option = document.createElement('option');
option.value = gov.code;
option.text = `${gov.name_ar} (${gov.name_en})`;
select.appendChild(option);
});
}
// Fetch cities based on selected governorate
async function loadCities(governorateCode) {
const response = await fetch(
`https://jenni.alzaeemexp.com/api/v2/reference/cities?governorate_code=${governorateCode}&size=100`
);
const data = await response.json();
const select = document.getElementById('city-select');
select.innerHTML = ''; // Clear existing
data.data.forEach(city => {
const option = document.createElement('option');
option.value = city.name_ar; // Use Arabic name for API
option.text = city.name_ar;
select.appendChild(option);
});
}
<!-- Governorate Dropdown -->
<label>Governorate:</label>
<select id="governorate-select" onchange="loadCities(this.value)">
<option value="">-- Select Governorate --</option>
<!-- Options loaded from API -->
</select>
<!-- City Dropdown -->
<label>City:</label>
<select id="city-select">
<option value="">-- Select City --</option>
<!-- Options loaded based on governorate -->
</select>
These APIs allow Master Customers to manage their stores (branches).
Each store belongs to a master customer and can be used when creating shipments with store_id.
Purpose: Retrieve list of all stores belonging to the authenticated master customer.
GET https://jenni.alzaeemexp.com/api/v2/merchants/my-stores
Authorization: Bearer {token}
{
"success": true,
"message": "Stores retrieved successfully",
"timestamp": "2025-11-02T10:00:00Z",
"data": [
{
"store_id": 101,
"store_name": "متجر الإلكترونيات - فرع الكرادة",
"store_phone": "07901234567",
"governorate_code": "BGD",
"address": "شارع الرشيد، بناية 10"
},
{
"store_id": 102,
"store_name": "متجر الإلكترونيات - فرع الجادرية",
"store_phone": "07801234567",
"governorate_code": "BGD",
"address": "شارع الجادرية، مجمع 5"
}
]
}
Purpose: Create a new store under the current master customer. The system automatically generates a random password for the store.
Important:
Save the generated_password from the response! This is the only time it will be provided.
The password is auto-generated by the system and cannot be retrieved later.
POST /v2/stores/create
Content-Type: application/json
Authorization: Bearer {token}
{
"store_name": "متجر الإلكترونيات - فرع المنصور",
"store_phone": "07901234567",
"governorate_code": "BGD",
"address": "شارع المنصور، مجمع 15، الطابق الأرضي",
"latitude": 33.3152,
"longitude": 44.3661
}
| Field | Type | Required | Description |
|---|---|---|---|
store_name |
String | ✅ Yes | Name of the store/branch |
store_phone |
String | ❌ No | Store contact phone (defaults to master customer phone) |
governorate_code |
String | ❌ No | Governorate code (e.g., BGD, BAS, NIN) - defaults to master customer's governorate |
address |
String | ❌ No | Full address of the store |
latitude |
Double | ❌ No | GPS latitude coordinate |
longitude |
Double | ❌ No | GPS longitude coordinate |
{
"success": true,
"message": "Store created successfully",
"timestamp": "2025-11-02T10:30:00Z",
"store_id": 103,
"store_name": "متجر الإلكترونيات - فرع المنصور",
"generated_password": "Xy9Kp2Lm"
}
Response Fields:
store_id: Unique store ID (use this when creating shipments)store_name: Name of the created storegenerated_password: Auto-generated password (save this!)store_id when creating shipments to specify which store the shipment belongs togovernorate_code must be a valid code from /v2/reference/governoratesPOST/PUT requests to UPDATE statusSUCCESSFUL_DELIVERY - the action of marking as deliveredPOST /v2/shipments/update-statusGET responses (READ-ONLY)DELIVERED - the current status after deliveryGET /v2/shipments/query
📌 Key Insight: Think of it like a door:
Action Code = OPEN (the action you do) →
Status = OPENED (the resulting state)
Some codes can be both! For example: POSTPONED can be an action (postponing) AND a status (currently postponed).
Action codes are the operations you can perform on a shipment (e.g., Postpone, Return, Deliver). Each shipment has specific actions available based on its current status.
💡 Tip: Use /v2/reference/available-actions?shipment_number=XXX
to get the exact actions allowed for a specific shipment.
Get complete list of all professional action codes with descriptions in English and Arabic.
{
"success": true,
"message": "Action codes retrieved successfully",
"timestamp": "2025-10-30T10:00:00Z",
"data": [
{
"action_code": "POSTPONED",
"action_name_en": "Postponed by customer request",
"action_name_ar": "مؤجل",
"description": "Postpone delivery to another date",
"required_fields": ["postponed_reason", "postponed_date_id"]
},
{
"action_code": "RETURN_TO_STORE",
"action_name_en": "Return to warehouse",
"action_name_ar": "راجع للمخزن",
"description": "Return shipment to warehouse",
"required_fields": ["return_reason"]
},
{
"action_code": "SUCCESSFUL_DELIVERY",
"action_name_en": "Successfully delivered",
"action_name_ar": "تسليم بنجاح",
"description": "Mark shipment as delivered",
"required_fields": []
}
]
}
This table combines all essential information: what the action does, what fields it requires, and what status it results in.
| Action Code | Arabic Name | Description | Required Fields | Results in Status |
|---|---|---|---|---|
SUCCESSFUL_DELIVERY |
تسليم بنجاح | Successfully delivered to recipient | - | DELIVERED |
SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE |
تسليم بنجاح مع تغيير مبلغ الوصل | Delivered with amount adjustment | new_amount_iqd or new_amount_usd |
DELIVERED_PRICE_CHANGED |
PARTIAL_DELIVERY |
تسليم جزئي أو استبدال | Partial delivery or exchange | return_quantity (optional) |
PARTIALLY_DELIVERED |
POSTPONED |
مؤجل | Customer requested postponement | postponed_reason, postponed_date_id |
POSTPONED |
RETURN_TO_STORE |
راجع للمخزن | Return shipment to warehouse | return_reason |
RTO_WH |
RETURNED_WITH_AGENT |
راجع عند المندوب | Shipment returned, with agent | return_reason |
RTO_WITH_DA |
ASSIGN_TO_AGENT |
إسناد لمندوب | Assign to delivery agent | agent_id |
OFD |
DELIVERY_REATTEMPT |
إعادة محاولة التوصيل | Schedule delivery retry | note (optional) |
DELIVERY_REATTEMPT |
TREATED |
تم معالجة الطلب | Issue has been handled | treated_message |
No Status Change |
PRICE_CHANGE_APPROVED |
موافقة تغيير السعر | Approve price change request | - | OFD |
MOVE_TO_AGENT |
نقل إلى مندوب | Move to delivery agent stage | agent_id |
OFD |
CHANGE_AGENT |
تغيير المندوب | Transfer to another agent | new_agent_id |
OFD |
/v2/reference/available-actions?shipment_number=XXX to check which actions are currently allowed.Returns only the actions that are currently allowed for a specific shipment.
| Parameter | Type | Required | Description |
|---|---|---|---|
| shipment_number | String | Optional* | Shipment tracking number (use this OR shipment_id) |
| shipment_id | Long | Optional* | Shipment ID (use this OR shipment_number) |
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=SHIP001" \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
{
"success": true,
"message": "Available actions retrieved successfully for DLVAGENT",
"timestamp": "2025-10-30T10:00:00Z",
"data": [
{
"action_code": "POSTPONED",
"action_name_en": "Postponed by customer request",
"action_name_ar": "مؤجل",
"description": "Postpone delivery to another date",
"required_fields": ["postponed_reason", "postponed_date_id"]
},
{
"action_code": "RETURN_TO_STORE",
"action_name_en": "Return to warehouse",
"action_name_ar": "راجع للمخزن",
"description": "Return shipment to warehouse",
"required_fields": ["return_reason"]
},
{
"action_code": "SUCCESSFUL_DELIVERY",
"action_name_en": "Successfully delivered",
"action_name_ar": "تسليم بنجاح",
"description": "Mark shipment as delivered",
"required_fields": []
}
],
"metadata": {
"shipment_id": 12345,
"shipment_number": "SHIP001",
"current_step": "OFD",
"current_step_ar": "قيد التوصيل",
"current_step_description": "Out for delivery - Agent on the way",
"current_stage": "WITH_AGENT",
"current_stage_ar": "عند المندوب",
"user_rank": "DLVAGENT"
}
}
All values in metadata are mapped names (not internal codes):
current_step: Abbreviated step name (e.g., OFD, IN_SC, DELIVERED)current_step_ar: Arabic step name (e.g., قيد التوصيل)current_step_description: Full English descriptioncurrent_stage: Stage name (SORTING_CENTER, RTO, DELIVERED, WITH_AGENT, etc.)current_stage_ar: Arabic stage name
Step 1: Query /v2/reference/available-actions for the shipment
Step 2: Present only the available actions to your user
Step 3: Submit the chosen action with required fields to /v2/shipments/update-status
POST /v2/shipments/update-status with action_code: "ASSIGN_TO_AGENT"
Action: ASSIGN_TO_AGENT
POST /v2/shipments/update-status with action_code: "SUCCESSFUL_DELIVERY"
Action: SUCCESSFUL_DELIVERY
Key Takeaway: You send Action Codes to UPDATE the shipment, and you receive Status codes when you QUERY the shipment. The table above shows which status results from each action.
✅ All step codes now use shortened abbreviations for better performance and readability.
✅ See Abbreviations Guide for complete reference.
✅ Old names still work for backward compatibility (auto-converted).
V2 Integration uses standardized, abbreviated step names for efficiency. Each step includes both English abbreviated code and Arabic translation.
Total: 26 step statuses mapped to abbreviated standard names. View complete list in the complete status table or Abbreviations Guide.
All V2 API responses follow a consistent, standardized format for easy parsing and error handling.
| Field | Type | Always Present | Description |
|---|---|---|---|
success |
Boolean | ✅ Yes | true if operation succeeded, false otherwise |
message |
String | ✅ Yes | Human-readable message describing the result |
timestamp |
String (ISO 8601) | ✅ Yes | Server timestamp when response was generated |
error_code |
String | ❌ Only on error | Machine-readable error code for programmatic handling |
{
"success": true,
"message": "Request processed with 2 accepted and 1 rejected",
"timestamp": "2025-10-30T10:30:00Z",
"accepted_shipments": [
{
"shipment_number": "SHIP001",
"shipment_id": 12345, // ✅ Save this for tracking!
"airway_bill_number": "AWB789",
"status": "created",
"created_at": "2025-10-30T10:30:00Z"
}
],
"rejected_shipments": [
{
"shipment_number": "SHIP002",
"airway_bill_number": "AWB790",
"reason": "Duplicate shipment number", // Human-readable
"error_code": "DUPLICATE_SHIPMENT", // Machine-readable
"field": "shipment_number" // Which field caused the error
},
{
"shipment_number": "SHIP003",
"airway_bill_number": "AWB791",
"reason": "Governorate mapping not found. Invalid governorate_code: XYZ. Please use valid codes from /v2/reference/governorates",
"error_code": "PROCESSING_ERROR", // Machine-readable
"field": null // No specific field error
}
],
"summary": {
"total_requested": 3,
"accepted_count": 1,
"rejected_count": 2
}
}
shipment_id to your database for future queriesreason to user, log error_code for debugging{
"success": true,
"message": "Query completed successfully",
"timestamp": "2025-10-30T10:30:00Z",
"shipments": [
{
// === Basic Information ===
"shipment_id": 12345,
"shipment_number": "SHIP001",
"airway_bill_number": "AWB789",
// === Current Status ===
"current_branch_name": "Main Branch",
"current_step": "OFD", // ✅ Abbreviated global name
"current_step_ar": "قيد التوصيل", // ✅ Arabic translation
// === Receiver Information ===
"receiver_name": "Ahmed Ali",
"receiver_phone_1": "07901234567",
"receiver_phone_2": "07801234567",
"governorate_name": "Baghdad",
"governorate_code": "BGD",
"city": "Al-Karrada",
"precise_address": "Baghdad-Al-Karrada-Street 10",
"address": "Street 10, Building 5",
// === Financial Information ===
"amount_iqd": 50000.0,
"amount_usd": 0,
"shipment_cost": 5000.0, // Delivery charge
"quantity": 1,
// === Flags ===
"special_case": false, // ✅ Boolean
"proof_of_delivery": true, // ✅ Boolean
"fragile": false, // ✅ Boolean
// === Additional Info ===
"product_info": "Samsung Galaxy S24",
"note": "Please call before delivery",
"return_reason": null, // Null if not returned
"return_reason_en": null, // English description (if returned)
"return_reason_ku": null, // Kurdish description (if returned)
"postponed_reason": null, // Null if not postponed
"postponed_reason_en": null, // English description (if postponed)
"postponed_reason_ku": null, // Kurdish description (if postponed)
"postponed_date": null, // Date when shipment is postponed to (ISO 8601 format, null if not postponed)
// === Sender Information ===
"sender_merchant_name": "ABC Trading",
"sender_merchant_phone": "07901111111",
"sender_store_name": "ABC Store 1",
"sender_store_phone": "07901111111",
// === Delivery Agent (if assigned) ===
"delivery_agent_name": "Ali Hassan",
"delivery_agent_phone": "07902222222",
// === Pickup Agent (if applicable) ===
"pickup_agent_name": null,
"pickup_agent_phone": null,
// === Settlement Information ===
"merchant_settlement_id": 0, // 0 = not settled yet
"merchant_settlement_date": null,
"merchant_settlement_accountant": null,
// === Timestamps ===
"created_at": "2025-10-30T08:00:00Z",
"created_by": "John Doe",
// === Image ===
"image_url": "https://domain.com/images/proof-123.jpg"
}
],
"not_found": ["SHIP002", "SHIP003"],
"summary": {
"total_requested": 3,
"found_count": 1,
"not_found_count": 2
}
}
null if not applicable (e.g., delivery_agent_name before assignment)true/false, never "Y"/"N" stringsYYYY-MM-DDTHH:mm:ssZ{
"success": true,
"message": "Shipment status updated successfully",
"timestamp": "2025-10-30T10:30:00Z",
"shipment_id": 12345,
"shipment_number": "SHIP001",
"action_performed": "POSTPONED", // Action that was executed
"new_status": "POSTPONED", // New step status
"new_status_ar": "مؤجل", // Arabic translation
"updated_at": "2025-10-30T10:30:00Z"
}
{
"success": false,
"message": "Validation failed", // General error message
"timestamp": "2025-10-30T10:30:00Z",
"error_code": "INVALID_PHONE_NUMBER", // ✅ Machine-readable code
"field": "receiver_phone_1", // ✅ Which field has error
"details": "Phone must match format: 07XXXXXXXXX" // Additional context
}
// Parse error response
if (!response.success) {
switch(response.error_code) {
case 'DUPLICATE_SHIPMENT':
showError(`Shipment ${response.field} already exists`);
break;
case 'INVALID_PHONE_NUMBER':
highlightField(response.field); // Highlight receiver_phone_1
showError(response.message);
break;
case 'INVALID_GOVERNORATE':
showGovernorateSelector(); // Show dropdown to choose valid one
break;
case 'PROCESSING_ERROR':
showError(response.reason || response.message); // Show detailed reason
if (response.reason?.includes('/v2/reference/governorates')) {
redirectToReferencePage(); // Guide user to valid codes
}
break;
default:
showError(response.message);
}
}
| HTTP Code | Meaning | When It Happens | What To Do |
|---|---|---|---|
200 OK |
Success | Request processed successfully (check success field for partial failures) |
Parse response data |
400 Bad Request |
Invalid Request | Malformed JSON, validation error, missing required fields | Check error_code and field, fix request |
401 Unauthorized |
Authentication Failed | Invalid/expired token, wrong credentials | Login again or refresh token |
403 Forbidden |
Access Denied | You don't have permission to access this resource | Check ownership or contact support |
404 Not Found |
Resource Not Found | Shipment or resource doesn't exist | Verify shipment_number or ID is correct |
500 Internal Server Error |
Server Error | Unexpected server-side error | Contact support with request details |
| Field Type | JSON Type | Example | Notes |
|---|---|---|---|
| Shipment Number | String | "SHIP001" |
Can contain letters, numbers, hyphens |
| ID Fields | Number (Integer/Long) | 12345 |
No quotes, numeric only |
| Amount Fields | Number (Double) | 50000.0 or 50000 |
Decimal allowed, no currency symbol |
| Boolean Flags | Boolean | true or false |
Not "Y"/"N" or "true"/"false" strings |
| Dates | String (ISO 8601) | "2025-10-30T10:30:00Z" |
UTC timezone, includes time |
| Null Values | null | null |
Not "null" string or empty string |
if (fragile == "true") → ✅ if (fragile === true)"shipment_id": "12345" → ✅ "shipment_id": 12345agent.name crashes → ✅ agent?.name || "Not assigned"// Parse create response
const response = await fetch(url, options);
const result = await response.json();
if (result.success) {
// Process accepted shipments
result.accepted_shipments.forEach(shipment => {
console.log(`✅ ${shipment.shipment_number}: ID=${shipment.shipment_id}`);
saveToDatabase(shipment.shipment_number, shipment.shipment_id);
});
// Handle rejections
result.rejected_shipments.forEach(rejected => {
console.error(`❌ ${rejected.shipment_number}: ${rejected.reason}`);
showUserError(rejected.shipment_number, rejected.reason);
});
// Show summary
console.log(`📊 Total: ${result.summary.total_requested}, ` +
`✅ Accepted: ${result.summary.accepted_count}, ` +
`❌ Rejected: ${result.summary.rejected_count}`);
} else {
console.error('Request failed:', result.message);
}
import requests
response = requests.post(url, json=data, headers=headers)
result = response.json()
if result['success']:
# Process accepted
for shipment in result['accepted_shipments']:
print(f"✅ {shipment['shipment_number']}: ID={shipment['shipment_id']}")
save_to_db(shipment['shipment_number'], shipment['shipment_id'])
# Handle rejected
for rejected in result['rejected_shipments']:
print(f"❌ {rejected['shipment_number']}: {rejected['reason']}")
log_error(rejected['error_code'], rejected['field'])
# Summary
summary = result['summary']
print(f"📊 Accepted: {summary['accepted_count']}/{summary['total_requested']}")
else:
print(f"Error: {result['message']}")
Not all fields are always present in query response. Here's when each field is populated:
| Field | Always Present | Condition |
|---|---|---|
shipment_id |
✅ Yes | - |
delivery_agent_name |
❌ No | Only if agent assigned |
pickup_agent_name |
❌ No | Only if pickup agent assigned |
return_reason |
❌ No | Only if shipment returned |
return_reason_en |
❌ No | Only if shipment returned (English description) |
return_reason_ku |
❌ No | Only if shipment returned (Kurdish description) |
postponed_reason |
❌ No | Only if shipment postponed |
postponed_reason_en |
❌ No | Only if shipment postponed (English description) |
postponed_reason_ku |
❌ No | Only if shipment postponed (Kurdish description) |
postponed_date |
❌ No | Only if shipment postponed (ISO 8601 date format, e.g., "2025-01-16T00:00:00Z") |
merchant_settlement_id |
✅ Yes | 0 if not settled, number if settled |
merchant_settlement_date |
❌ No | Only if settled (null otherwise) |
image_url |
❌ No | Only if delivery proof uploaded |
current_step |
✅ Yes | Always has value (global name) |
current_step_ar |
✅ Yes | Always has Arabic translation |
// JavaScript - Safe access
const agentName = shipment.delivery_agent_name || "Not assigned yet";
const settlementDate = shipment.merchant_settlement_date || "Not settled";
const returnReason = shipment.return_reason || "N/A";
const returnReasonEn = shipment.return_reason_en || null;
const returnReasonKu = shipment.return_reason_ku || null;
const postponedReason = shipment.postponed_reason || null;
const postponedReasonEn = shipment.postponed_reason_en || null;
const postponedReasonKu = shipment.postponed_reason_ku || null;
const postponedDate = shipment.postponed_date || null;
// Optional chaining (modern JS)
const agentPhone = shipment?.delivery_agent_phone ?? "N/A";
// Python - Safe access
agent_name = shipment.get('delivery_agent_name') or 'Not assigned'
settlement_date = shipment.get('merchant_settlement_date', 'Not settled')
| API | Max Items per Response | Pagination | Notes |
|---|---|---|---|
| Create Shipments | 100 shipments | ❌ No | Split large batches into multiple requests |
| Query Shipments (Specific Lookup) | 100 IDs/Numbers | ❌ No | Split queries into chunks of 100 |
| Query Shipments (Paginated) | 100 per page | ✅ Yes | Use page & page_size parameters to navigate |
| Orders in Process | 100 per page | ✅ Yes | Use page and size parameters |
| Cities List | 100 per page | ✅ Yes | Default: 20 per page |
| Governorates | All (no limit) | ❌ No | Small dataset (~20 items) |
Code examples are organized into 9 main categories for easier navigation across 34 APIs:
Choose from 5 popular languages - all examples include authentication, error handling, and comments.
Command-line examples for all APIs. Expand categories to see individual APIs.
curl -X POST "https://jenni.alzaeemexp.com/api/v2/auth/login" \
-H "Content-Type: application/json" \
-d '{
"username": "your_username",
"password": "your_password"
}'
curl -X POST "https://jenni.alzaeemexp.com/api/v2/auth/refresh" \
-H "Authorization: Bearer YOUR_REFRESH_TOKEN"
curl -X POST "https://jenni.alzaeemexp.com/api/v2/shipments/create" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"system_code": "YOUR_COMPANY_CODE",
"shipments": [
{
"shipment_number": "SHIP001",
"external_shipment_id": "12345",
"receiver_name": "أحمد علي",
"receiver_phone_1": "07901234567",
"governorate_code": "BGD",
"city": "الكرادة",
"address": "Street 10, Building 5, Floor 2",
"amount_iqd": 50000,
"amount_usd": 0,
"quantity": 1,
"is_proof_of_delivery": true,
"is_fragile": false,
"have_return_item": false,
"is_special_case": false,
"product_info": "Mobile Phone - Samsung Galaxy",
"note": "Please call before delivery"
}
]
}'
Three Query Modes:
curl -X POST "https://jenni.alzaeemexp.com/api/v2/shipments/query" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"shipment_ids": [12345, 12346, 12347]
}'
curl -X POST "https://jenni.alzaeemexp.com/api/v2/shipments/query" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"shipment_numbers": ["SHIP001", "SHIP002", "SHIP003"]
}'
curl -X POST "https://jenni.alzaeemexp.com/api/v2/shipments/query" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"page": 0,
"page_size": 20
}'
curl -X POST "https://jenni.alzaeemexp.com/api/v2/shipments/update-status" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"shipment_id": 12345,
"action": "POSTPONED",
"postponed_reason": "العميل غير موجود",
"postponed_reason_en": "Customer not available",
"postponed_reason_ku": "کڕیار بەردەست نییە",
"postponed_date_id": 1
}'
curl -X PUT "https://jenni.alzaeemexp.com/api/v2/shipments/edit" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"shipment_id": 12345,
"receiver_phone_1": "07901234567",
"amount_iqd": 60000,
"address": "Updated address"
}'
curl -X POST "https://jenni.alzaeemexp.com/api/v2/shipments/stickers" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"shipment_numbers": ["SHIP001", "SHIP002"],
"width_mm": 100,
"height_mm": 150
}' \
--output stickers.pdf
curl -X DELETE "https://jenni.alzaeemexp.com/api/v2/orders/12345" \
-H "Authorization: Bearer YOUR_TOKEN"
# Get JSON
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/governorates"
# Download Excel
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/governorates?download=excel" --output governorates.xlsx
# Download JSON
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/governorates?download=json" --output governorates.json
# All cities (paginated)
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/cities?page=1&size=50"
# Cities for Baghdad only
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/cities?governorate_code=BGD"
# Search cities
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/cities?search=الكرادة"
# Download Excel
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/cities?download=excel" --output cities.xlsx
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/return-reasons" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/postponed-reasons" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=SHIP001" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/action-codes"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/countries"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/statistics/orders-by-status" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/orders/in-process?page=1&size=20" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/orders/by-step?step_code=OFD&page=1&size=20" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X DELETE "https://jenni.alzaeemexp.com/api/v2/orders/12345" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/merchants/my-stores" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X POST "https://jenni.alzaeemexp.com/api/v2/stores/create" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"store_name": "متجر الإلكترونيات",
"store_phone": "07901234567",
"governorate_code": "BGD",
"address": "شارع الرشيد، بناية 10",
"latitude": 33.3152,
"longitude": 44.3661
}'
curl -X POST "https://jenni.alzaeemexp.com/api/v2/merchant-management/create" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"merchant_name": "New Merchant",
"phone": "07901234567",
"system_code": "NEWMERCH001"
}'
curl -X PUT "https://jenni.alzaeemexp.com/api/v2/merchant-management/update" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"merchant_id": 123,
"merchant_name": "Updated Name",
"phone": "07901234567"
}'
curl -X POST "https://jenni.alzaeemexp.com/api/v2/tickets/create" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"message": "لدي مشكلة في التوصيل",
"ticket_cause_name": "Delivery Issue",
"ticket_department_name": "Operations"
}'
curl -X GET "https://jenni.alzaeemexp.com/api/v2/tickets/by-user?page=1&size=20" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X POST "https://jenni.alzaeemexp.com/api/v2/tickets/search" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"page": 1,
"size": 20
}'
curl -X GET "https://jenni.alzaeemexp.com/api/v2/tickets/statuses" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/tickets/departments" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/tickets/causes" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/tickets/550e8400-e29b-41d4-a716-446655440000/history" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/tickets/initial-status" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X POST "https://jenni.alzaeemexp.com/api/v2/tickets/550e8400-e29b-41d4-a716-446655440000/attachment" \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@/path/to/attachment.pdf"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/payments/123" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/payments/settled?page=1&size=20" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/returns/456" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X GET "https://jenni.alzaeemexp.com/api/v2/returns/settled?page=1&size=20" \
-H "Authorization: Bearer YOUR_TOKEN"
YOUR_DOMAIN/v2/shipments/create when configured in integration settings.
# Example: What Jenni Logistics sends to YOUR server
curl -X POST "https://yourstore.com/v2/shipments/create" \
-H "Content-Type: application/json" \
-d '{
"system_code": "YOUR_SYSTEM_CODE",
"auth_method": "TOKEN",
"static_token": "your-secret-token",
"shipments": [
{
"shipment_id": 12345,
"shipment_id": 12345,
"receiver_name": "أحمد علي محمد",
"receiver_phone_1": "07901234567",
"governorate_code": "BGD",
"city_name": "الكرادة",
"address": "شارع الرشيد، بناية 10",
"amount_iqd": 50000.0,
"quantity": 2,
"note": "يفضل الاتصال قبل التوصيل",
"sender_name": "متجر ABC",
"sender_phone": "07801234567"
}
]
}'
# Example: Partial Delivery with Return
curl -X POST "https://jenni.alzaeemexp.com/api/v2/push/update-status" \
-H "Content-Type: application/json" \
-d '{
"system_code": "YOUR_SYSTEM_CODE",
"auth_method": "TOKEN",
"static_token": "your-secret-token",
"updates": [
{
"shipment_id": 12345,
"action_code": "PARTIAL_DELIVERY",
"is_partial": true,
"quantity_delivered": 3,
"quantity_returned": 2,
"partial_has_returned": true,
"partial_return_action": "RETURN_TO_STORE",
"proof_image_url": "https://cdn.example.com/proof.jpg",
"received_by_name": "أحمد علي"
}
]
}'
Total APIs: 36 APIs across 10 categories
Modern async/await implementation using fetch API. Expand categories to see individual APIs.
// Login and get JWT token
async function login(username, password) {
try {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Save tokens
localStorage.setItem('accessToken', data.token);
localStorage.setItem('refreshToken', data.refreshToken);
console.log('✅ Login successful');
return data;
} catch (error) {
console.error('❌ Login failed:', error);
throw error;
}
}
// Refresh JWT token
async function refreshToken() {
const refreshToken = localStorage.getItem('refreshToken');
try {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/auth/refresh', {
method: 'POST',
headers: {
'Authorization': `Bearer ${refreshToken}`
}
});
const data = await response.json();
// Update tokens
localStorage.setItem('accessToken', data.token);
localStorage.setItem('refreshToken', data.refreshToken);
return data;
} catch (error) {
console.error('❌ Token refresh failed:', error);
throw error;
}
}
// Create shipments
async function createShipments(shipments) {
const token = localStorage.getItem('accessToken');
try {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
system_code: 'YOUR_COMPANY_CODE',
shipments: shipments
})
});
const data = await response.json();
if (data.success) {
console.log(`✅ Accepted: ${data.summary.accepted_count}`);
console.log(`❌ Rejected: ${data.summary.rejected_count}`);
// Process accepted
data.accepted_shipments.forEach(ship => {
console.log(`✅ ${ship.shipment_number}: ID=${ship.shipment_id}`);
});
// Handle rejected
data.rejected_shipments.forEach(ship => {
console.error(`❌ ${ship.shipment_number}: ${ship.reason}`);
});
}
return data;
} catch (error) {
console.error('Error creating shipments:', error);
throw error;
}
}
// Example usage
const shipments = [
{
shipment_number: 'SHIP001',
external_shipment_id: '12345', // ✅ REQUIRED: Your internal order ID (STRING - can be any format)
receiver_name: 'أحمد علي',
receiver_phone_1: '07901234567',
governorate_code: 'BGD',
city: 'الكرادة',
amount_iqd: 50000,
amount_usd: 0,
quantity: 1,
is_proof_of_delivery: true,
is_fragile: false,
have_return_item: false,
is_special_case: false,
product_info: 'Mobile Phone - Samsung Galaxy',
note: 'Please call before delivery'
}
];
createShipments(shipments);
Three Query Modes:
async function queryByIds(shipmentIds) {
const token = localStorage.getItem('accessToken');
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ shipment_ids: shipmentIds })
});
const data = await response.json();
console.log(`Found: ${data.summary.found_count}`);
return data;
}
// Example
queryByIds([12345, 12346, 12347]);
async function queryByNumbers(shipmentNumbers) {
const token = localStorage.getItem('accessToken');
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ shipment_numbers: shipmentNumbers })
});
const data = await response.json();
data.shipments.forEach(ship => {
console.log(`📦 ${ship.shipment_number}: ${ship.current_step_ar}`);
});
return data;
}
// Example
queryByNumbers(['SHIP001', 'SHIP002']);
async function getAllShipmentsPaginated(page = 0, pageSize = 20) {
const token = localStorage.getItem('accessToken');
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
page: page,
page_size: pageSize
})
});
const data = await response.json();
// Pagination info
console.log(`Page ${data.pagination.current_page + 1} of ${data.pagination.total_pages}`);
console.log(`Total Records: ${data.pagination.total_records}`);
console.log(`Has Next: ${data.pagination.has_next_page}`);
return data;
}
// Example: Get first page
getAllShipmentsPaginated(0, 20);
// Example: Get next page
getAllShipmentsPaginated(1, 20);
// Update shipment status
async function updateStatus(shipmentNumber, action, extraFields = {}) {
const token = localStorage.getItem('accessToken');
const requestBody = {
shipment_number: shipmentNumber,
action: action,
...extraFields
};
try {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/update-status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(requestBody)
});
const data = await response.json();
if (data.success) {
console.log(`✅ ${data.message}`);
console.log(`New status: ${data.new_status_ar}`);
}
return data;
} catch (error) {
console.error('Error updating status:', error);
throw error;
}
}
// Example: Postpone
updateStatus('SHIP001', 'POSTPONED', {
postponed_reason: 'العميل غير موجود',
postponed_reason_en: 'Customer not available',
postponed_reason_ku: 'کڕیار بەردەست نییە',
postponed_date_id: 1
});
// Example: Return to Store
updateStatus('SHIP001', 'RETURN_TO_STORE', {
return_reason: 'العنوان خاطئ',
return_reason_en: 'Wrong address',
return_reason_ku: 'ناونیشان هەڵەیە'
});
// Example: Successful Delivery
updateStatus('SHIP001', 'SUCCESSFUL_DELIVERY');
// Edit shipment information
async function editShipment(shipmentId, updates) {
const token = localStorage.getItem('accessToken');
try {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/edit', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
shipment_id: shipmentId,
...updates
})
});
const data = await response.json();
console.log('✅ Shipment updated:', data);
return data;
} catch (error) {
console.error('Error editing shipment:', error);
throw error;
}
}
// Example
editShipment(12345, {
receiver_phone_1: '07901234567',
amount_iqd: 60000,
address: 'Updated address'
});
// Generate stickers PDF
async function generateStickers(shipmentNumbers, width, height) {
const token = localStorage.getItem('accessToken');
try {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/stickers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
shipment_numbers: shipmentNumbers,
width_mm: width,
height_mm: height
})
});
// Download PDF
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'stickers.pdf';
a.click();
console.log('✅ Stickers downloaded');
} catch (error) {
console.error('Error generating stickers:', error);
throw error;
}
}
// Example
generateStickers(['SHIP001', 'SHIP002'], 100, 150);
// Delete shipment by ID
async function deleteShipment(shipmentId) {
const token = localStorage.getItem('accessToken');
if (!confirm(`Delete shipment ${shipmentId}?`)) return;
try {
const response = await fetch(`https://jenni.alzaeemexp.com/api/v2/orders/${shipmentId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
console.log('✅ Shipment deleted:', data);
return data;
} catch (error) {
console.error('Error deleting shipment:', error);
throw error;
}
}
// Get governorates list - NO AUTH REQUIRED
async function getGovernorates() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/reference/governorates');
const data = await response.json();
data.data.forEach(gov => {
console.log(`${gov.code}: ${gov.name_ar} (${gov.name_en})`);
});
return data.data;
}
// Get cities - NO AUTH REQUIRED
async function getCities(governorateCode = null) {
const url = governorateCode
? `https://jenni.alzaeemexp.com/api/v2/reference/cities?governorate_code=${governorateCode}`
: 'https://jenni.alzaeemexp.com/api/v2/reference/cities';
const response = await fetch(url);
const data = await response.json();
return data.data;
}
// Example: Get all Baghdad cities
const baghdadCities = await getCities('BGD');
// Get return reasons - AUTH REQUIRED
async function getReturnReasons(token) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/reference/return-reasons', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
return data.data;
}
// Get postponed reasons - AUTH REQUIRED
async function getPostponedReasons(token) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/reference/postponed-reasons', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
return data.data;
}
// Get all action codes - NO AUTH REQUIRED
async function getActionCodes() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/reference/action-codes');
const data = await response.json();
return data.data;
}
// Get all countries
async function getCountries() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/reference/countries');
const data = await response.json();
return data.data;
}
// Get available actions for specific shipment
async function getAvailableActions(shipmentNumber) {
const token = localStorage.getItem('accessToken');
const response = await fetch(
`https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=${shipmentNumber}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
const data = await response.json();
return data.data;
}
// Example
const actions = await getAvailableActions('SHIP001');
console.log('Available actions:', actions);
// Get all action codes
async function getActionCodes() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/reference/action-codes');
const data = await response.json();
return data.data;
}
// Get order statistics by status
async function getOrderStatistics() {
const token = localStorage.getItem('accessToken');
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/statistics/orders-by-status', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
data.statistics.forEach(stat => {
console.log(`${stat.status_name_ar}: ${stat.count}`);
});
return data.statistics;
}
// Get orders in process (with pagination)
async function getOrdersInProcess(page = 1, size = 20) {
const token = localStorage.getItem('accessToken');
const response = await fetch(
`https://jenni.alzaeemexp.com/api/v2/orders/in-process?page=${page}&size=${size}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
const data = await response.json();
console.log(`Page ${data.pagination.current_page} of ${data.pagination.total_pages}`);
console.log(`Total orders: ${data.pagination.total_elements}`);
return data.orders;
}
// Get orders filtered by step
async function getOrdersByStep(stepCode, page = 1, size = 20) {
const token = localStorage.getItem('accessToken');
const response = await fetch(
`https://jenni.alzaeemexp.com/api/v2/orders/by-step?step_code=${stepCode}&page=${page}&size=${size}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
const data = await response.json();
return data.orders;
}
// Example: Get all orders "out for delivery"
const ordersOnWay = await getOrdersByStep('OFD');
// Delete shipment by ID (only in initial stages: SORTING_CENTER, NEW_CUSTOMER_SHIPMENTS and initial steps: NEW_ORDER_TO_PRINT, NEW_ORDER_TO_PICKUP, NEW_WITH_PA)
async function deleteShipment(shipmentId) {
const token = localStorage.getItem('accessToken');
const response = await fetch(
`https://jenni.alzaeemexp.com/api/v2/orders/${shipmentId}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
}
);
return await response.json();
}
// Get merchant stores - MASTER CUSTOMER ONLY
async function getMyStores() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/merchants/my-stores', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
return data.data;
}
// Create new store
async function createStore(storeName, phone, governorateCode, address) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/stores/create', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
store_name: storeName,
store_phone: phone,
governorate_code: governorateCode,
address: address,
latitude: 33.3152,
longitude: 44.3661
})
});
const data = await response.json();
console.log('Store created:', data.store_id);
console.log('Generated password:', data.generated_password);
return data;
}
// Create merchant - AGGREGATOR ROLE ONLY
async function createMerchant(merchantData) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/merchant-management/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
merchant_name: merchantData.name,
phone: merchantData.phone,
system_code: merchantData.code
})
});
return await response.json();
}
// Update merchant - AGGREGATOR ROLE ONLY
async function updateMerchant(merchantId, updates) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/merchant-management/update', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
merchant_id: merchantId,
...updates
})
});
return await response.json();
}
// Create support ticket
async function createTicket(ticketData) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/tickets/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': \`Bearer \${token}\`
},
body: JSON.stringify({
message: ticketData.message,
ticket_cause_name: ticketData.cause,
ticket_department_name: ticketData.department
})
});
return await response.json();
}
// Get user tickets
async function getMyTickets(page = 1, size = 20) {
const response = await fetch(
\`https://jenni.alzaeemexp.com/api/v2/tickets/by-user?page=\${page}&size=\${size}\`,
{
headers: {
'Authorization': \`Bearer \${token}\`
}
}
);
return await response.json();
}
// Search tickets with filters
async function searchTickets(filters) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/tickets/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': \`Bearer \${token}\`
},
body: JSON.stringify(filters)
});
return await response.json();
}
// Get ticket statuses
async function getTicketStatuses() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/tickets/statuses', {
headers: {
'Authorization': \`Bearer \${token}\`
}
});
return await response.json();
}
// Get ticket departments
async function getTicketDepartments() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/tickets/departments', {
headers: {
'Authorization': \`Bearer \${token}\`
}
});
return await response.json();
}
// Get ticket causes
async function getTicketCauses() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/tickets/causes', {
headers: {
'Authorization': \`Bearer \${token}\`
}
});
return await response.json();
}
// Get ticket history
async function getTicketHistory(ticketId) {
const response = await fetch(
\`https://jenni.alzaeemexp.com/api/v2/tickets/\${ticketId}/history\`,
{
headers: {
'Authorization': \`Bearer \${token}\`
}
}
);
return await response.json();
}
// Get initial ticket status
async function getInitialTicketStatus() {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/tickets/initial-status', {
headers: {
'Authorization': \`Bearer \${token}\`
}
});
return await response.json();
}
// Upload ticket attachment
async function uploadTicketAttachment(ticketId, file) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(
\`https://jenni.alzaeemexp.com/api/v2/tickets/\${ticketId}/attachment\`,
{
method: 'POST',
headers: {
'Authorization': \`Bearer \${token}\`
},
body: formData
}
);
return await response.json();
}
// Get payment details
async function getPaymentInfo(paymentId) {
const response = await fetch(
\`https://jenni.alzaeemexp.com/api/v2/payments/\${paymentId}\`,
{
headers: {
'Authorization': \`Bearer \${token}\`
}
}
);
return await response.json();
}
// Get settled payments
async function getSettledPayments(page = 1, size = 20) {
const response = await fetch(
\`https://jenni.alzaeemexp.com/api/v2/payments/settled?page=\${page}&size=\${size}\`,
{
headers: {
'Authorization': \`Bearer \${token}\`
}
}
);
return await response.json();
}
// Get return manifest details
async function getReturnInfo(returnId) {
const response = await fetch(
\`https://jenni.alzaeemexp.com/api/v2/returns/\${returnId}\`,
{
headers: {
'Authorization': \`Bearer \${token}\`
}
}
);
return await response.json();
}
// Get settled returns
async function getSettledReturns(page = 1, size = 20) {
const response = await fetch(
\`https://jenni.alzaeemexp.com/api/v2/returns/settled?page=\${page}&size=\${size}\`,
{
headers: {
'Authorization': \`Bearer \${token}\`
}
}
);
return await response.json();
}
Total: 34 APIs organized in 9 categories for easy navigation
Using requests library with proper error handling. Expand categories to see individual APIs.
import requests
BASE_URL = "https://jenni.alzaeemexp.com/api"
def login(username, password):
"""Login and get JWT token"""
response = requests.post(
f"{BASE_URL}/v2/auth/login",
json={
"username": username,
"password": password
}
)
if response.status_code == 200:
data = response.json()
print("✅ Login successful")
return data['token'], data['refreshToken']
else:
raise Exception(f"Login failed: {response.text}")
# Usage
token, refresh_token = login("your_username", "your_password")
def refresh_token(refresh_token):
"""Refresh JWT token"""
response = requests.post(
f"{BASE_URL}/v2/auth/refresh",
headers={
"Authorization": f"Bearer {refresh_token}"
}
)
data = response.json()
return data['token'], data['refreshToken']
def create_shipments(token, shipments):
"""Create shipments in bulk (max 100)"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
data = {
"system_code": "YOUR_COMPANY_CODE",
"shipments": shipments
}
response = requests.post(
f"{BASE_URL}/v2/shipments/create",
headers=headers,
json=data
)
result = response.json()
print(f"✅ Accepted: {result['summary']['accepted_count']}")
print(f"❌ Rejected: {result['summary']['rejected_count']}")
return result
# Example
shipments = [
{
"shipment_number": "SHIP001",
"external_shipment_id": "12345", # ✅ REQUIRED: Your internal order ID
"receiver_name": "أحمد علي",
"receiver_phone_1": "07901234567",
"governorate_code": "BGD",
"city": "الكرادة",
"amount_iqd": 50000,
"amount_usd": 0,
"quantity": 1,
"is_proof_of_delivery": True,
"is_fragile": False,
"have_return_item": False,
"is_special_case": False,
"product_info": "Mobile Phone - Samsung Galaxy",
"note": "Please call before delivery"
}
]
result = create_shipments(token, shipments)
Three Query Modes:
# MODE 1: Query by IDs
def query_by_ids(token, shipment_ids):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
response = requests.post(
f"{BASE_URL}/v2/shipments/query",
headers=headers,
json={"shipment_ids": shipment_ids}
)
return response.json()
# MODE 2: Query by Numbers
def query_by_numbers(token, shipment_numbers):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
response = requests.post(
f"{BASE_URL}/v2/shipments/query",
headers=headers,
json={"shipment_numbers": shipment_numbers}
)
return response.json()
# MODE 3: Get All User's Shipments (Paginated)
def get_all_shipments_paginated(token, page=0, page_size=20):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
response = requests.post(
f"{BASE_URL}/v2/shipments/query",
headers=headers,
json={"page": page, "page_size": page_size}
)
data = response.json()
# Pagination info
print(f"Page {data['pagination']['current_page'] + 1} of {data['pagination']['total_pages']}")
print(f"Total Records: {data['pagination']['total_records']}")
return data
# Examples
query_by_ids(token, [12345, 12346])
query_by_numbers(token, ['SHIP001', 'SHIP002'])
get_all_shipments_paginated(token, 0, 20)
def update_status(token, shipment_number, action, **kwargs):
"""Update shipment status"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
data = {
"shipment_number": shipment_number,
"action": action,
**kwargs
}
response = requests.post(
f"{BASE_URL}/v2/shipments/update-status",
headers=headers,
json=data
)
result = response.json()
if result['success']:
print(f"✅ {result['message']}")
print(f"New status: {result['new_status_ar']}")
return result
# Example: Postpone
update_status(token, 'SHIP001', 'POSTPONED',
postponed_reason='العميل غير موجود',
postponed_reason_en='Customer not available',
postponed_reason_ku='کڕیار بەردەست نییە',
postponed_date_id=1
)
# Example: Return to Store
update_status(token, 'SHIP001', 'RETURN_TO_STORE',
return_reason='العنوان خاطئ',
return_reason_en='Wrong address',
return_reason_ku='ناونیشان هەڵەیە'
)
# Example: Successful Delivery
update_status(token, 'SHIP001', 'SUCCESSFUL_DELIVERY')
def get_governorates():
"""Get all governorates - NO AUTH REQUIRED"""
response = requests.get(f"{BASE_URL}/v2/reference/governorates")
data = response.json()
return data['data']
# Cache governorates
governorates = get_governorates()
for gov in governorates:
print(f"{gov['code']}: {gov['name_ar']}")
def get_cities(governorate_code=None):
"""Get cities - NO AUTH REQUIRED"""
params = {}
if governorate_code:
params['governorate_code'] = governorate_code
response = requests.get(
f"{BASE_URL}/v2/reference/cities",
params=params
)
data = response.json()
return data['data']
# Get all Baghdad cities
baghdad_cities = get_cities('BGD')
def get_return_reasons(token):
"""Get return reasons - AUTH REQUIRED"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/reference/return-reasons",
headers=headers
)
data = response.json()
return data['data']
# Example
reasons = get_return_reasons(token)
for reason in reasons:
print(f"{reason['code']}: {reason['description']}")
def get_postponed_reasons(token):
"""Get postponed reasons - AUTH REQUIRED"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/reference/postponed-reasons",
headers=headers
)
data = response.json()
return data['data']
# Example
reasons = get_postponed_reasons(token)
for reason in reasons:
print(f"{reason['code']}: {reason['description']}")
def get_action_codes():
"""Get all action codes - NO AUTH REQUIRED"""
response = requests.get(f"{BASE_URL}/v2/reference/action-codes")
data = response.json()
return data['data']
# Example
actions = get_action_codes()
for action in actions:
print(f"{action['code']}: {action['name_en']} ({action['name_ar']})")
def get_countries():
"""Get all countries - NO AUTH REQUIRED"""
response = requests.get(f"{BASE_URL}/v2/reference/countries")
data = response.json()
return data['data']
# Example
countries = get_countries()
for country in countries:
print(f"{country['code']}: {country['name']}")
def get_order_statistics(token):
"""Get order count grouped by status"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/statistics/orders-by-status",
headers=headers
)
data = response.json()
for stat in data['statistics']:
print(f"{stat['status_name_ar']}: {stat['count']}")
return data['statistics']
# Example
stats = get_order_statistics(token)
def get_orders_in_process(token, page=1, size=20):
"""Get orders in process with pagination"""
headers = {
"Authorization": f"Bearer {token}"
}
params = {
"page": page,
"size": size
}
response = requests.get(
f"{BASE_URL}/v2/orders/in-process",
headers=headers,
params=params
)
data = response.json()
print(f"Page {data['pagination']['current_page']} of {data['pagination']['total_pages']}")
print(f"Total: {data['pagination']['total_elements']} orders")
return data['orders']
def get_orders_in_process(token, page=1, size=20):
"""Get orders in process"""
headers = {
"Authorization": f"Bearer {token}"
}
params = {
"page": page,
"size": size
}
response = requests.get(
f"{BASE_URL}/v2/orders/in-process",
headers=headers,
params=params
)
return response.json()
def get_orders_by_step(token, step_code, page=1, size=20):
"""Get orders filtered by step"""
headers = {
"Authorization": f"Bearer {token}"
}
params = {
"step_code": step_code,
"page": page,
"size": size
}
response = requests.get(
f"{BASE_URL}/v2/orders/by-step",
headers=headers,
params=params
)
return response.json()
def delete_order(token, shipment_id):
"""Delete shipment by ID (only in initial stages: SORTING_CENTER, NEW_CUSTOMER_SHIPMENTS and initial steps: NEW_ORDER_TO_PRINT, NEW_ORDER_TO_PICKUP, NEW_WITH_PA)"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.delete(
f"{BASE_URL}/v2/orders/{shipment_id}",
headers=headers
)
return response.json()
def get_my_stores(token):
"""Get merchant stores - MASTER CUSTOMER ONLY"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/merchants/my-stores",
headers=headers
)
data = response.json()
return data['data']
def create_store(token, store_name, phone, governorate_code, address):
"""Create new store under current master customer"""
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
data = {
"store_name": store_name,
"store_phone": phone,
"governorate_code": governorate_code,
"address": address,
"latitude": 33.3152,
"longitude": 44.3661
}
response = requests.post(
f"{BASE_URL}/v2/stores/create",
headers=headers,
json=data
)
return response.json()
def create_merchant(token, merchant_data):
"""Create merchant - AGGREGATOR ROLE ONLY"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
payload = {
"merchant_name": merchant_data['name'],
"phone": merchant_data['phone'],
"system_code": merchant_data['code']
}
response = requests.post(
f"{BASE_URL}/v2/merchant-management/create",
headers=headers,
json=payload
)
return response.json()
def update_merchant(token, merchant_id, updates):
"""Update merchant - AGGREGATOR ROLE ONLY"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
payload = {
"merchant_id": merchant_id,
**updates
}
response = requests.put(
f"{BASE_URL}/v2/merchant-management/update",
headers=headers,
json=payload
)
return response.json()
def create_ticket(token, ticket_data):
"""Create support ticket"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
payload = {
"message": ticket_data['message'],
"ticket_cause_name": ticket_data['cause'],
"ticket_department_name": ticket_data['department']
}
response = requests.post(
f"{BASE_URL}/v2/tickets/create",
headers=headers,
json=payload
)
return response.json()
def get_my_tickets(token, page=1, size=20):
"""Get user tickets"""
headers = {
"Authorization": f"Bearer {token}"
}
params = {
"page": page,
"size": size
}
response = requests.get(
f"{BASE_URL}/v2/tickets/by-user",
headers=headers,
params=params
)
return response.json()
def search_tickets(token, filters):
"""Search tickets with filters"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
response = requests.post(
f"{BASE_URL}/v2/tickets/search",
headers=headers,
json=filters
)
return response.json()
def get_ticket_statuses(token):
"""Get ticket statuses"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/tickets/statuses",
headers=headers
)
return response.json()
def get_ticket_departments(token):
"""Get ticket departments"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/tickets/departments",
headers=headers
)
return response.json()
def get_ticket_causes(token):
"""Get ticket causes"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/tickets/causes",
headers=headers
)
return response.json()
def get_ticket_history(token, ticket_id):
"""Get ticket history"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/tickets/{ticket_id}/history",
headers=headers
)
return response.json()
def get_initial_ticket_status(token):
"""Get initial ticket status"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/tickets/initial-status",
headers=headers
)
return response.json()
def upload_ticket_attachment(token, ticket_id, file_path):
"""Upload attachment to ticket"""
headers = {
"Authorization": f"Bearer {token}"
}
files = {
'file': open(file_path, 'rb')
}
response = requests.post(
f"{BASE_URL}/v2/tickets/{ticket_id}/attachment",
headers=headers,
files=files
)
return response.json()
def get_payment_info(token, payment_id):
"""Get payment details"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/payments/{payment_id}",
headers=headers
)
return response.json()
def get_settled_payments(token, page=1, size=20):
"""Get settled payments"""
headers = {
"Authorization": f"Bearer {token}"
}
params = {
"page": page,
"size": size
}
response = requests.get(
f"{BASE_URL}/v2/payments/settled",
headers=headers,
params=params
)
return response.json()
def get_return_info(token, return_id):
"""Get return manifest details"""
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(
f"{BASE_URL}/v2/returns/{return_id}",
headers=headers
)
return response.json()
def get_settled_returns(token, page=1, size=20):
"""Get settled returns"""
headers = {
"Authorization": f"Bearer {token}"
}
params = {
"page": page,
"size": size
}
response = requests.get(
f"{BASE_URL}/v2/returns/settled",
headers=headers,
params=params
)
return response.json()
Total: 34 APIs organized in 9 categories for easy navigation
pip install requestsSpring Boot RestTemplate implementation. Expand categories to see individual APIs.
Complete examples using Spring's RestTemplate with proper dependency injection and error handling.
@Service
@RequiredArgsConstructor
public class V2IntegrationService {
private final RestTemplate restTemplate;
private static final String BASE_URL = "https://jenni.alzaeemexp.com/api";
public JwtResponse login(String username, String password) {
String url = BASE_URL + "/v2/auth/login";
LoginRequest request = new LoginRequest();
request.setUsername(username);
request.setPassword(password);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<LoginRequest> entity = new HttpEntity<>(request, headers);
ResponseEntity<JwtResponse> response = restTemplate.postForEntity(
url, entity, JwtResponse.class
);
return response.getBody();
}
}
// DTO Classes
@Data
class LoginRequest {
private String username;
private String password;
}
@Data
class JwtResponse {
private String token;
private String refreshToken;
}
// Example: Create ShipmentDto with all important fields
ShipmentDto shipment = ShipmentDto.builder()
.shipmentNumber("SHIP001")
.externalShipmentId("12345") // ✅ REQUIRED: Your internal order ID
.receiverName("أحمد علي")
.receiverPhone1("07901234567")
.governorateCode("BGD")
.city("الكرادة")
.address("Street 10, Building 5, Floor 2")
.amountIqd(50000.0)
.amountUsd(0.0)
.quantity(1)
.isProofOfDelivery(true)
.isFragile(false)
.haveReturnItem(false)
.isSpecialCase(false)
.productInfo("Mobile Phone - Samsung Galaxy")
.note("Please call before delivery")
.build();
List<ShipmentDto> shipments = List.of(shipment);
// Create request
public ShipmentCreateResponseDto createShipments(
String token,
List<ShipmentDto> shipments) {
String url = BASE_URL + "/v2/shipments/create";
ShipmentCreateRequestDto request = new ShipmentCreateRequestDto();
request.setSystemCode("YOUR_COMPANY_CODE");
request.setShipments(shipments);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<ShipmentCreateRequestDto> entity =
new HttpEntity<>(request, headers);
ResponseEntity<ShipmentCreateResponseDto> response =
restTemplate.postForEntity(url, entity, ShipmentCreateResponseDto.class);
ShipmentCreateResponseDto result = response.getBody();
log.info("✅ Accepted: {}", result.getSummary().getAcceptedCount());
log.info("❌ Rejected: {}", result.getSummary().getRejectedCount());
return result;
}
Three Query Modes:
// MODE 1: Query by IDs
public ShipmentQueryResponseDto queryByIds(String token, List<Long> shipmentIds) {
String url = BASE_URL + "/v2/shipments/query";
ShipmentQueryRequestDto request = new ShipmentQueryRequestDto();
request.setShipmentIds(shipmentIds);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<ShipmentQueryRequestDto> entity =
new HttpEntity<>(request, headers);
ResponseEntity<ShipmentQueryResponseDto> response =
restTemplate.postForEntity(url, entity, ShipmentQueryResponseDto.class);
return response.getBody();
}
// MODE 2: Query by Numbers
public ShipmentQueryResponseDto queryByNumbers(String token, List<String> shipmentNumbers) {
String url = BASE_URL + "/v2/shipments/query";
ShipmentQueryRequestDto request = new ShipmentQueryRequestDto();
request.setShipmentNumbers(shipmentNumbers);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<ShipmentQueryRequestDto> entity =
new HttpEntity<>(request, headers);
ResponseEntity<ShipmentQueryResponseDto> response =
restTemplate.postForEntity(url, entity, ShipmentQueryResponseDto.class);
return response.getBody();
}
// MODE 3: Get All User's Shipments (Paginated)
public ShipmentQueryResponseDto getAllShipmentsPaginated(
String token, Integer page, Integer pageSize) {
String url = BASE_URL + "/v2/shipments/query";
ShipmentQueryRequestDto request = new ShipmentQueryRequestDto();
request.setPage(page);
request.setPageSize(pageSize);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<ShipmentQueryRequestDto> entity =
new HttpEntity<>(request, headers);
ResponseEntity<ShipmentQueryResponseDto> response =
restTemplate.postForEntity(url, entity, ShipmentQueryResponseDto.class);
ShipmentQueryResponseDto result = response.getBody();
// Pagination info
System.out.println("Page " + (result.getPagination().getCurrentPage() + 1) +
" of " + result.getPagination().getTotalPages());
System.out.println("Total Records: " + result.getPagination().getTotalRecords());
return result;
}
// Examples
queryByIds(token, Arrays.asList(12345L, 12346L));
queryByNumbers(token, Arrays.asList("SHIP001", "SHIP002"));
getAllShipmentsPaginated(token, 0, 20);
public List<GovernorateDto> getGovernorates() {
// NO AUTH REQUIRED - Public geographic data
String url = BASE_URL + "/v2/reference/governorates";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<GovernorateResponseDto> response =
restTemplate.exchange(url, HttpMethod.GET, entity,
GovernorateResponseDto.class);
return response.getBody().getData();
}
@Data
class GovernorateDto {
private String code;
private String nameAr;
private String nameEn;
}
public List<CityDto> getCities(String governorateCode) {
// NO AUTH REQUIRED - Public geographic data
String url = BASE_URL + "/v2/reference/cities";
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
if (governorateCode != null) {
builder.queryParam("governorate_code", governorateCode);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<CityResponseDto> response =
restTemplate.exchange(builder.toUriString(), HttpMethod.GET,
entity, CityResponseDto.class);
return response.getBody().getData();
}
public List<ReasonDto> getReturnReasons(String token) {
// AUTH REQUIRED - Business policies
String url = BASE_URL + "/v2/reference/return-reasons";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<ReasonResponseDto> response =
restTemplate.exchange(url, HttpMethod.GET, entity,
ReasonResponseDto.class);
return response.getBody().getData();
}
@Data
class ReasonDto {
private String code;
private String description;
private Boolean active;
}
public List<ReasonDto> getPostponedReasons(String token) {
// AUTH REQUIRED - Business policies
String url = BASE_URL + "/v2/reference/postponed-reasons";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<ReasonResponseDto> response =
restTemplate.exchange(url, HttpMethod.GET, entity,
ReasonResponseDto.class);
return response.getBody().getData();
}
public List<ActionCodeDto> getActionCodes() {
// NO AUTH REQUIRED - Public reference
String url = BASE_URL + "/v2/reference/action-codes";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<ActionCodeResponseDto> response =
restTemplate.exchange(url, HttpMethod.GET, entity,
ActionCodeResponseDto.class);
return response.getBody().getData();
}
@Data
class ActionCodeDto {
private String code;
private String nameEn;
private String nameAr;
}
public List<CountryDto> getCountries() {
String url = BASE_URL + "/v2/reference/countries";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<ReferenceDataResponseDto> response =
restTemplate.exchange(url, HttpMethod.GET, entity,
ReferenceDataResponseDto.class);
return response.getBody().getData();
}
public StatisticsDto getOrderStatistics(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<StatisticsDto> response = restTemplate.exchange(
BASE_URL + "/v2/statistics/orders-by-status",
HttpMethod.GET,
entity,
StatisticsDto.class
);
return response.getBody();
}
public OrdersInProcessDto getOrdersInProcess(String token, int page, int size) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(BASE_URL + "/v2/orders/in-process")
.queryParam("page", page)
.queryParam("size", size);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<OrdersInProcessDto> response = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
entity,
OrdersInProcessDto.class
);
return response.getBody();
}
public OrdersInProcessDto getOrdersByStep(String token, String stepCode, int page, int size) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(BASE_URL + "/v2/orders/by-step")
.queryParam("step_code", stepCode)
.queryParam("page", page)
.queryParam("size", size);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<OrdersInProcessDto> response = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
entity,
OrdersInProcessDto.class
);
return response.getBody();
}
// Example: Get all orders "out for delivery"
OrdersInProcessDto ordersOfd = getOrdersByStep(token, "OFD", 1, 20);
public Object deleteOrder(String token, Long shipmentId) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<Object> response = restTemplate.exchange(
BASE_URL + "/v2/orders/" + shipmentId,
HttpMethod.DELETE,
entity,
Object.class
);
return response.getBody();
}
public List<StoreDto> getMyStores(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<ReferenceDataResponseDto> response = restTemplate.exchange(
BASE_URL + "/v2/merchants/my-stores",
HttpMethod.GET,
entity,
ReferenceDataResponseDto.class
);
return response.getBody().getData();
}
public Object createStore(String token, String storeName, String phone, String governorateCode, String address) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("store_name", storeName);
requestBody.put("store_phone", phone);
requestBody.put("governorate_code", governorateCode);
requestBody.put("address", address);
requestBody.put("latitude", 33.3152);
requestBody.put("longitude", 44.3661);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Object> response = restTemplate.exchange(
BASE_URL + "/v2/stores/create",
HttpMethod.POST,
entity,
Object.class
);
return response.getBody();
}
public Object createMerchant(String token, MerchantCreateRequestDto request) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<MerchantCreateRequestDto> entity = new HttpEntity<>(request, headers);
ResponseEntity<Object> response = restTemplate.postForEntity(
BASE_URL + "/v2/merchant-management/create",
entity,
Object.class
);
return response.getBody();
}
public Object updateMerchant(String token, MerchantUpdateRequestDto request) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<MerchantUpdateRequestDto> entity = new HttpEntity<>(request, headers);
ResponseEntity<Object> response = restTemplate.exchange(
BASE_URL + "/v2/merchant-management/update",
HttpMethod.PUT,
entity,
Object.class
);
return response.getBody();
}
public TicketDto createTicket(String token, TicketDto request) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<TicketDto> entity = new HttpEntity<>(request, headers);
ResponseEntity<TicketDto> response = restTemplate.postForEntity(
BASE_URL + "/v2/tickets/create",
entity,
TicketDto.class
);
return response.getBody();
}
public TicketResponseDto getMyTickets(String token, int page, int size) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(BASE_URL + "/v2/tickets/by-user")
.queryParam("page", page)
.queryParam("size", size);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<TicketResponseDto> response = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
entity,
TicketResponseDto.class
);
return response.getBody();
}
public TicketResponseDto searchTickets(String token, TicketFilterDto filters) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<TicketFilterDto> entity = new HttpEntity<>(filters, headers);
ResponseEntity<TicketResponseDto> response = restTemplate.postForEntity(
BASE_URL + "/v2/tickets/search",
entity,
TicketResponseDto.class
);
return response.getBody();
}
public TicketResponseDto getTicketStatuses(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<TicketResponseDto> response = restTemplate.exchange(
BASE_URL + "/v2/tickets/statuses",
HttpMethod.GET,
entity,
TicketResponseDto.class
);
return response.getBody();
}
public TicketResponseDto getTicketDepartments(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<TicketResponseDto> response = restTemplate.exchange(
BASE_URL + "/v2/tickets/departments",
HttpMethod.GET,
entity,
TicketResponseDto.class
);
return response.getBody();
}
public TicketResponseDto getTicketCauses(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<TicketResponseDto> response = restTemplate.exchange(
BASE_URL + "/v2/tickets/causes",
HttpMethod.GET,
entity,
TicketResponseDto.class
);
return response.getBody();
}
public TicketResponseDto getTicketHistory(String token, UUID ticketId) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<TicketResponseDto> response = restTemplate.exchange(
BASE_URL + "/v2/tickets/" + ticketId + "/history",
HttpMethod.GET,
entity,
TicketResponseDto.class
);
return response.getBody();
}
public TicketStatusDto getInitialTicketStatus(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<TicketStatusDto> response = restTemplate.exchange(
BASE_URL + "/v2/tickets/initial-status",
HttpMethod.GET,
entity,
TicketStatusDto.class
);
return response.getBody();
}
public Object getPaymentInfo(String token, Integer paymentId) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<Object> response = restTemplate.exchange(
BASE_URL + "/v2/payments/" + paymentId,
HttpMethod.GET,
entity,
Object.class
);
return response.getBody();
}
public Object getSettledPayments(String token, int page, int size) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(BASE_URL + "/v2/payments/settled")
.queryParam("page", page)
.queryParam("size", size);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<Object> response = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
entity,
Object.class
);
return response.getBody();
}
public Object getReturnInfo(String token, Integer returnId) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<Object> response = restTemplate.exchange(
BASE_URL + "/v2/returns/" + returnId,
HttpMethod.GET,
entity,
Object.class
);
return response.getBody();
}
public Object getSettledReturns(String token, int page, int size) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(BASE_URL + "/v2/returns/settled")
.queryParam("page", page)
.queryParam("size", size);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<Object> response = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
entity,
Object.class
);
return response.getBody();
}
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok (optional) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
Using cURL with proper error handling. Expand categories to see individual APIs.
Complete examples using cURL with proper error handling and JSON parsing.
<?php
define('BASE_URL', 'https://jenni.alzaeemexp.com/api');
function login($username, $password) {
$data = json_encode([
'username' => $username,
'password' => $password
]);
$ch = curl_init(BASE_URL . '/v2/auth/login');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$result = json_decode($response, true);
echo "✅ Login successful\n";
return [
'token' => $result['token'],
'refreshToken' => $result['refreshToken']
];
} else {
throw new Exception("Login failed: $response");
}
}
// Usage
$tokens = login('your_username', 'your_password');
?>
<?php
function createShipments($token, $shipments) {
$data = json_encode([
'system_code' => 'YOUR_COMPANY_CODE',
'shipments' => $shipments
]);
$ch = curl_init(BASE_URL . '/v2/shipments/create');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
echo "✅ Accepted: " . $result['summary']['accepted_count'] . "\n";
echo "❌ Rejected: " . $result['summary']['rejected_count'] . "\n";
return $result;
}
// Example
$shipments = [
[
'shipment_number' => 'SHIP001',
'external_shipment_id' => '12345', // ✅ REQUIRED: Your internal order ID
'receiver_name' => 'أحمد علي',
'receiver_phone_1' => '07901234567',
'governorate_code' => 'BGD',
'city' => 'الكرادة',
'amount_iqd' => 50000,
'amount_usd' => 0,
'quantity' => 1,
'is_proof_of_delivery' => true,
'is_fragile' => false,
'have_return_item' => false,
'is_special_case' => false,
'product_info' => 'Mobile Phone - Samsung Galaxy',
'note' => 'Please call before delivery'
]
];
$result = createShipments($token, $shipments);
?>
Three Query Modes:
<?php
// MODE 1: Query by IDs
function queryByIds($token, $shipmentIds) {
$data = json_encode(['shipment_ids' => $shipmentIds]);
$ch = curl_init(BASE_URL . '/v2/shipments/query');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// MODE 2: Query by Numbers
function queryByNumbers($token, $shipmentNumbers) {
$data = json_encode(['shipment_numbers' => $shipmentNumbers]);
$ch = curl_init(BASE_URL . '/v2/shipments/query');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// MODE 3: Get All User's Shipments (Paginated)
function getAllShipmentsPaginated($token, $page = 0, $pageSize = 20) {
$data = json_encode([
'page' => $page,
'page_size' => $pageSize
]);
$ch = curl_init(BASE_URL . '/v2/shipments/query');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
// Pagination info
echo "Page " . ($result['pagination']['current_page'] + 1) .
" of " . $result['pagination']['total_pages'] . "\n";
echo "Total Records: " . $result['pagination']['total_records'] . "\n";
return $result;
}
// Examples
queryByIds($token, [12345, 12346]);
queryByNumbers($token, ['SHIP001', 'SHIP002']);
getAllShipmentsPaginated($token, 0, 20);
?>
<?php
function getGovernorates() {
// NO AUTH REQUIRED - Public geographic data
$ch = curl_init(BASE_URL . '/v2/reference/governorates');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json'
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
return $result['data'];
}
// Cache governorates
$governorates = getGovernorates();
foreach ($governorates as $gov) {
echo $gov['code'] . ": " . $gov['name_ar'] . "\n";
}
?>
<?php
function getCities($governorateCode = null) {
// NO AUTH REQUIRED - Public geographic data
$url = BASE_URL . '/v2/reference/cities';
if ($governorateCode) {
$url .= '?governorate_code=' . urlencode($governorateCode);
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json'
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
return $result['data'];
}
// Get all Baghdad cities
$baghdadCities = getCities('BGD');
?>
<?php
function getReturnReasons($token) {
// AUTH REQUIRED - Business policies
$ch = curl_init(BASE_URL . '/v2/reference/return-reasons');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
return $result['data'];
}
// Example
$reasons = getReturnReasons($token);
foreach ($reasons as $reason) {
echo $reason['code'] . ": " . $reason['description'] . "\n";
}
?>
<?php
function getPostponedReasons($token) {
// AUTH REQUIRED - Business policies
$ch = curl_init(BASE_URL . '/v2/reference/postponed-reasons');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
return $result['data'];
}
// Example
$reasons = getPostponedReasons($token);
foreach ($reasons as $reason) {
echo $reason['code'] . ": " . $reason['description'] . "\n";
}
?>
<?php
function getActionCodes() {
// NO AUTH REQUIRED - Public reference
$ch = curl_init(BASE_URL . '/v2/reference/action-codes');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json'
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
return $result['data'];
}
// Example
$actions = getActionCodes();
foreach ($actions as $action) {
echo $action['code'] . ": " . $action['name_en'] .
" (" . $action['name_ar'] . ")\n";
}
?>
<?php
function getCountries() {
$ch = curl_init(BASE_URL . '/v2/reference/countries');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
return $result['data'];
}
?>
function getOrderStatistics($token) {
$ch = curl_init(BASE_URL . '/v2/statistics/orders-by-status');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getOrdersInProcess($token, $page = 1, $size = 20) {
$url = BASE_URL . "/v2/orders/in-process?page=" . $page . "&size=" . $size;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getOrdersByStep($token, $stepCode, $page = 1, $size = 20) {
$url = BASE_URL . "/v2/orders/by-step?step_code=" . $stepCode .
"&page=" . $page . "&size=" . $size;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
return $data['orders'];
}
// Example: Get all orders "out for delivery"
$ordersOfd = getOrdersByStep($token, 'OFD');
function deleteOrder($token, $shipmentId) {
$ch = curl_init(BASE_URL . '/v2/orders/' . $shipmentId);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getMyStores($token) {
// Get merchant stores - MASTER CUSTOMER ONLY
$ch = curl_init(BASE_URL . "/v2/merchants/my-stores");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
return $data['data'];
}
function createStore($token, $storeName, $phone, $governorateCode, $address) {
$data = [
"store_name" => $storeName,
"store_phone" => $phone,
"governorate_code" => $governorateCode,
"address" => $address,
"latitude" => 33.3152,
"longitude" => 44.3661
];
$ch = curl_init(BASE_URL . '/v2/stores/create');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function createMerchant($token, $merchantData) {
// Create merchant - AGGREGATOR ROLE ONLY
$payload = json_encode([
'merchant_name' => $merchantData['name'],
'phone' => $merchantData['phone'],
'system_code' => $merchantData['code']
]);
$ch = curl_init(BASE_URL . "/v2/merchant-management/create");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer " . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function updateMerchant($token, $merchantId, $updates) {
// Update merchant - AGGREGATOR ROLE ONLY
$payload = json_encode(array_merge(
['merchant_id' => $merchantId],
$updates
));
$ch = curl_init(BASE_URL . "/v2/merchant-management/update");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer " . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function createTicket($token, $ticketData) {
$payload = json_encode([
'message' => $ticketData['message'],
'ticket_cause_name' => $ticketData['cause'],
'ticket_department_name' => $ticketData['department']
]);
$ch = curl_init(BASE_URL . '/v2/tickets/create');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getMyTickets($token, $page = 1, $size = 20) {
$url = BASE_URL . "/v2/tickets/by-user?page=" . $page . "&size=" . $size;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function searchTickets($token, $filters) {
$payload = json_encode($filters);
$ch = curl_init(BASE_URL . '/v2/tickets/search');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getTicketStatuses($token) {
$ch = curl_init(BASE_URL . '/v2/tickets/statuses');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getTicketDepartments($token) {
$ch = curl_init(BASE_URL . '/v2/tickets/departments');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getTicketCauses($token) {
$ch = curl_init(BASE_URL . '/v2/tickets/causes');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getTicketHistory($token, $ticketId) {
$ch = curl_init(BASE_URL . '/v2/tickets/' . $ticketId . '/history');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getInitialTicketStatus($token) {
$ch = curl_init(BASE_URL . '/v2/tickets/initial-status');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function uploadTicketAttachment($token, $ticketId, $filePath) {
$cfile = new CURLFile($filePath);
$ch = curl_init(BASE_URL . '/v2/tickets/' . $ticketId . '/attachment');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, ['file' => $cfile]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getPaymentInfo($token, $paymentId) {
$ch = curl_init(BASE_URL . '/v2/payments/' . $paymentId);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getSettledPayments($token, $page = 1, $size = 20) {
$url = BASE_URL . "/v2/payments/settled?page=" . $page . "&size=" . $size;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getReturnInfo($token, $returnId) {
$ch = curl_init(BASE_URL . '/v2/returns/' . $returnId);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function getSettledReturns($token, $page = 1, $size = 20) {
$url = BASE_URL . "/v2/returns/settled?page=" . $page . "&size=" . $size;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
YOUR_TOKEN with your actual JWT token from loginYOUR_COMPANY_CODE with your registered system codeLearn from practical scenarios that demonstrate how to use the V2 API effectively.
// Your order object from website
const order = {
orderId: "ORD-2025-001",
customerName: "أحمد علي",
customerPhone: "07901234567",
address: "شارع الكرادة، بناية 5، الطابق 2",
city: "الكرادة",
governorate: "بغداد",
totalAmount: 75000, // IQD
items: [
{ name: "Samsung Galaxy S24", price: 75000, qty: 1 }
]
};
// Map to V2 Integration API format
const shipmentRequest = {
system_code: "YOUR_ECOMMERCE_CODE",
shipments: [
{
shipment_number: order.orderId, // Use your order ID
receiver_name: order.customerName,
receiver_phone_1: order.customerPhone,
governorate_code: "BGD", // Map "Baghdad" to "BGD"
city: order.city, // City name in Arabic
address: order.address,
amount_iqd: order.totalAmount,
amount_usd: 0,
quantity: 1,
product_info: order.items[0].name,
note: "Order from website"
}
]
};
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(shipmentRequest)
});
const result = await response.json();
if (result.success && result.accepted_shipments.length > 0) {
const shipment = result.accepted_shipments[0];
console.log('✅ Shipment created:', shipment.shipment_id);
// Save shipment_id to your database for tracking
await saveShipmentId(order.orderId, shipment.shipment_id);
} else {
console.error('❌ Failed:', result.rejected_shipments[0].reason);
}
// Later, customer wants to track the order
async function trackOrder(orderId) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
shipment_numbers: [orderId]
})
});
const result = await response.json();
if (result.shipments.length > 0) {
const shipment = result.shipments[0];
console.log('📦 Status:', shipment.current_step_ar); // Arabic status name
console.log('🚚 Agent:', shipment.delivery_agent_name);
return shipment;
}
}
# Python example: Process 500 shipments
import requests
import json
BASE_URL = "https://jenni.alzaeemexp.com/api"
TOKEN = "your_jwt_token_here"
def create_shipments_in_batches(all_shipments, batch_size=100):
"""
Create shipments in batches of 100 (API limit)
"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {TOKEN}"
}
total_accepted = 0
total_rejected = 0
# Split into batches
for i in range(0, len(all_shipments), batch_size):
batch = all_shipments[i:i + batch_size]
request_data = {
"system_code": "YOUR_CODE",
"shipments": batch
}
response = requests.post(
f"{BASE_URL}/v2/shipments/create",
headers=headers,
json=request_data
)
result = response.json()
total_accepted += result['summary']['accepted_count']
total_rejected += result['summary']['rejected_count']
# Log rejected shipments
for rejected in result['rejected_shipments']:
print(f"❌ {rejected['shipment_number']}: {rejected['reason']}")
print(f"✅ Batch {i//batch_size + 1}: " +
f"Accepted={result['summary']['accepted_count']}, " +
f"Rejected={result['summary']['rejected_count']}")
print(f"\n📊 Total: Accepted={total_accepted}, Rejected={total_rejected}")
# Example usage
shipments = load_shipments_from_file("daily_orders.csv") # Your 500 shipments
create_shipments_in_batches(shipments)
GET https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=SHIP001
GET https://jenni.alzaeemexp.com/api/v2/reference/postponed-reasons
// User selections
const selectedReason = "العميل غير موجود";
const postponeDate = 1; // Tomorrow
const updateRequest = {
shipment_number: "SHIP001",
action: "POSTPONED",
postponed_reason: selectedReason,
postponed_reason_en: "Customer not available",
postponed_reason_ku: "کڕیار بەردەست نییە",
postponed_date_id: postponeDate,
note: "سيتم التواصل غداً"
};
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/update-status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(updateRequest)
});
const result = await response.json();
console.log('✅ Status updated:', result.new_status_ar);
Customer refuses to accept the entire shipment
{
"shipment_id": 12345,
"action": "RETURN_TO_STORE",
"return_reason": "رفض الاستلام",
"return_reason_en": "Customer refused",
"return_reason_ku": "کڕیار ڕەتکردەوە"
// return_quantity not needed - all items returned
}
Customer accepts 2 items, returns 1 item from order of 3
{
"shipment_id": 12345,
"action": "PARTIAL_DELIVERY",
"return_quantity": 1, // ✅ Specify partial quantity
"new_amount_iqd": 50000, // New amount after return
"note": "تم استرجاع قطعة واحدة"
}
{
"shipment_id": 12345,
"action": "SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE",
"new_amount_iqd": 40000, // New negotiated amount
"note": "العميل وافق على مبلغ 40,000 بدلاً من 50,000",
"image_url": "https://your-storage.com/proof-of-delivery.jpg" // Optional
}
// Get customer's active order IDs from your database
const customerOrders = await getCustomerActiveOrders(customerId);
// Returns: ["ORD-001", "ORD-002", "ORD-003", ...]
// Split into chunks of 100 (API limit)
const chunks = chunkArray(customerOrders, 100);
let allShipments = [];
for (const chunk of chunks) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
shipment_numbers: chunk
})
});
const result = await response.json();
allShipments = allShipments.concat(result.shipments);
}
// Display tracking information
allShipments.forEach(shipment => {
displayShipmentCard({
orderId: shipment.shipment_number,
status: shipment.current_step_ar, // Arabic status
agent: shipment.delivery_agent_name, // Agent name
agentPhone: shipment.delivery_agent_phone,
currentBranch: shipment.current_branch_name,
estimatedDelivery: calculateETA(shipment.current_step)
});
});
// Helper function
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
// Agent scans barcode
const scannedBarcode = "SHIP001";
// Step 1: Get shipment info
const shipmentInfo = await queryShipment(scannedBarcode);
// Step 2: Get available actions
const actionsResponse = await fetch(
`https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=${scannedBarcode}`,
{ headers: { 'Authorization': `Bearer ${token}` }}
);
const actions = await actionsResponse.json();
// Step 3: Show action buttons to agent
showActionButtons(actions.data); // [POSTPONED, RETURN_TO_STORE, SUCCESSFUL_DELIVERY, ...]
// Step 4: Agent selects "Delivered Successfully"
async function deliverShipment(shipmentNumber) {
const response = await fetch('https://jenni.alzaeemexp.com/api/v2/shipments/update-status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
shipment_number: shipmentNumber,
action: "SUCCESSFUL_DELIVERY",
agent_latitude: currentLocation.latitude, // GPS location
agent_longitude: currentLocation.longitude,
image_url: uploadedProofImage, // Delivery proof photo
note: "تم التسليم بنجاح"
})
});
const result = await response.json();
if (result.success) {
showSuccessMessage("✅ تم تسليم الشحنة بنجاح");
}
}
Important Configuration:
You MUST configure your domain in Jenni Logistics system settings:
https://yourstore.com
Jenni Logistics will automatically send status updates to:
YOUR_DOMAIN/v2/push/update-status
Create POST /v2/push/update-status endpoint on YOUR server to receive automatic updates from Jenni Logistics:
is_connection_url to your domain (e.g., https://yourstore.com)/v2/push/update-status to your domainYOUR_DOMAIN/v2/push/update-status// server.js - Express.js Example
const express = require('express');
const app = express();
app.use(express.json());
// This endpoint receives automatic status updates from Jenni Logistics
app.post('/v2/push/update-status', async (req, res) => {
try {
console.log('📬 Received status update from Jenni Logistics:', req.body);
const { system_code, updates } = req.body;
// Verify system code
if (system_code !== 'YOUR_SYSTEM_CODE') {
return res.status(401).json({
success: false,
message: 'Invalid system code'
});
}
// Process each update
for (const update of updates) {
console.log(`\n🔄 Processing update for: ${update.shipment_number}`);
console.log(`Action: ${update.action_code}`);
console.log(`Current Step: ${update.current_step} (${update.current_step_ar})`);
console.log(`Current Stage: ${update.current_stage} (${update.current_stage_ar})`);
if (update.agent_latitude && update.agent_longitude) {
console.log(`📍 Agent Location: ${update.agent_latitude}, ${update.agent_longitude}`);
}
// Update YOUR database
await updateOrderInYourDatabase({
orderNumber: update.shipment_number,
status: update.current_step,
statusArabic: update.current_step_ar,
stage: update.current_stage,
action: update.action_code,
agentLatitude: update.agent_latitude,
agentLongitude: update.agent_longitude,
governorate: update.governorate_name,
note: update.note,
amountIQD: update.amount_iqd,
quantityDelivered: update.quantity_delivered,
quantityReturned: update.quantity_returned,
updatedAt: new Date()
});
// Send notification to customer
if (update.action_code === 'SUCCESSFUL_DELIVERY') {
await sendCustomerNotification(
update.shipment_number,
'Your order has been delivered successfully! 🎉'
);
} else if (update.action_code === 'POSTPONED') {
await sendCustomerNotification(
update.shipment_number,
`Delivery postponed: ${update.note}`
);
}
}
// MUST respond with success
res.json({
success: true,
message: 'Status updates processed successfully',
received_count: updates.length
});
} catch (error) {
console.error('❌ Error processing status update:', error);
res.status(500).json({
success: false,
message: error.message
});
}
});
app.listen(3000, () => {
console.log('✅ Server listening on port 3000');
console.log('📬 Ready to receive status updates from Jenni Logistics');
});
// Helper functions
async function updateOrderInYourDatabase(data) {
// Your database logic here
console.log('💾 Updating database:', data);
}
async function sendCustomerNotification(orderNumber, message) {
// Your notification logic (email/SMS/push)
console.log('📧 Sending notification:', orderNumber, message);
}
When a shipment status changes in Jenni Logistics, YOUR endpoint will receive:
// POST to: YOUR_DOMAIN/v2/push/update-status
// Headers: Content-Type: application/json
// Authorization: Bearer YOUR_TOKEN (if TOKEN auth method)
{
"system_code": "YOUR_SYSTEM_CODE",
"updates": [
{
"shipment_number": "ORD-2024-001",
"action_code": "SUCCESSFUL_DELIVERY",
"current_step": "DELIVERED",
"current_step_ar": "سلمت بنجاح",
"current_stage": "DELIVERED",
"current_stage_ar": "الواصل",
"governorate_code": "BGD",
"governorate_name": "BAGHDAD",
"note": "تم التسليم بنجاح للمستلم",
"agent_latitude": 33.3152,
"agent_longitude": 44.3661,
"amount_iqd": 50000,
"quantity_delivered": 2,
"quantity_returned": 0
}
]
}
| Field | Type | Description |
|---|---|---|
system_code |
String | Your unique system code (for verification) |
shipment_number |
String | The shipment number you originally sent to Jenni Logistics |
action_code |
String | The action that was taken (e.g., SUCCESSFUL_DELIVERY, POSTPONED, RETURNED_WITH_AGENT) |
current_step |
String | Current status in English (e.g., "DELIVERED", "OFD") - Uses StepStatusMapper global names |
current_step_ar |
String | Current status in Arabic (e.g., "سلمت بنجاح", "قيد التوصيل") - Uses StepStatusMapper Arabic names |
current_stage |
String | Current stage in English (SORTING_CENTER, WITH_AGENT, DELIVERED, RTO, etc.) |
current_stage_ar |
String | Current stage in Arabic (المخزن, عند المندوب, الواصل, الراجع, etc.) |
governorate_code |
String | Governorate code (BGD, BAS, NIN, etc.) |
governorate_name |
String | Governorate name in English (BAGHDAD, BASRA, NINAWA, etc.) |
note |
String | Additional note from agent or system (optional) |
agent_latitude |
Double | Agent's GPS location - Latitude (when agent performs action) |
agent_longitude |
Double | Agent's GPS location - Longitude (when agent performs action) |
amount_iqd |
Double | Shipment amount in IQD (may change if customer requests) |
quantity_delivered |
Integer | Quantity delivered (for partial deliveries) |
quantity_returned |
Integer | Quantity returned (for partial returns) |
Best Practices:
{ "success": true } immediately (Jenni expects confirmation)system_code matches yours for securityagent_latitude and agent_longitude to track delivery locationsCommon Action Codes You Will Receive:
SUCCESSFUL_DELIVERY → Shipment delivered successfullyPARTIAL_DELIVERY → Partial delivery or exchangePOSTPONED → Delivery postponed by customerRETURNED_WITH_AGENT → Customer refused deliveryASSIGN_TO_AGENT → Assigned to delivery agentMOVE_TO_AGENT → Moved to agent's manifest (out for delivery)RETURN_TO_STORE → Returned to warehouse/sorting centerSee full list in Action Codes Reference section
available-actions before showing UI optionsAll V2 API errors return structured error codes for programmatic handling. Use these codes in your application logic.
| Error Code | Description | Solution |
|---|---|---|
INVALID_SYSTEM_CODE |
system_code is missing or not found in authorized systems | Provide valid system_code from your integration configuration |
DUPLICATE_SHIPMENT |
Shipment number already exists for this system | Use a unique shipment_number or query existing shipment |
MERCHANT_ID_REQUIRED |
merchant_id is required for AGGREGATOR users | Include merchant_id field in request (AGGREGATOR only) |
INVALID_PHONE_NUMBER |
Phone number format is invalid (must be 10-15 digits) | Use valid phone format (e.g., 07901234567) |
INVALID_AMOUNT |
Amount must be greater than or equal to zero | Provide valid non-negative amount_iqd value |
INVALID_QUANTITY |
Quantity must be greater than zero | Provide quantity of at least 1 |
INVALID_GOVERNORATE |
Governorate code not found in system mapping | Use valid code from GET /v2/reference/governorates (e.g., BGD, BAS) |
NEGATIVE_AMOUNT_NOT_ALLOWED |
Negative amounts blocked by system configuration | Use positive amount or contact support to enable negative amounts |
PROCESSING_ERROR |
Unexpected error during shipment processing | Check error details in "reason" field and retry |
| Error Code | Description | Solution |
|---|---|---|
MISSING_IDENTIFIER |
shipment_id is required but not provided | Provide valid shipment_id (must be > 0) |
MISSING_ACTION |
action field is required | Provide valid action code (e.g., POSTPONED, SUCS_DLV) |
INVALID_ACTION |
Action not allowed for current shipment status | GET /v2/reference/available-actions?shipment_id=XXX to check allowed actions |
MISSING_POSTPONED_REASON |
postponed_reason required for POSTPONED action | Provide Arabic text reason (any text accepted, auto-created if new) |
MISSING_POSTPONED_DATE_ID |
postponed_date_id required for POSTPONED action | Provide postponed_date_id (1=tomorrow, 2=2 days, 3=3 days max) |
INVALID_POSTPONED_DATE_ID |
postponed_date_id must be between 1-3 | Use 1 (tomorrow), 2 (in 2 days), or 3 (in 3 days) only |
MISSING_RETURN_REASON |
return_reason required for RTN_TOSTORE/RTN_WITHAGENT actions | Provide Arabic text reason (any text accepted, auto-created if new) |
INVALID_RETURN_QUANTITY |
return_quantity must be at least 1 (for partial returns) | Provide valid return quantity >= 1 |
MISSING_TREATED_MESSAGE |
treated_message required for TREATED action | Provide treatment description text (min 3 characters) |
TREATED_MESSAGE_TOO_SHORT |
treated_message must be at least 3 characters | Provide descriptive treatment message |
MISSING_NEW_AMOUNT |
new_amount_iqd/new_amount_usd required for SUCS_DLV_CHANGEAMT | Provide new_amount_iqd or new_amount_usd when changing amount |
| Error Code | Description | Solution |
|---|---|---|
RATE_LIMIT_EXCEEDED |
Too many requests - rate limit exceeded | Wait specified seconds before retrying (see response message) |
SHIPMENT_NOT_FOUND |
Shipment not found by provided identifier | Verify shipment_id or shipment_number is correct |
VALIDATION_ERROR |
Request validation failed (multiple fields) | Check field_errors array in response for details |
INTEGRATION_ERROR |
Integration processing error | Check error message for details, contact support if persists |
INTERNAL_ERROR |
Internal server error occurred | Retry request, contact support if issue persists |
INVALID_ARGUMENT |
Invalid argument in request | Check request parameters and format |
success field in response (true/false)error_code for programmatic error handlingmessage field to users (localized Arabic/English)timestamp for debuggingfailed_shipments array for individual errorsQuick and clear answers to the most frequently asked questions about V2 Integration
Concept: Jenni Logistics sends shipments to you
Your Role: Delivery Service Provider
Concept: You send shipments to Jenni Logistics → Jenni Logistics delivers → Jenni Logistics sends you automatic status updates
POST /v2/shipments/createYOUR_DOMAIN/v2/push/update-statusPOST /v2/shipments/queryYour Role: Merchant/Store/E-commerce/App
→ Use Push Out
Jenni Logistics sends you shipments, and you deliver them with your vehicles and drivers
→ Use Receive In
You send your shipments to Jenni Logistics, and Jenni Logistics delivers them with their vehicles and drivers
Why?
/v2/shipments/createRecommended Technologies:
Node.js, Python Flask/Django, PHP Laravel, Java Spring Boot
Why?
Recommended Technologies:
Any language that supports HTTP requests (cURL, Postman, JavaScript, Python, PHP, Java)
POST /v2/shipments/create
Receive shipments from Jenni Logistics
POST /v2/push/update-status
Send status updates to Jenni Logistics
Jenni will use its Token/Login credentials to call your server
You send shipments → Jenni Logistics delivers → Jenni Logistics sends you automatic status updates
POST /v2/push/update-status
Receive automatic status updates from Jenni Logistics (includes agent GPS)
POST /v2/auth/login
Get JWT token
POST /v2/shipments/create
Create shipments in Jenni
POST /v2/shipments/query
Manual status check (automatic updates via webhook)
POST /v2/shipments/update-status
Update if needed
PUT /v2/shipments/edit
Edit shipment details
You use your username/password to get Token
Large Logistics Company:
Each mode requires separate system configuration. Contact your administrator or see Integration Modes for details.
| Step | From | To | API | Server |
|---|---|---|---|---|
| 1️⃣ Send Shipment | Jenni | You | POST /v2/shipments/create |
YOUR server |
| 2️⃣ Status Update | You | Jenni | POST /v2/push/update-status |
Jenni server |
| Step | From | To | API | Server |
|---|---|---|---|---|
| 1️⃣ Login | You | Jenni | POST /v2/auth/login |
Jenni server |
| 2️⃣ Create Shipment | You | Jenni | POST /v2/shipments/create |
Jenni server |
| 3️⃣ Status Updates (Auto) | Jenni | You | POST /v2/push/update-status |
YOUR server |
| 3️⃣ OR Query Status (Manual) | You | Jenni | POST /v2/shipments/query |
Jenni server |
Automatic Status Updates (Receive In Mode):
YOUR_DOMAIN/v2/push/update-statusPOST /v2/shipments/query for manual checks anytimeSee Scenario 8 for complete implementation example with all fields explained
Summary: In Push Out Jenni Logistics calls you first, in Receive In you call Jenni Logistics first, then Jenni Logistics sends you automatic updates.
A logistics company in Basra receives orders from Jenni Logistics customers in Baghdad and delivers them
A branch in Sulaymaniyah receives shipments from the main branch in Baghdad
An external delivery company specialized in international shipping
Online store sends its orders to Jenni Logistics for delivery
Marketplace app uses Jenni Logistics to deliver products
Point of sale system for retail stores sends sales to Jenni Logistics
Warehouse management system sends shipments automatically
All APIs return the same error format:
{
"success": false,
"message": "User-friendly error message",
"timestamp": "2025-11-02T10:00:00Z",
"error_code": "INVALID_GOVERNORATE" // For programmatic handling
}
success field firsterror_code for debuggingmessage to users| Operation | Maximum | Notes |
|---|---|---|
| Create Shipments | 100 per request | Bulk array support |
| Query by IDs/Numbers | 100 per request | Specific lookup mode |
| Query Paginated | 100 per page | Unlimited total with pagination |
| Update Status | 100 per request | Bulk update supported |
| Generate Stickers | 100 per request | Returns PDF file |
| Token Validity | 24 hours | Use refresh token before expiration |
| Refresh Token Validity | 30 days | Re-login after expiration |
Rate limits are applied per user/IP using a token bucket system:
| Endpoint Type | Rate Limit | Examples |
|---|---|---|
| Public Endpoints | 100 requests/minute | Docs, reference data (governorates, cities, action codes) |
| Authenticated Endpoints | 60 requests/minute | Read operations (POST /v2/shipments/query) |
| Write Operations | 30 requests/minute | POST /v2/shipments/create, POST /v2/shipments/stickers, POST /v2/shipments/update-status, PUT /v2/shipments/edit |
Response: HTTP 429 with error code RATE_LIMIT_EXCEEDED
Header: X-Rate-Limit-Retry-After-Seconds contains the exact wait time in seconds
Action: Always use the value from the header - the wait time is dynamic and calculated based on when the last token was consumed
Important: The wait time is dynamic, not fixed. It depends on when the last token was consumed:
💡 Key Point: Never assume a fixed wait time. Always read X-Rate-Limit-Retry-After-Seconds header value from the 429 response - it tells you exactly how long to wait.
/v2/shipments/stickers):00:00.000 | Send 30 requests → All tokens consumed
00:00.000 | Request #31 → ❌ HTTP 429
Response: "Too many requests. Please try again in 2 seconds."
Header: X-Rate-Limit-Retry-After-Seconds: 2
00:00.500 | Request #32 → ❌ HTTP 429
Header: X-Rate-Limit-Retry-After-Seconds: 1.5 (dynamic calculation)
00:02.000 | Token refills → ✅ Request #33 succeeds (1 token available)
00:04.000 | Another token refills → ✅ Request #34 succeeds
00:06.000 | Another token refills → ✅ Request #35 succeeds
...
01:00.000 | Full bucket refill (30 tokens) → Can send 30 requests again
X-Rate-Limit-Remaining header to track available tokensUse the Try It Live section to test APIs directly from your browser
Download the complete Postman Collection
Copy code examples from Integration Examples
Testing Strategy: Start with one shipment, verify success, then increase gradually. See Testing Guide for 40+ test cases.
All internal codes are converted to professional names via mappers:
| Code Type | Mapper | Example |
|---|---|---|
action_code |
ActionCodeMapper | SUCCESSFUL_DELIVERY, POSTPONED |
current_step |
StepStatusMapper | OFD, IN_SC, DELIVERED |
current_stage |
StageMapper | SORTING_CENTER, RTO, WITH_AGENT |
governorate_code |
GovernorateMapper | BGD, BAS, NIN |
Guarantees:
See Action Codes Reference for complete list.
Token-based security with 24-hour expiration + 30-day refresh token
System verifies shipment ownership for every operation (MASTERCUSTOMER, DLVAGENT, AGGREGATOR, PICKUPAGENT)
All communications encrypted via HTTPS (TLS 1.2+)
No internal codes or sensitive information exposed to consumers
All inputs validated before processing
Protection from excessive requests (max 100 items per request)
Security Best Practices:
Pagination available in:
POST /v2/shipments/query (Mode 3: All user shipments)GET /v2/orders/in-processGET /v2/orders/by-stepGET /v2/reference/citiesGET /v2/payments/settledGET /v2/returns/settled{
"shipments": [...],
"pagination": {
"current_page": 0, // 0-indexed
"page_size": 100,
"total_records": 2548,
"total_pages": 26,
"has_next_page": true,
"has_previous_page": false
}
}
// Fetch all pages
async function fetchAllShipments() {
let page = 0;
let allShipments = [];
while (true) {
const response = await queryShipments({ page, page_size: 100 });
allShipments = allShipments.concat(response.shipments);
if (!response.pagination.has_next_page) break;
page++;
}
return allShipments;
}
V2 Integration is designed to support bidirectional integration between ANY two V2-compatible systems (Jenni-to-Jenni, Jenni-to-External, External-to-Jenni):
System A (Source) → System B (Destination)
1. System A sends shipment with external_shipment_id = "12345" (System A's internal ID - STRING)
2. System B receives and stores: receivedFromCaseId = "12345" (STRING)
3. System B processes shipment (delivered, returned, etc.)
4. System B sends status update with external_id = 12345
5. System A receives update and matches using external_id
6. ✅ Status synchronized correctly!
Works for: Jenni Logistics ↔ Jenni, Jenni Logistics ↔ External V2, External V2 ↔ Jenni
| Field | Purpose | Required? |
|---|---|---|
external_shipment_id |
Source Jenni's internal shipment ID | YES |
shipment_number |
Receipt number (must be unique) | YES |
external_id |
Used in status updates to match back to source | YES |
sender_name |
Original sender information | Recommended |
sender_phone |
Original sender contact | Recommended |
Purpose: Handle transient network errors
Purpose: Ensure delivery even if system is down
14:00:00 | Immediate Retry 1/3: Failed
14:00:01 | Immediate Retry 2/3: Failed
14:00:03 | Immediate Retry 3/3: Failed
14:00:03 | ✅ Saved to persistent retry queue
14:15:00 | Persistent Retry 1/3: Failed
14:45:00 | Persistent Retry 2/3: Failed
15:45:00 | Persistent Retry 3/3: Failed
15:45:01 | ❌ Status: FAILED (manual intervention required)
15:45:01 | 📋 Remains in database for 24 hours
15:45:01 | 🔔 Administrator notified
integration_pending_update table with status FAILEDm_syslog table// Use shipment_id or external_shipment_id for duplicate detection
@PostMapping("/v2/shipments/create")
public ResponseEntity<?> createShipments(@RequestBody CreateRequest request) {
List<Result> results = new ArrayList<>();
for (Shipment shipment : request.getShipments()) {
// Check if already processed
String key = shipment.getExternal_shipment_id();
if (redis.exists(key)) {
log.info("Duplicate detected: {}", key);
results.add(Result.success(key, redis.get(key)));
continue;
}
// Process and save shipment to your database
String externalId = processAndSaveShipment(shipment);
// Mark as processed in cache (7-day TTL)
redis.setex(key, 604800, externalId);
results.add(Result.success(key, externalId));
}
return ResponseEntity.ok(results);
}
Common Reasons:
| Reason | Solution |
|---|---|
| 🔴 Trying to access a shipment that doesn't belong to you | Verify that the shipment is yours (use correct shipment_id) |
| 🔴 Wrong Rank | "Create Store" operation requires MASTERCUSTOMER rank only |
| 🔴 Expired Token | Use /v2/auth/refresh to renew |
| 🔴 Wrong system_code | Verify you're using the correct system_code assigned to you |
You must use correct codes from /v2/reference/governorates
BGD - BaghdadBAS - BasraNIN - NinevehARB - ErbilNJF - NajafKRB - KarbalaANB - AnbarBBL - BabylonDHQ - DuhokDYL - DiyalaKRK - Kirkuk# Get complete list
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/governorates"
No problem! The system handles it automatically
address// Your request
{
"governorate_code": "BGD",
"city_name": "New University District", // ❌ Not in database
"address": "Street 14, Building 5"
}
// System handles it automatically:
{
"district_id": 999, // "Unknown" district for Baghdad
"address": "Street 14, Building 5 New University District" // ✅ City added
}
// ✅ Shipment created successfully!
POST /v2/shipments/query (Best Performance)Query up to 100 shipments by ID/Number in single request
{
"shipment_ids": [12345, 12346, 12347]
}
POST /v2/shipments/query (Paginated)Get all your shipments with pagination (100 per page)
{
"page": 0,
"page_size": 100
}
Receive automatic notifications when status changes
Performance Tip: Query API optimized with batch loading - 100 shipments in ~50ms with only 5 DB queries!
Quick solutions to the most frequently encountered problems.
POST /v2/auth/refresh with Authorization: Bearer {refreshToken}
# Add this header to all authenticated requests:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
shipment_number already exists in the system
POST /v2/shipments/queryGET /v2/reference/governorates
?download=excel07901234567, 07801234567, 009647901234567079012345 (too short - less than 10 digits)12345 (too short)merchant_id
merchant_id in your requestmerchant_id is automatically set (leave it null)GET /v2/reference/available-actions?shipment_number=XXX
available-actions first to know which actions are currently possible!
postponed_reason or return_reason"العميل غير موجود" → Works immediately
"سبب جديد مخصص" → Added as inactive, request accepted
"العميل سافر للخارج" → Added as inactive, request accepted
GET /v2/reference/postponed-reasons
GET /v2/reference/return-reasons
postponed_date_id for POSTPONED action
{
"shipment_id": 12345,
"action": "POSTPONED",
"postponed_reason": "العميل غير موجود",
"postponed_reason_en": "Customer not available",
"postponed_reason_ku": "کڕیار بەردەست نییە",
"postponed_date_id": 1 // ✅ REQUIRED!
}
// Valid values:
// 1 = Tomorrow
// 2 = In 2 days
// 3 = In 3 days - Maximum allowed
page & page_size| Question | Answer |
|---|---|
| Can I update a shipment after creation? | Yes, use PUT /v2/shipments/edit to modify shipment information. Only shipments in initial stages (SORTING_CENTER, NEW_CUSTOMER_SHIPMENTS) and initial steps (NEW_ORDER_TO_PRINT, NEW_ORDER_TO_PICKUP, NEW_WITH_PA) can be edited. |
| How do I download reference data? | Add ?download=excel or ?download=json to any reference API |
| What's the difference between shipment_number and airway_bill_number? | shipment_number is your unique tracking ID (used for search). airway_bill_number is optional external reference |
| Can I search by airway_bill_number? | No, use shipment_number for queries. airway_bill_number is display-only |
| How long is the JWT token valid? | Access token: 24 hours. Refresh token: 30 days. Use refresh before expiration |
| Can I test the API without coding? | Yes! Use the Try It Live section below to test directly from browser |
| Do you provide test credentials? | Contact support to request sandbox/test credentials |
| What if I need a new governorate or city? | Contact support to add new locations to the system |
Our technical support team is ready to assist you!
https://jenni.alzaeemexp.com/api
Get a new access token using your refresh token
Login first to enableshipment_history - All status changespartial_history - Same structure as shipment_history (for partial deliveries only)treatment_history - Call center communicationsGet list of all governorates (no authentication required)
Get list of all cities (no authentication required)
Get list of active return reasons
Login first to enableGet list of active postponed reasons
Login first to enableGet complete list of all 40+ unified action codes (no authentication required)
Get list of all supported countries (no authentication required)
Get count of your orders grouped by status
Login first to enableGet list of your merchant stores
Login first to enableGet all your tickets
Login first to enableRefer to the "Complete Code Examples" section for full implementation details
All 34 APIs are available in the tabs above, organized by category:
For complete code examples in all languages (cURL, JavaScript, Python, Java, PHP), check the "Complete Code Examples" section.
Import our Postman collection to test all APIs instantly! All endpoints, examples, and environments pre-configured.
All API endpoints ready to use with sample data
Automatic token extraction and injection in headers
Switch between development, staging, and production
Generate code in any language from Postman
https://jenni.alzaeemexp.com/api/v2/docs/postman-collection
Set up Postman environment variables:
| Variable Name | Production | Description |
|---|---|---|
baseUrl |
https://jenni.alzaeemexp.com/api |
API base URL |
token |
(auto-set after login) | JWT access token |
refreshToken |
(auto-set after login) | JWT refresh token |
systemCode |
YOUR_PROD_CODE |
Your company code |
The collection includes a test script that automatically extracts the token from login response and saves it to the environment. No manual token copying needed!
Import the collection using one of the methods above
Click "Environments" → "Create Environment" → Add baseUrl variable → Set value to https://jenni.alzaeemexp.com/api
Open "Login" request → Update username and password in request body
Click "Send" → Token will be automatically saved to environment
All requests will now use the saved token automatically. Start testing!
Watch our video tutorial on how to set up and use the Postman collection: Coming Soon
Follow this guide to thoroughly test your integration before going to production.
| ☐ | Test Case | Expected Result | How to Test |
|---|---|---|---|
| ☐ | Login with valid credentials | Returns token & refreshToken | POST /v2/auth/login |
| ☐ | Login with invalid credentials | Returns 401 Unauthorized | Use wrong password |
| ☐ | Refresh token before expiration | Returns new tokens | POST /v2/auth/refresh |
| ☐ | Use expired token | Returns 401 Unauthorized | Wait 24+ hours or use old token |
| ☐ | Test Case | Expected Result | Notes |
|---|---|---|---|
| ☐ | Create single shipment with all required fields | Accepted, returns shipment_id | Basic happy path |
| ☐ | Create 100 shipments (max limit) | All accepted (if valid) | Test batch processing |
| ☐ | Try to create 101 shipments | Error: Max 100 allowed | Validate limit enforcement |
| ☐ | Create shipment with duplicate shipment_number | Rejected: DUPLICATE_SHIPMENT | Test duplicate detection |
| ☐ | Create with invalid phone format | Rejected: INVALID_PHONE_NUMBER | Test validation |
| ☐ | Create with invalid governorate code | Rejected: PROCESSING_ERROR (governorate mapping not found) | Test governorate validation |
| ☐ | Create with unknown city name | Accepted (uses unknown district + adds city to address) | System handles unknown cities gracefully |
| ☐ | Create with negative amount | Rejected: INVALID_AMOUNT or NEGATIVE_AMOUNT_NOT_ALLOWED | Test amount validation |
| ☐ | AGGREGATOR user without merchant_id | Rejected: MERCHANT_ID_REQUIRED | Test role-based validation |
| ☐ | Test Case | Expected Result | Notes |
|---|---|---|---|
| ☐ | Query existing shipment | Returns complete shipment info | Verify all fields present |
| ☐ | Query non-existent shipment | Returns empty shipments, shipment in not_found | Test not found handling |
| ☐ | Query 100 shipments (max limit) | Returns all found shipments | Test limit |
| ☐ | Query 101+ shipments | Error: Max 100 allowed | Validate limit enforcement |
| ☐ | Verify step status mapping | current_step has global name (e.g., IN_SC) | Not internal code |
| ☐ | Verify boolean fields | fragile, proof_of_delivery, have_return_item are true/false | Data type check |
| ☐ | Test Case | Expected Result | Notes |
|---|---|---|---|
| ☐ | Postpone shipment with all required fields | Success, status updated to POSTPONED | Include postponed_reason + postponed_date_id |
| ☐ | Postpone without postponed_date_id | Error: MISSING_POSTPONED_DATE_ID | Validate required field |
| ☐ | Postpone with invalid reason text | Error: INVALID_POSTPONED_REASON | Use exact text from reference API |
| ☐ | Return shipment with reason | Success, status updated to RTO_WH | Action: RETURN_TO_STORE |
| ☐ | Partial return with quantity | Success, partial quantity recorded | Action: PARTIAL_DELIVERY with return_quantity |
| ☐ | Deliver successfully | Success, status updated to DELIVERED | Action: SUCCESSFUL_DELIVERY |
| ☐ | Deliver with amount change | Success, amount updated | Action: SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE with new_amount_iqd |
| ☐ | Try invalid action for current status | Error: INVALID_ACTION | Test action availability check |
| ☐ | Test Case | Expected Result |
|---|---|---|
| ☐ | Send empty shipments array | Error or empty result |
| ☐ | Send malformed JSON | 400 Bad Request |
| ☐ | Missing Authorization header | 401 Unauthorized |
| ☐ | Wrong Authorization format (without Bearer) | 401 Unauthorized |
| ☐ | Very long shipment_number (>255 chars) | Rejected: Field too long |
| ☐ | Special characters in fields | Accepted if valid, sanitized if needed |
| ☐ | Unicode/Emoji in text fields | Accepted (Arabic text supported) |
| ☐ | Zero amount (amount_iqd = 0) | Accepted (valid for free samples) |
| ☐ | Update someone else's shipment | 403 Forbidden (ownership check) |
| ☐ | Test Case | Acceptance Criteria | Tool |
|---|---|---|---|
| ☐ | Create 100 shipments simultaneously | Response within 10 seconds | Postman/JMeter |
| ☐ | Query 100 shipments (specific lookup) | Response within 3 seconds | Postman |
| ☐ | Download reference data (Excel) | File downloads successfully | Browser/curl |
| ☐ | Concurrent requests (10 requests/second) | All requests processed successfully | JMeter/k6 |
| ☐ | Network timeout handling | Graceful timeout, retry logic works | Simulate slow network |
Use this test data for comprehensive testing:
{
"system_code": "YOUR_CODE",
"shipments": [
{
"shipment_number": "TEST-001",
"receiver_name": "أحمد علي محمد",
"receiver_phone_1": "07901234567",
"receiver_phone_2": "07801234567",
"governorate_code": "BGD",
"city": "الكرادة",
"address": "شارع 10، بناية 5، الطابق 2",
"amount_iqd": 50000,
"amount_usd": 0,
"quantity": 1,
"product_info": "Samsung Galaxy S24",
"note": "يرجى الاتصال قبل التسليم"
},
{
"shipment_number": "TEST-002",
"receiver_name": "فاطمة حسن",
"receiver_phone_1": "07702345678",
"governorate_code": "BAS",
"city": "المعقل",
"address": "حي الجزائر",
"amount_iqd": 75000,
"quantity": 2,
"is_fragile": true,
"is_proof_of_delivery": true,
"have_return_item": false
},
{
"shipment_number": "TEST-003",
"receiver_name": "عمر خالد",
"receiver_phone_1": "07803456789",
"governorate_code": "NIN",
"city": "الموصل",
"amount_iqd": 0,
"quantity": 1,
"note": "عينة مجانية"
}
]
}
// Test cases that should be REJECTED:
[
{
"shipment_number": "INVALID-PHONE",
"receiver_phone_1": "123456789", // ❌ Wrong format
"amount_iqd": 50000,
// ... other fields
},
{
"shipment_number": "INVALID-GOV",
"governorate_code": "XXX", // ❌ Invalid code
"amount_iqd": 50000,
// ... other fields
},
{
"shipment_number": "NEGATIVE-AMOUNT",
"amount_iqd": -5000, // ❌ Negative amount
// ... other fields
},
{
"shipment_number": "ZERO-QUANTITY",
"quantity": 0, // ❌ Must be >= 1
// ... other fields
}
]
Our Postman collection includes automated tests that verify:
| ☐ | All test cases passed |
| ☐ | Error handling implemented |
| ☐ | Token refresh logic working |
| ☐ | Reference data cached locally |
| ☐ | Logging configured |
| ☐ | Retry mechanism in place |
| ☐ | Validation rules applied |
| ☐ | Team trained on V2 API |
| ☐ | Support contact established |
| ☐ | Production credentials received |
| ☐ | Monitoring/alerts configured |
| ☐ | Rollback plan prepared |
| ☐ | Documentation reviewed |
| ☐ | Go-live date scheduled |
GET responsesDELIVERED - shipment is currently deliveredPOST/PUT requestsSUCCESSFUL_DELIVERY - perform delivery🔄 Workflow:
GET /v2/shipments/query → Receive current_step = "OFD" (Out For Delivery)GET /v2/reference/available-actions → See what you can DOPOST /v2/shipments/update-status with action_code: "SUCCESSFUL_DELIVERY"GET /v2/shipments/query → Receive current_step = "DELIVERED" (new status!)Step statuses represent where the shipment is in its lifecycle (e.g., In Sorting Center, Out For Delivery, Delivered). These are read-only values you receive when querying shipments.
📊 View: This table shows all 26+ possible statuses a shipment can have throughout its journey from creation to final delivery or return.
| Global Name (EN) | Arabic Name (AR) | Description |
|---|---|---|
IN_SC |
داخل مركز الفرز | Shipment received in sorting center |
PRINT_MANIFEST_DA |
طباعة المنفيست لمندوبين التوصيل | Delivery manifest printed for courier |
NEW_WITH_PA |
شحنات جديدة مع مندوب الأستلام | New shipment in transit to branch |
DELIVERY_REATTEMPT |
إعادة محاولة التوصيل | Rescheduled for delivery retry |
RTO_CONFIRMED |
راجع مؤكد | Return confirmed by merchant |
POSTPONED_CONFIRMED |
مؤجل مؤكد | Postponement confirmed by merchant |
PENDING_DELIVERY_APPROVAL |
بانتظار موافقة التسليم | Awaiting delivery confirmation from recipient |
REJECTED_PRICE_CHANGE |
مرفوض تغيير السعر | Price change request refused |
OFD |
قيد التوصيل | Shipment out for delivery with courier |
RTO_WITH_DA |
راجع عند المندوب | Returned shipment with delivery agent |
POSTPONED |
مؤجل | Delivery postponed by recipient request |
RTO_WH |
راجع كلي في المخزن | Shipment has been fully returned to warehouse |
RTO_ARCHIVED |
أرشيف شحنات راجعة | Returned shipment archived |
PARTIALLY_DELIVERED |
تسليم جزئيا أو أستبدال | Shipment partially delivered or item replaced |
DELIVERED_PRICE_CHANGED |
سلمت مع تغيير المبلغ | Delivered with modified amount |
FORCE_DELIVERY |
واصل أجباري | Mandatory delivery required |
DELIVERED |
سلمت بنجاح | Shipment delivered successfully to recipient |
DELIVERED_ARCHIVED |
أرشيف المسلم بنجاح | Successfully delivered shipment archived |
RTO_FROM_BRANCH |
استلام الراجع من الفروع | Returned shipment being collected from branch |
RTO_IN_TRANSIT_WH |
رواجع الفروع في المخزن | Returned shipment in warehouse awaiting branch pickup |
BRANCH_PRINT_MANIFEST |
طباعة منفيست للفروع | Inter-branch manifest printed |
RTO_READY_FOR_BRANCH |
رواجع جاهزة للتسليم للفروع | Returned shipments ready for branch transfer |
WITH_MA |
شحنات في الطريق للفروع | Shipment in mid-mile transit to branch |
NEW_IN_TRANSIT |
شحنات جديدة بين فرعين | Shipment in transit between branches |
NEW_ORDER_TO_PRINT |
جاهز للطبع | Ready for sticker printing |
NEW_ORDER_TO_PICKUP |
جاهز للنقل | Shipment ready for courier pickup |
GET /v2/shipments/query, you receive current_step field with these status codesIN_SC → OFD → DELIVERED or RTO_WHDELIVEREDPARTIALLY_DELIVEREDDELIVERED_PRICE_CHANGEDRTO_WH (In Warehouse)RTO_WITH_DA (With Agent)RTO_ARCHIVEDIN_SC (Sorting Center)OFD (Out For Delivery)POSTPONED| Code | Global Name (EN) | Arabic Name (AR) | Description |
|---|---|---|---|
ANB |
ANBAR | الأنبار | Anbar governorate (Al-Ramadi city) |
ARB |
ERBIL | أربيل | Erbil governorate (Kurdistan Region) |
BAS |
BASRAH | البصرة | Basrah governorate (Southern Iraq) |
BBL |
BABYLON | بابل | Babylon governorate (Hillah city) |
BGD |
BAGHDAD | بغداد | Baghdad governorate (Capital) |
DHI |
DHI_QAR | ذي قار | Dhi Qar governorate (Nasiriyah city) |
DOH |
DUHOK | دهوك | Duhok governorate (Kurdistan Region) |
DYL |
DIYALA | ديالى | Diyala governorate (Baqubah city) |
KRB |
KARBALA | كربلاء | Karbala governorate (Holy city) |
KRK |
KIRKUK | كركوك | Kirkuk governorate (Northern Iraq) |
MTH |
MUTHANNA | المثنى | Muthanna governorate (Samawah city) |
MYS |
MAYSAN | ميسان | Maysan governorate (Al-Amarah city) |
NIN |
NINEVEH | نينوى | Nineveh governorate (Mosul city) |
NJF |
NAJAF | النجف | Najaf governorate (Holy city) |
QAD |
QADISIYYAH | القادسية | Qadisiyyah governorate (Al-Diwaniyah city) |
SAH |
SALAH_AL_DIN | صلاح الدين | Salah al-Din governorate (Tikrit city) |
SMH |
SULAYMANIYAH | السليمانية | Sulaymaniyah governorate (Kurdistan Region) |
WST |
WASIT | واسط | Wasit governorate (Al-Kut city) |