V2 Integration API

Modern, Smart, and Standardized Integration for Delivery Management

Universal V2 Protocol - Compatible with ALL V2 Systems

Version 2.0 - Production Ready

Quick Start Guide

⚠️ STOP! Read This First!

This Quick Start assumes you're already registered with Jenni Logistics.

If you don't have your system_code and credentials yet:

Register First (Required) Registration takes 1-3 business days
Get Started in 3 Simple Steps!
1️⃣
Login
{
  "username": "user",
  "password": "pass"
}
Get your JWT token
2️⃣
Create Shipment
{
  "shipment_number": "001",
  "receiver_name": "أحمد",
  "amount_iqd": 50000
}
Submit your shipments
3️⃣
Track Status
{
  "shipment_numbers": 
    ["001"]
}
Query shipment info
📋 API Summary Table
# 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

API Cheat Sheet

Quick Reference Guide

Bookmark this section for quick access to the most common API patterns and field requirements.

Authentication
# 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}
Create Shipment (Minimum)
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
  }]
}
✅ Required fields only
⚠️ Required: external_shipment_id is mandatory (sender's shipment ID for tracking). Jenni Logistics uses this ID when sending status updates back to you.
💡 Important: Save the shipment_id from response - you'll need it to update shipment status later.
Query Shipments
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)
Update Status (Common Actions)
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"
}
Receive In Mode: You update status in Jenni
📋 Field Requirements by Action (Update Status API)

These requirements apply to POST /v2/shipments/update-status endpoint (Receive In Mode)

Action Required Fields Optional Fields Example
POSTPONED postponed_reason
postponed_date_id
note postponed_date_id: 1 (غداً)
RETURN_TO_STORE
RETURNED_WITH_AGENT
return_reason return_quantity
note
return_quantity for partial returns
TREATED treated_message note Min 3 characters
SUCCESSFUL_DELIVERY - image_url
note
No extra fields needed
SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE new_amount_iqd OR
new_amount_usd
image_url
note
At least one amount required
PARTIAL_DELIVERY - return_quantity
new_amount_iqd
note
Partial delivery/exchange
🎯 Common Governorate Codes
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

⚡ Quick Tips
Do's
  • ✅ Validate phone format: 07XXXXXXXXX
  • ✅ Check for duplicate shipment_number
  • ✅ Use /reference/cities to validate city names
  • ✅ Get /available-actions before updating status
  • ✅ Max 100 shipments in create request
  • ✅ Max 100 shipments/IDs in query request (specific lookup)
  • ✅ Max 100 per page in paginated query mode
Don'ts
  • ❌ Don't use reason codes - use actual text
  • ❌ Don't skip required fields for actions
  • ❌ Don't use incorrect governorate codes
  • ❌ Don't send negative amounts (unless allowed)
  • ❌ Don't exceed max limits (100 create, 100 query, 100 per page)
  • ❌ Don't forget Authorization header
Download Reference Data

Get complete lists in Excel or JSON format:

Note:
  • 🔒 Requires Authentication: Return Reasons, Postponed Reasons
    💡 Login in Try It Live section to download
  • 🌐 Public Access: Governorates, Cities (no authentication needed)

Abbreviations Guide

Understanding Abbreviations

To keep API responses concise and efficient, we use standardized abbreviations for common terms. Here's your complete reference guide.

📝 Standard Abbreviations
Agent Types
Term Abbreviation
Pickup Agent PA
Delivery Agent DA
Midmile Agent MA
Locations & Operations
Term Abbreviation
Warehouse WH
Sorting Center SC
Return To Origin / Returned Orders RTO
Out For Delivery OFD
🔤 Abbreviated Step Codes - Complete List

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
🎯 Common Naming Patterns
Returns Pattern

All return-related steps start with RTO_

  • RTO_CONFIRMED
  • RTO_WITH_DA
  • RTO_WH
  • RTO_ARCHIVED
  • RTO_FROM_BRANCH
  • RTO_IN_TRANSIT_WH
  • RTO_READY_FOR_BRANCH
Delivery Pattern

Delivery-related steps are clear:

  • OFD - Out For Delivery
  • DELIVERED - Success
  • DELIVERED_PRICE_CHANGED
  • DELIVERED_ARCHIVED
  • PARTIALLY_DELIVERED
  • FORCE_DELIVERY
Processing Pattern

New orders and processing:

  • NEW_WITH_PA - With Pickup Agent
  • IN_SC - In Sorting Center
  • NEW_ORDER_TO_PRINT
  • NEW_ORDER_TO_PICKUP
  • NEW_IN_TRANSIT - Between Branches
  • WITH_MA - With Midmile Agent
Quick Abbreviation Decoder
PA = Pickup Agent
DA = Delivery Agent
MA = Midmile Agent
WH = Warehouse
SC = Sorting Center
RTO = Return To Origin
OFD = Out For Delivery
RTO_WH = Return To Warehouse
RTO_DA = Return With DA
IN_SC = In Sorting Center
WITH_MA = With Midmile Agent
NEW_PA = New With Pickup Agent
📐 Naming Guidelines
✅ Do This
  • Use abbreviations consistently
  • Follow RTO_ prefix for all returns
  • Use agent type suffixes (_PA, _DA, _MA)
  • Keep location suffixes (_WH, _SC)
  • Store both step_name (abbreviated like OFD, IN_SC) and step_name_ar (Arabic) for display
❌ Avoid This
  • Don't use old long names (e.g., OUT_FOR_DELIVERY → use OFD)
  • Don't mix old and new naming styles
  • Don't hardcode status names - always use mapper
  • Don't display internal codes to end users - show Arabic names
Migration from Old Names

The API automatically handles both old and new names for backward compatibility:

Old Names (Still Supported):
  • OUT_FOR_DELIVERYOFD
  • IN_SORTING_CENTERIN_SC
  • RETURNED_WITH_AGENTRTO_WITH_DA
  • DELIVERED_SUCCESSFULLYDELIVERED
✅ Recommendation:

Update your systems to use the new abbreviated names for:
• Shorter API responses (less bandwidth)
• Faster parsing
• Industry-standard naming
• Better readability in logs

Overview

IMPORTANT: Registration Required First!

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:

  • ✅ Get your unique system_code (your company identifier)
  • ✅ Receive authentication credentials (TOKEN or USERNAME/PASSWORD)
  • ✅ Configure your domain URL (if needed for webhooks)
API Base URL

Your current API base URL is:

https://jenni.alzaeemexp.com/api

All 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.

Universal V2 Compatibility

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.

What's New in V2?
  • Automatic Status Updates: Real-time webhook notifications sent to YOUR_DOMAIN/v2/push/update-status including agent GPS location
  • Unified Property Names: All properties use snake_case (e.g., tracking_number, sender_merchant_id)
  • Global Step Names: Standardized status names in English and Arabic (e.g., IN_SC, DELIVERED)
  • Professional Terminology: Industry-standard terms (e.g., merchant_settlement instead of customer_payment)
  • Complete Isolation: Independent from legacy system, future-proof architecture
  • Dual Retry Mechanism: Immediate (1s-4s) + Persistent (15min-60min) for all V2 systems
  • Bidirectional Support: Full two-way integration with external_shipment_id tracking
  • Comprehensive Documentation: Clear examples, error codes, and use cases

Registration & Setup Guide

⚠️ STOP! Read This First!

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:

  • You choose your system_code (e.g., YOUR_COMPANY_NAME)
  • You provide it in your registration email
  • Jenni Logistics confirms it's available and activates it
  • Jenni Logistics does NOT assign system_code to you - you create it!

📖 Please read the section below about "What is system_code?" before proceeding.

Required Before Integration

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.

What is 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.

Examples of system_code:
  • ECOMMERCE_STORE_01 - E-commerce website
  • ABC_DELIVERY_CO - Delivery company
  • PHARMACY_CHAIN_IRAQ - Pharmacy chain
  • MARKETPLACE_APP - Mobile marketplace
Important Rules:
  • ✅ Use uppercase letters and underscores
  • ✅ Make it descriptive and memorable
  • ✅ Keep it short (max 50 characters)
  • ❌ No spaces or special characters (except _)
  • ❌ Must be unique - no duplicates allowed

⚠️ CRITICAL: Who Provides system_code?

YOU provide the system_code, NOT Jenni Logistics!

  • You choose your own unique identifier (e.g., YOUR_COMPANY_NAME)
  • You include it in your registration email
  • Jenni Logistics confirms it's available and not already taken
  • If available: It gets activated for your account
  • If taken: You'll be asked to choose a different one

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.

Step-by-Step Registration Process

1
Determine Your Integration Type

Choose which integration mode suits your business:

Receive In Mode

You send shipments → Jenni Logistics delivers

  • E-commerce websites
  • Mobile apps
  • Merchants & stores
Push Out Mode

Jenni Logistics sends shipments → You deliver

  • Delivery companies
  • Logistics providers
  • Branch hubs
2
Choose Authentication Method
LOGIN (Username/Password)

Username and password authentication with dynamic tokens

✓ Secure ✓ Token rotation
Note: Jenni Logistics provides username and password credentials. You use these to login and receive access tokens.
3
Prepare Required Information

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:
  • Receive In Mode with Webhook (you want automatic status updates sent to your system)
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:
  • TOKEN Method: Provide your static token
  • LOGIN Method: Provide username and password
Example for TOKEN: YOUR_STATIC_TOKEN
Example for LOGIN: Username: jenni_api_user, Password: your_secure_password
Your Domain/URL Conditional REQUIRED for:
  • Push Out Mode (Jenni Logistics sends shipments to you)
  • Receive In Mode with Webhook (you want automatic status updates)
⚠️ IMPORTANT:
If you want automatic webhook status updates sent to your system, you MUST provide:
  1. Your Domain/URL (where Jenni Logistics will send updates)
  2. Your Authentication Method (TOKEN or LOGIN)
  3. Your Authentication Credentials:
    • If TOKEN: Your static token
    • If LOGIN: Your username and password

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
4
Contact Jenni Logistics Technical Team

Send your registration request with all required information to:

Email
Primary contact method
Phone/WhatsApp
+964 790 XXX XXXX For urgent matters
Email Template

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]
5
Confirm Your Registration

Within 1-3 business days, Jenni Logistics technical team will confirm your registration and provide you with:

LOGIN Authentication Credentials:
  • 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)
  • API Base URL - Jenni Logistics API endpoint
  • Documentation link - This page!

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.

Security Best Practices:
  • 🔒 Never share your credentials publicly (GitHub, forums, etc.)
  • 🔒 Store securely - Use environment variables, not hardcoded values
  • 🔒 Change password immediately after first login (for LOGIN method)
  • 🔒 Use HTTPS only - never send credentials over HTTP
  • 🔒 Rotate tokens regularly for production systems
6
Test Your Integration

Start with testing in the following order:

Ready to Start?

Once testing is successful, you can gradually increase volume and move to production!

Common Registration Questions

How long does registration take?

Usually 1-3 business days. Urgent requests can be processed within 24 hours - mention urgency in your email.

Is there a registration fee?

Contact the sales team for pricing information. Technical integration setup is typically included in service packages.

Can I change my system_code later?

Not recommended after going live as it requires reconfiguration. Choose wisely! Test systems can be changed easily.

Can I have multiple system_codes?

Yes! Large companies often have separate codes for different divisions (e.g., COMPANY_WEBSITE, COMPANY_APP, COMPANY_WHOLESALE).

Do I need a domain to integrate?

Only if:

  • You're using Push Out Mode (Jenni Logistics sends to you)
  • You want automatic webhook notifications (Receive In with webhooks)
⚠️ Important: If you do NOT provide your Domain/URL and Authentication credentials during registration, automatic webhook status updates will NOT be sent to your system. You will need to manually query shipment status using POST /v2/shipments/query instead.
For simple Receive In with manual queries, no domain needed!

Can I test before going live?

Absolutely! Request test credentials first. Test with small volumes, then request production credentials when ready.

Integration Modes

Understanding Integration Modes

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.

Quick Decision Tree

Who will handle the delivery?

You deliver

Mode 1: Push Out

Jenni → Your System

  • ✅ Implement /v2/shipments/create on YOUR server
  • ✅ Receive shipments from Jenni Logistics
  • ✅ Call /v2/push/update-status on Jenni
View Code Examples
Jenni delivers

Mode 2: Receive In

Your System → Jenni

  • ✅ Call /v2/auth/login on Jenni
  • ✅ Call /v2/shipments/create on Jenni
  • ✅ Track with /v2/shipments/query
View Code Examples

Mode 1: Push Out

Jenni Logistics sends shipments to external systems

Direction: Jenni → External System
When to Use:
  • Your system receives shipments FROM Jenni
  • Jenni creates the shipment first
  • You act as a delivery service provider
  • You need to update delivery status back to Jenni Logistics
How it Works:
1
Jenni Creates Shipment Customer places order in Jenni
2
Jenni Pushes to You POST /v2/shipments/create On YOUR server
3
You Process Delivery Your drivers deliver the shipment
4
You Send Status Updates POST /v2/push/update-status On Jenni Logistics server
APIs You Need to Implement:
POST /v2/shipments/create On YOUR server - Receive shipments from Jenni Logistics
Auto-Trigger: Jenni Logistics sends shipments automatically to your endpoint when they are assigned to is_dlv_agent. Real-time push, no polling needed!
APIs You Will Call:
POST /v2/push/update-status On Jenni Logistics server - Send status updates to Jenni Logistics
IMPORTANT: Include required fields based on action:
  • POSTPONED: postponed_reason + postponed_date_id
  • RETURNED_*: return_reason
  • DELIVERY_REATTEMPT: treatment details in note
Configuration Required:
is_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
Use Case Examples:
Third-Party Delivery Service

External delivery company receives orders from Jenni Logistics

Branch/Hub System

Branch or collection center receives shipments from main system

Partner Logistics Provider

Logistics partner receives specific shipments

Mode 2: Receive In

External systems send shipments to Jenni Logistics

Direction: External System → Jenni
When to Use:
  • Your system sends shipments TO Jenni
  • You create the shipment first
  • You need Jenni Logistics to handle delivery
  • You need to track shipment status in your system
How it Works:
1
You Create Shipment Customer places order in your system
2
You Push to Jenni Logistics POST /v2/shipments/create On Jenni Logistics server
3
Jenni Processes Delivery Jenni drivers deliver the shipment
4
Auto Status Updates Jenni → POST /v2/push/update-status On YOUR server (automatic)
5
Manual Query (Optional) POST /v2/shipments/query Check status anytime

Automatic Status Updates from Jenni Logistics:

  • ✅ Jenni Logistics automatically sends real-time status updates to YOUR_DOMAIN/v2/push/update-status (on YOUR server)
  • ✅ Updates include: action, current step, stage, agent GPS location, and more
  • ✅ Instant notifications when agent delivers, postpones, or returns shipment
  • ✅ You can also use POST /v2/shipments/query for manual checks anytime
  • ⚠️ You must implement POST /v2/push/update-status on YOUR server to receive these updates

Required Configuration:

In Jenni Logistics system settings, configure:

  • Your Domain: e.g., https://yourstore.com
  • Auth Method: TOKEN or LOGIN
  • Integration Type: Receive In (V2)

Jenni Logistics will send updates to: YOUR_DOMAIN/v2/push/update-status

APIs You Must Implement on YOUR Server:
POST /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

APIs You Will Call:
POST /v2/shipments/create Create new shipments in Jenni
POST /v2/shipments/query Query shipment status from Jenni Logistics
POST /v2/shipments/update-status Update shipment status (if needed)
PUT /v2/shipments/edit Edit shipment information
Configuration Required:
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
Use Case Examples:
E-commerce Website

E-commerce website sends its orders to Jenni Logistics

Mobile App / Marketplace

Mobile application or online marketplace

Merchant POS System

Point of sale system for merchants

Warehouse Management System

Warehouse management system

Quick Comparison

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 = TRUE
is_v2_push_url = YOUR_URL
is_dlv_agent = AGENT_ID
system_code = YOUR_CODE
tokenToreceiveCases = YOUR_TOKEN
is_rcv_agent = AGENT_ID
Main APIs you use Implement /receive
Call /update-status
Call /create
Call /query
Status updates flow You → Jenni Jenni → You (webhook or polling)
Technical complexity Medium - Need server endpoint Low - Just REST API calls

Which Mode Should I Choose?

Choose Push Out If:
  • You are a delivery/logistics company
  • You have your own fleet of drivers
  • You want to receive shipments FROM Jenni Logistics customers
  • You need to manage delivery operations in your system
  • You have a server that can receive API calls
Example: Delivery company wants to receive orders from Jenni Logistics customers
Choose Receive In If:
  • You are an e-commerce business
  • You sell products online/offline
  • You need Jenni Logistics TO deliver your shipments
  • You want to track shipments from your system
  • You prefer simple REST API integration
Example: E-commerce store wants Jenni Logistics to deliver its orders to customers
Important Notes
  • Authentication: Each mode has its own authentication flow. Make sure to configure the correct credentials.
  • Testing: Always test with a small batch first before going live.
  • Questions? See FAQ section for detailed answers about modes, authentication, and more.

Webhook Integration Guide

What is a Webhook?

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).

Without Webhook (Polling)

❌ Your server asks Jenni Logistics every 5 minutes: "Any updates?"

Wastes resources, delays, not real-time

With Webhook (Push)

✅ Jenni Logistics tells YOUR server instantly: "Status changed!"

Real-time, efficient, no delays

When Do You Need Webhooks?

You NEED Webhooks If:
  • ✅ You send shipments to Jenni Logistics (Receive In Mode)
  • ✅ You want real-time customer notifications (SMS/Email)
  • ✅ You need to update order status in YOUR database automatically
  • ✅ You want to track agent GPS location in real-time
  • ✅ You display "Track Order" page to customers on your website
You DON'T Need Webhooks If:
  • You only check status manually (admin dashboard)
  • Status updates every 30+ minutes are acceptable
  • You prefer calling Query API yourself when needed
  • You're in testing phase (use Query API first)

You can always use POST /v2/shipments/query instead

How Webhook Works - Visual Flow

Step 1: You Send Shipment to Jenni Logistics

You call: POST /v2/shipments/create

Step 2: Jenni Logistics Agent Processes Shipment

Agent picks up, delivers, or updates status

Step 3: Jenni Logistics Sends Webhook to YOUR Server

POST to: YOUR_DOMAIN/v2/push/update-status

Step 4: Your Server Processes Update

Update database, send customer SMS/Email

Step 5: Customer Sees Update

Real-time status on your website/app

CRITICAL: What You MUST Implement

1️⃣ Create This Endpoint on YOUR Server:
POST YOUR_DOMAIN/v2/push/update-status

This endpoint receives automatic notifications from Jenni Logistics

2️⃣ Provide Your Domain to Jenni Logistics Team:

Example:

https://yourstore.com

https://api.yourstore.com

Jenni Logistics will automatically send updates to: YOUR_DOMAIN/v2/push/update-status

What Jenni Logistics Sends to Your Webhook (Request Body)

When a shipment status changes, Jenni Logistics will send a POST request to your endpoint with this JSON body:

Important: Shipment Identification

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 number
  • shipment_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
    }
  ]
}
Shipment Identification Fields:

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 Descriptions:
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., "کڕیار ڕەتکردەوە")

Implementation Examples (All Languages)

// 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> handleJenniWebhook(
            @RequestBody Map request) {
        
        try {
            log.info("📬 Received webhook from Jenni Logistics: {}", request);

            String systemCode = (String) request.get("system_code");
            List> updates = (List) request.get("updates");

            // 1. Verify system_code for security
            if (!"YOUR_SYSTEM_CODE".equals(systemCode)) {
                log.error("❌ Invalid system_code: {}", systemCode);
                return ResponseEntity.status(401).body(Map.of(
                    "success", false,
                    "message", "Invalid system code"
                ));
            }

            // 2. Process each update
            for (Map update : 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
                String orderNumber = (String) update.get("shipment_number"); // Your tracking number
                Long jenniShipmentId = ((Number) update.get("shipment_id")).longValue(); // Always available - Jenni's unique ID
                String action = (String) update.get("action_code");
                String status = (String) update.get("current_step");
                String statusAr = (String) update.get("current_step_ar");

                log.info("\n🔄 Processing update for order: {} (Jenni ID: {})", orderNumber, jenniShipmentId);
                log.info("   Action: {}", action);
                log.info("   Status: {} ({})", status, statusAr);

                // Optional: Log GPS location
                if (update.get("agent_latitude") != null && update.get("agent_longitude") != null) {
                    log.info("   📍 Agent Location: {}, {}", 
                        update.get("agent_latitude"), 
                        update.get("agent_longitude")
                    );
                }

                // 3. Update database
                // You can use either shipment_number or shipment_id to find the order
                orderService.updateOrderStatus(
                    orderNumber, // Use this if you match by order number
                    jenniShipmentId, // Use this if you store Jenni's shipment_id
                    status,
                    statusAr,
                    action,
                    (String) update.get("note"),
                    (Double) update.get("agent_latitude"),
                    (Double) update.get("agent_longitude"),
                    (Double) update.get("amount_iqd")
                );

                // 4. Send customer notification
                switch (action) {
                    case "SUCCESSFUL_DELIVERY":
                        smsService.sendSMS(
                            orderNumber,
                            "Your order has been delivered successfully! 🎉"
                        );
                        break;
                    
                    case "POSTPONED":
                        String postponedReason = (String) update.getOrDefault(
                            "postponed_reason_en", 
                            update.getOrDefault("postponed_reason", "Unknown reason")
                        );
                        smsService.sendSMS(
                            orderNumber,
                            "Delivery postponed: " + postponedReason + ". Will contact you soon."
                        );
                        break;
                    
                    case "RETURNED_WITH_AGENT":
                        String returnReason = (String) update.getOrDefault(
                            "return_reason_en",
                            update.getOrDefault("return_reason", "Unknown reason")
                        );
                        smsService.sendSMS(
                            orderNumber,
                            "Order returned: " + returnReason
                        );
                        break;
                }

                log.info("   ✅ Order {} updated successfully", orderNumber);
            }

            // 5. Return success response
            Map<String, Object> response = new HashMap<>();
            response.put("success", true);
            response.put("message", "Successfully processed " + updates.size() + " update(s)");
            response.put("received_count", updates.size());

            return ResponseEntity.ok(response);

        } catch (Exception e) {
            log.error("❌ Error processing webhook: {}", e.getMessage(), e);
            return ResponseEntity.status(500).body(Map.of(
                "success", false,
                "message", e.getMessage()
            ));
        }
    }
}

Webhook Examples for Different Actions

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"
  }]
}
postponed_date_id values:
  • 0 = Empty/null (no date specified)
  • 1 = Today or tomorrow (within 1 day)
  • 2 = In 2 days (day after tomorrow)
  • 3 = In 3 or more days

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"
  }]
}

Testing Your Webhook

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"
    }]
  }'
Expected Response from YOUR Server:
{
  "success": true,
  "message": "Successfully processed 1 update(s)",
  "received_count": 1
}

Common Issues & Troubleshooting

Issue 1: Webhook Not Receiving Updates

Possible Causes:

  • Your domain not configured in Jenni Logistics system
  • Firewall blocking Jenni's IP addresses
  • SSL certificate issues (must use HTTPS)
  • Endpoint path incorrect (must be exactly /v2/push/update-status)

Solution: Contact Jenni Logistics technical team to verify configuration and check server logs

Issue 2: Receiving Updates But Errors in Processing

Possible Causes:

  • Database connection issues
  • Null values not handled properly
  • Wrong field names in YOUR code

Solution: Add comprehensive error logging and handle null values safely

Issue 3: Webhook Called Multiple Times

Possible Causes:

  • Your server returned error (Jenni retries on failure)
  • Response took too long (timeout → retry)

Solution: Implement idempotency - check if update already processed before saving to database

Issue 4: Security Concerns

Best Practices:

  • ✅ Always verify system_code
  • ✅ Use HTTPS only (never HTTP)
  • ✅ Implement rate limiting
  • ✅ Log all webhook requests for audit
  • ✅ Use IP whitelist if possible (ask Jenni Logistics for IPs)
Webhook Best Practices Summary
DO:
  • ✅ Respond with { "success": true } IMMEDIATELY
  • ✅ Process heavy operations asynchronously (queue/background job)
  • ✅ Verify system_code for security
  • ✅ Handle null values safely (use ?? null or defaults)
  • ✅ Log all webhook requests for debugging
  • ✅ Implement idempotency (prevent duplicate processing)
  • ✅ Use agent GPS location to show customer real-time tracking
DON'T:
  • ❌ Don't perform slow operations before responding
  • ❌ Don't return error for valid requests (Jenni will retry)
  • ❌ Don't ignore webhook errors (fix them immediately)
  • ❌ Don't expose webhook endpoint publicly without authentication
  • ❌ Don't assume all fields are present (check for null)
  • ❌ Don't process duplicate updates multiple times

Integration Mode Examples

Complete End-to-End Examples

This section provides complete, working examples for both integration modes. Each example includes authentication, error handling, and real-world scenarios.

Mode 1: Push Out - Complete Implementation

Jenni Logistics sends shipments to your system → You deliver → You send status updates back

Scenario: Third-Party Delivery Service

Your Role: Delivery company that receives orders from Jenni Logistics and delivers them

What You Need to Do:

  1. Implement /v2/shipments/create endpoint on YOUR server to receive shipments from Jenni Logistics
  2. Receive shipments from Jenni Logistics automatically
  3. Process deliveries with your drivers
  4. Send status updates back to Jenni Logistics via POST /v2/push/update-status
Step 1: Implement Receive Endpoint (Your Server)
// 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...');
});
Step 2: Send Status Updates to Jenni Logistics Server
Important: After receiving shipments from Jenni Logistics, you need to send status updates back to Jenni Logistics server when delivery status changes (e.g., delivered, postponed, returned).
// 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
};
Complete Workflow Example
// 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');
Step 1: Implement Receive Endpoint (Your Server)
# 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...')
Step 2: Send Status Updates to Jenni Logistics Server
Important: After receiving shipments from Jenni Logistics, you need to send status updates back to Jenni Logistics server when delivery status changes (e.g., delivered, postponed, returned).
# 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')
Step 1: Implement Receive Endpoint (Your Server)
<?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
}
?>
Step 2: Send Status Updates to Jenni Logistics Server
Important: After receiving shipments from Jenni Logistics, you need to send status updates back to Jenni Logistics server when delivery status changes (e.g., delivered, postponed, returned).
<?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');
?>
Step 1: Implement Receive Endpoint (Your Server)
// 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);
    }
}
Step 2: Send Status Updates to Jenni Logistics Server
Important: After receiving shipments from Jenni Logistics, you need to send status updates back to Jenni Logistics server when delivery status changes (e.g., delivered, postponed, returned).
// 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 Required in Jenni Logistics System

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

Integration Workflow

Complete Integration Flow

Follow this step-by-step workflow to successfully integrate with our V2 API.

1
Registration & Setup
  • Contact support to register your company
  • Receive your system_code (company identifier)
  • Get your login credentials (username and password)
  • Determine your role: MERCHANT or AGGREGATOR
📧 Contact: thulfiqar.a@wedatum.co to get started
2
Authentication
POST https://jenni.alzaeemexp.com/api/v2/auth/login
{
  "username": "your_username",
  "password": "your_password"
}

Response:
{
  "token": "eyJhbGciOi...",
  "refreshToken": "eyJhbGciOi..."
}
⏱️ Token Validity: Access token = 24 hours, Refresh token = 30 days
3
Download Reference Data (One-time)

Download and cache these lists locally for validation:

  • GET /v2/reference/governorates?download=excel
  • GET /v2/reference/cities?download=excel
  • GET /v2/reference/return-reasons?download=excel
  • GET /v2/reference/postponed-reasons?download=excel
💡 Tip: Cache these lists locally and refresh weekly to stay up-to-date
4
Create Shipments
POST 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
    }
  ]
}
✅ Result: You receive shipment_id for each accepted shipment
⚠️ Important: Store the shipment_id in your database - you'll need it when updating shipment status or tracking shipments.
5
Query & Track 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
6
Update Status (Optional)

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
}
7
Monitor & Statistics
# 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
✨ Integration Best Practices
Recommended Practices
  • Cache reference data: Download governorates/cities once, update weekly
  • Token refresh: Implement automatic refresh before expiration
  • Batch operations: Group shipments (up to 100) for efficiency
  • Error handling: Parse error_code and field to show user-friendly messages
  • Available actions: Always check before allowing user to perform action
  • Validate locally: Check phone format, amounts, etc. before API call
Performance Tips
  • Use bulk APIs: Create/query multiple shipments in one request
  • Implement retry logic: For network failures (with exponential backoff)
  • Handle timeouts: Set reasonable timeout (30 seconds recommended)
  • Log API calls: Keep request/response logs for debugging
  • Monitor rate limits: Don't exceed recommended request rates
✅ Integration Checklist
Before Going Live:
  • ☐ Test login with your credentials
  • ☐ Successfully create test shipments
  • ☐ Query shipments and parse response
  • ☐ Test update status operations
  • ☐ Download all reference data
  • ☐ Implement token refresh logic
Production Ready:
  • ☐ Error handling implemented
  • ☐ Validation rules applied
  • ☐ Logging configured
  • ☐ Retry mechanism in place
  • ☐ Reference data cached
  • ☐ Performance tested

Key Features

Secure Authentication

JWT token-based authentication with 24h access + 30-day refresh tokens

Unified Action Codes

Professional, standardized action codes (SUCCESSFUL_DELIVERY, RETURN_TO_STORE, etc.)

Global Standards

26 step statuses + 40+ action codes mapped to international conventions

Multilingual Support

Full Arabic + English responses with RTL/LTR support

Bulk Operations

Create up to 100 shipments or query up to 100 in a single API call. Pagination supports 100 per page.

Field-Level Validation

Detailed validation with specific error codes and field-level rejection reasons

Geographic Validation

Real-time validation for 18 governorates and 100+ cities across Iraq

Multi-Currency Support

Accept payments in IQD and USD with automatic conversion tracking

Reference Data APIs

Access governorates, cities, reasons, and action codes (some public, some protected)

Flexible Downloads

Export reference data in Excel or JSON formats for offline use

Partial Returns

Support for partial delivery/returns with quantity and amount tracking

Interactive Documentation

Live API tester, 5 language examples (cURL, JS, Python, Java, PHP)

Bidirectional Integration

Full V2 support for any two systems with external_shipment_id tracking and status synchronization

Dual Retry Mechanism

Immediate retry (1s-4s) + Persistent retry (15min-60min) for all V2 integrations

Smart Shipment Lookup

4-tier priority search for all V2 systems: shipment_id → external_id → sentToCaseId → receipt number

Sender Information

Track sender_name and sender_phone from any V2 source system for complete origin details

More Features
  • ✅ Real-time status tracking
  • ✅ Postponement up to 3 days
  • ✅ Image upload for proof of delivery
  • ✅ Pagination for large datasets (100/page)
  • ✅ Bidirectional V2 integration support
  • ✅ Comprehensive audit logging
  • ✅ Agent assignment & tracking
  • ✅ Amount change handling
  • ✅ Duplicate detection (batch optimized)
  • ✅ Persistent retry (24h window)
  • ✅ Statistics & reporting APIs
  • ✅ Delete/recall shipments
  • ✅ Edit shipment details
  • ✅ Generate shipping stickers
  • ✅ V1 & V2 compatibility

Push & Status Update APIs (Push Out Mode)

Understanding Push Out Mode

Push Out Mode means Jenni Logistics pushes shipments to your delivery system. This involves two-way communication:

  1. Jenni → Your Server: Jenni Logistics automatically sends new shipments to your endpoint
  2. Your Server → Jenni: You send status updates back to Jenni Logistics when delivery status changes
Two Different Servers

Important: The two APIs below run on different servers:

Step 1: Jenni Logistics → Your Server
  • What: Jenni Logistics sends shipments to you
  • Endpoint: YOUR_DOMAIN/v2/shipments/create
  • You must: Implement this endpoint on YOUR server
Step 2: Your Server → Jenni
  • What: You send status updates back to Jenni Logistics
  • Endpoint: JENNI_DOMAIN/v2/push/update-status
  • You must: Call Jenni's endpoint when status changes
Automatic Trigger: When shipments are assigned to the delivery agent configured in is_dlv_agent, Jenni Logistics automatically sends them to your endpoint (is_v2_push_url) in real-time. No manual triggering required!
POST /v2/shipments/create On YOUR Server Step 1: Jenni Logistics → You

Purpose: Jenni Logistics automatically sends shipments to this endpoint on YOUR server when they are assigned to your delivery agents.

Important: You must implement this endpoint on YOUR server, not on Jenni Logistics server. Jenni Logistics will call this endpoint to push shipments to your system.
Configuration:
  • In Jenni Logistics integration settings, configure is_v2_push_url to your domain (e.g., https://yourstore.com or https://yourstore.com/api)
  • Jenni automatically appends /v2/shipments/create to your configured URL
  • Final endpoint: YOUR_DOMAIN/v2/shipments/create
  • ⚠️ You MUST implement this exact path on your server
Request Headers (Jenni Sends to Your Server):
POST 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)
Request Body:
{
  "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
    }
  ]
}
CRITICAL: shipment_id vs shipment_number
  • shipment_id: Unique case identifier - ALWAYS unique across entire system
  • shipment_number: Receipt/tracking number - CAN BE DUPLICATED across different customers
  • ⚠️ NEVER use shipment_number alone for matching - it can be duplicate!
  • ✅ ALWAYS use shipment_id for safe matching, or shipment_number + customer_id combination
Jenni-to-Jenni Integration Support

Jenni now sends BOTH field formats for compatibility:

  • external_shipment_id = shipment_id (for receiving Jenni Logistics instance)
  • city = city_name (both formats sent)
  • ✅ If receiving system is another Jenni Logistics instance, it will accept the shipment using external_shipment_id
  • ✅ If receiving system is non-Jenni, it uses shipment_id and city_name as before

Result: Jenni Logistics Push Out format is now 100% compatible with Jenni Logistics Receive In format!

CRITICAL: Response Format Requirements

Your endpoint MUST return the following format EXACTLY:

  • accepted_shipments array (not "results")
  • rejected_shipments array (for failed shipments)
  • summary object with total_requested, accepted_count, rejected_count
  • success boolean, message string, timestamp string
  • DO NOT use "results" array - this format is deprecated
Expected Response (REQUIRED FORMAT):
{
  "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
  }
}
Response Fields Explained:
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
Implementation Example (Your Server):
@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);
    }
}
POST /v2/push/update-status On Jenni Logistics Server Step 2: You → Jenni

Purpose: Your system calls this endpoint on Jenni Logistics server to send status updates back when delivery status changes.

Important: This endpoint exists on Jenni server. You call this endpoint from YOUR server to send status updates to Jenni Logistics (e.g., when driver delivers, postpones, or returns shipment).
Request (You Send to Jenni Logistics):
{
  "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"
    }
  ]
}
Implementation Example (You Call Jenni):
// 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);
}

Global Action Codes

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

Authentication Methods

Method 1: Static TOKEN

Best for: Simple integrations with fixed token

Configuration:

Admin Configuration Required: Contact your system administrator to configure static token authentication for your integration.

How it Works:
  • Jenni reads token from database
  • Sends as Authorization: Bearer {token}
  • No expiration - token never changes
  • Simple but less secure
Method 2: Dynamic LOGIN

Best for: Secure integrations with token refresh

Configuration:

Admin Configuration Required: Contact your system administrator to configure LOGIN authentication with username/password for your integration.

How it Works:
  • Jenni calls your login endpoint first
  • Gets JWT token with expiration
  • Caches token and auto-refreshes
  • More secure, supports rotation

Best Practices & Security

DO:
  • ✅ Always use shipment_id for matching - it's unique
  • ✅ Return shipment_id in response, not receipt number
  • ✅ Validate authentication on every request
  • ✅ Log all incoming shipments with timestamps
  • ✅ Handle duplicate shipments gracefully (idempotency)
  • ✅ Send status updates immediately when status changes
  • ✅ Use global action codes (SUCCESSFUL_DELIVERY, etc.)
  • ✅ Include proof of delivery images when available
DON'T:
  • ❌ Never use receipt number alone for matching
  • ❌ Never search database by receipt without customer filter
  • ❌ Don't modify shipment_id or shipment_number
  • ❌ Don't store tokens in plain text
  • ❌ Don't skip validation - check every field
  • ❌ Don't send status updates in batches - send immediately
  • ❌ Don't use custom action codes - use global standards
  • ❌ Don't ignore partial delivery scenarios

Retry Mechanism & Reliability

Built-in Reliability for All V2 Systems

V2 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.

How Automatic Retry Works

When ANY V2 system sends requests (shipments or status updates), it automatically retries failed requests:

Retryable Errors

V2 systems will automatically retry these errors:

  • Network errors (connection refused, timeout, reset)
  • Server errors (500, 502, 503, 504)
  • Rate limiting (429 Too Many Requests)
  • Request timeout (408)
Result: Temporary issues are automatically recovered
Non-Retryable Errors

V2 systems will NOT retry these errors:

  • Authentication errors (401, 403)
  • Not found (404)
  • Bad request (400)
  • Validation errors (422)
Result: Immediate failure (requires fixing the issue)

Retry Configuration

⚠️ Important: Jenni Logistics uses TWO different retry mechanisms:
  • Immediate Retry (for transient network errors): Fast exponential backoff within seconds
  • Persistent Retry (for failed status updates): Saves to database and retries over hours
Immediate Retry (Network 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
Persistent Retry (Status Updates)

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
How it works: Failed status updates are saved to a persistent retry queue. A background job runs every 5 minutes to process pending updates. This ensures reliable delivery even if your system is temporarily down.

Retry Timeline Examples

Immediate Retry: Network Timeout (Recovers on 2nd Attempt)
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)
Immediate Retry: Server Error (Recovers on 3rd Attempt)
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)
Persistent Retry: Status Update Failure (Your System Down)
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)
Persistent Retry: All Attempts Exhausted
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
Scenario: Authentication Error (No Retry)
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)

What This Means for You

Best Practices
  • Handle Timeouts Gracefully: Your server has 30 seconds to respond
  • Use Idempotency: Same request may arrive multiple times if first attempt times out
  • Return Proper HTTP Status:
    • 5xx for temporary issues (will retry)
    • 4xx for permanent issues (no retry)
  • Log Request IDs: Track duplicate requests using shipment_id
  • Implement Rate Limiting: Return 429 if too many requests (V2 systems will backoff)
Important Notes
  • ⚠️ Duplicate Detection: If your server responds after timeout, V2 system may retry. Check for duplicates using shipment_id
  • ⚠️ Rate Limiting: V2 systems respect 429 responses and will retry with exponential backoff
  • ⚠️ Response Time: Respond within 30 seconds to avoid timeout and retry
  • ⚠️ Partial Success: If batch processing, return detailed status for each shipment

Implementing Idempotency

To 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);
}
Best Practice: Store processed shipment IDs in Redis/Cache with 7-day TTL to detect duplicates

Implementing Rate Limiting

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);
}
Note: V2 systems automatically retry 429 responses with exponential backoff

Monitoring & Troubleshooting

Key Metrics to Monitor:
  • 📊 Response Time: Should be under 30 seconds
  • 📊 Success Rate: Track 2xx vs 4xx/5xx responses
  • 📊 Duplicate Rate: How many duplicate requests received
  • 📊 Error Types: Which errors are most common
  • 📊 Peak Load: Maximum requests per minute
  • 📊 Average Batch Size: Shipments per request
  • 📊 Timeout Rate: Requests exceeding 30s
  • 📊 Rate Limit Hits: How often 429 returned
Common Issues & Solutions:
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
Summary
Immediate Retry (Network/Server Errors):
  • ✅ Automatic retry for transient errors (network, 5xx, 429)
  • ✅ Fast exponential backoff: 1s → 2s → 4s
  • ✅ Maximum 3 immediate retry attempts
  • ✅ Total window: ~7 seconds
  • ✅ Transparent to end user
Persistent Retry (Failed Status Updates):
  • ✅ Saves failed updates to database
  • ✅ Scheduled retries: 15min → 30min → 60min
  • ✅ Maximum 3 persistent attempts
  • ✅ Total window: ~2 hours
  • ✅ Background job runs every 5 minutes
  • ✅ Expires after 24 hours if still failing
Best Practices (All V2 Systems):
  • ✅ Implement idempotency to handle duplicate requests safely
  • ✅ Respond within 30 seconds to avoid timeouts
  • ✅ Return proper HTTP status codes (4xx for permanent, 5xx for temporary)
  • ✅ Use shipment_id or external_id for duplicate detection
  • ✅ Return 429 with Retry-After header for rate limiting
  • ✅ Support V2 response format: {"success": true/false, "message": "..."}

Authentication

POST /v2/auth/login

Authenticate user and receive JWT access token and refresh token.

Request Body:
{
  "username": "john.doe",
  "password": "yourSecurePassword123"
}
Response (Success):
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
POST /v2/auth/refresh

Refresh your JWT token before expiration.

Request Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
How to Use Your Token

Include the token in the Authorization header as Bearer {token} for all subsequent API calls.

Create Shipments

POST /v2/shipments/create

Create shipments in bulk (maximum 100 shipments per request).

Important Notes:
  • ⚠️ Maximum Limit: You can create up to 100 shipments per request
  • system_code: Use your registered company/system name (provided during registration)
  • 🔗 external_shipment_id (NEW - REQUIRED): Sender's original shipment ID for bidirectional tracking. Jenni Logistics will use this ID when sending status updates back to you.
  • ⚠️ If using ID as receipt number: Provide the SAME value in both shipment_number AND external_shipment_id fields
  • If you are a MERCHANT: store_id is optional
  • If you are an AGGREGATOR: merchant_id is required
Request Body Example:
{
  "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
    }
  ]
}
Response Example:
{
  "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
  }
}
Request Fields:
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)
Available Governorate Codes (Corrected)

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.

Query Shipments

POST /v2/shipments/query Auth Required

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).

Three Query Modes:
  1. MODE 1: Query by specific shipment_ids (Case IDs) - max 100
  2. MODE 2: Query by specific shipment_numbers (tracking numbers) - max 100
  3. MODE 3: Paginated list of all user's shipments - use page & page_size (max 100 per page)
📋 Request Examples:

{
  "shipment_ids": [12345, 12346, 12347]
}
Returns only the requested shipments (if owned by user)

{
  "shipment_numbers": ["SHIP001", "SHIP002", "SHIP003"]
}
Returns only the requested shipments (if owned by user)

{
  "page": 0,
  "page_size": 20
}
Returns all shipments owned by the authenticated user (paginated)
Default Behavior: If no parameters are provided, returns first 100 shipments
📤 Response Examples:

{
  "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
  }
}
Includes 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
  }
}
Includes pagination metadata for navigating through results
Query Response Includes:
  • Shipment ID & Tracking Number
  • Complete Receiver Information
  • Sender (Merchant & Store) Details
  • Financial Amounts & Costs
  • Current Status with Global Names
  • 📊 Shipment History Array
  • 📦 Partial Delivery History
  • Delivery Agent Information
  • Pickup Agent Information
  • Settlement Information
  • Delivery Verification Details
  • Image URL (if available)
  • 📞 Treatment History (Call Center)
Complete History Tracking: Each shipment includes three history arrays:
  • 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

Partial Return Information: For shipments with partial returns:
  • is_partial_return - Boolean flag (true/false)
  • quantity_delivered - Quantity successfully delivered
  • quantity_returned - Quantity returned
  • partial_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 Arabic
  • partial_return_note - Contains branch name where partial return is located (e.g., "الجزء الراجع موجود في فرع: Baghdad Branch")
High Performance: Query API uses advanced batch loading techniques to deliver fast responses even with 100 shipments, ensuring sub-second response times.

Update Shipment Status

POST /v2/shipments/update-status

Update shipment status with various actions (postpone, return, delivered, etc.).

Important Notes:
  • postponed_reason: You can use any text (e.g., "العميل غير موجود" or custom text) - New reasons will be added automatically as inactive
  • return_reason: You can use any text (e.g., "العنوان خاطئ" or custom text) - New reasons will be added automatically as inactive
  • ✅ Required fields by action:
    • POSTPONED → postponed_reason + postponed_date_id (1=tomorrow, 2=in 2 days, 3=in 3 days)
    • RETURN_TO_STORE / RETURNED_WITH_AGENT → return_reason + optional return_quantity for partial returns
    • TREATED → treated_message required (minimum 3 characters)
    • SUCCESSFUL_DELIVERY → No extra fields needed
    • SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE → new_amount_iqd or new_amount_usd required
  • 📌 Note: See recommended reasons from /v2/reference/postponed-reasons and /v2/reference/return-reasons (but you can use any text)
Request Body Example (POSTPONED):
{
  "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": "العميل رفض المنتج"
}
Response Example:
{
  "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"
}
Request Fields:
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:
  • 1 = Tomorrow
  • 2 = In 2 days
  • 3 = In 3 days - Maximum allowed
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
Available Actions:
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
Available Return Reasons (TEXT)

✅ You can use any text for return_reason field.

📝 Recommended reasons (active in system):

  • هاتف الزبون مغلق
  • الزبون رفض الطلب
  • الزبون لم يطلب
  • الزبون لم يرد
  • سيستلم الطلب لاحقا من المحل
  • القياس ليس المطلوب
  • البضاعة ليست هي المطلوبة
  • الزبون مستلم سابقا
  • الزبون غلق الهاتف بعد الأتفاق
  • هاتف الزبون غير داخل بالخدمة
  • تحويل الى غير محافظة
  • الزبون لا يرد بعد الاتفاق
  • الزبون حاظر المندوب
  • رقم الزبون خطأ
  • الطلب ملغي
  • الطلب مكرر
  • راجع بسبب اختلاف السعر
  • الزبون لا يرد بعد سماع المكالمة
  • الزبون لا يرد ليومين
  • الزبون لا يرد بعد التأجيل
  • تحويل الى غير محطة
Note: New reasons will be added as inactive (rtn_active=false) for admin review.
Available Postponed Reasons (TEXT)

✅ You can use any text for postponed_reason field.

📝 Recommended reasons (active in system):

  • تم تغيير العنوان من قبل الزبون
  • تم التأجيل لرغبة الزبون
  • مسافر ولا يوجد من يستلم
  • تحويل الى غير منطقة
  • المتجر يريد تأجيل الطلب
  • مؤجل بسبب فحص الطلبيات
  • مؤجل ليلا لرغبة الزبون
  • مؤجل بالاتفاق مع الزبون
  • تحويل الى مندوب اخر
Note: New reasons will be added as inactive (post_active=false) for admin review.
More Examples:
Return Action Example:
{
  "shipment_number": "SHIP002",
  "action": "RETURN_TO_STORE",
  "return_reason": "العنوان خاطئ",
  "note": "العميل أعطى عنوان خاطئ"
}
Treated Action Example:
{
  "shipment_number": "SHIP003",
  "action": "TREATED",
  "treated_message": "العميل سيتواصل معنا غداً لاستلام الشحنة"
}
Success with Amount Change Example:
{
  "shipment_number": "SHIP004",
  "action": "SUCCESSFUL_DELIVERY_WITH_AMOUNT_CHANGE",
  "new_amount_iqd": 45000,
  "note": "العميل دفع 45000 بدلاً من 50000"
}

Reference Data APIs

What is Reference Data?

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.

Security Policy:
  • 🌍 Public Data: Governorates, Cities, Action Codes - No authentication required (geographic & general reference)
  • 🔐 Protected Data: Return Reasons, Postponed Reasons - Authentication required (business policies)
GET /v2/reference/governorates Public

Get list of all active governorates. No authentication required (public geographic data).

Request Examples:
# 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
Response Example:
{
  "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 /v2/reference/cities Public

Get list of cities/districts with pagination. Filter by governorate code (optional). No authentication required (public geographic data).

Parameters:
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"
Request Examples:
# 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
💡 Pro Tip: Download all cities as Excel, then use VLOOKUP or filtering in your system to validate city names before API calls.
GET /v2/reference/return-reasons Auth Required

Get list of active return reasons (actual text to use in update-status API). Requires authentication.

Response Example:
{
  "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": "کڕیار ڕەتکردەوە"
    }
  ]
}
Important: When updating status with return action, use the text value directly.
Example: "return_reason": "العنوان خاطئ"
GET /v2/reference/postponed-reasons Auth Required

Get list of active postponed reasons (actual text to use in update-status API). Requires authentication.

Response Example:
{
  "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": "کڕیار سەرقاڵە"
    }
  ]
}
Important: When updating status with postponed action, use the text value directly.
Example: "postponed_reason": "العميل غير موجود"
GET /v2/reference/available-actions

Get actions that are currently available for a specific shipment (based on its current status).

Parameters:
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)
Example Request:
GET https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=SHIP001
Headers: Authorization: Bearer {token}
Response Example:
{
  "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"
  }
}
About metadata Field

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 step
  • current_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.)
Best Practice Workflow:
  1. Step 1: Call this API to get available actions for the shipment
  2. Step 2: Show only these actions in your UI (dropdown/buttons)
  3. Step 3: When user selects an action, show required fields based on required_fields
  4. Step 4: Submit to /v2/shipments/update-status
📥 Download Reference Data
Authentication Notice:
  • 🔒 Requires Authentication: Return Reasons, Postponed Reasons
    💡 To download these files, you must login first in the Try It Live section below
  • 🌐 Public Access: Governorates, Cities, Action Codes (no login required)
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
Login in Try It Live to download
Show dropdown in UI for return actions
Postponed Reasons
Auth Required
/v2/reference/postponed-reasons
Login in Try It Live to download
Show dropdown in UI for postpone actions
Action Codes
Public
/v2/reference/action-codes Complete list of all possible actions with descriptions
💻 Implementation Example: Create Dropdown
JavaScript Example - Build Governorate Dropdown:
// 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);
  });
}
HTML Example:
<!-- 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>
💾 Caching Strategy
Update Frequency
  • Governorates: Rarely change - cache for 1 month
  • Cities: Occasionally updated - cache for 1 week
  • Return Reasons: May change - cache for 3 days
  • Postponed Reasons: May change - cache for 3 days
  • Action Codes: Stable - cache for 1 month
Storage Options
  • Database: Import Excel into your DB tables
  • Local Storage: Store JSON in browser localStorage
  • Cache Service: Use Redis/Memcached for backend
  • Static Files: Download Excel, convert to CSV/JSON

Stores Management APIs

About Stores Management

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.

GET /v2/merchants/my-stores Auth Required

Purpose: Retrieve list of all stores belonging to the authenticated master customer.

Request Example:
GET https://jenni.alzaeemexp.com/api/v2/merchants/my-stores
Authorization: Bearer {token}
Response Example:
{
  "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"
    }
  ]
}
POST /v2/stores/create Auth Required

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.

Request Headers:
POST /v2/stores/create
Content-Type: application/json
Authorization: Bearer {token}
Request Body:
{
  "store_name": "متجر الإلكترونيات - فرع المنصور",
  "store_phone": "07901234567",
  "governorate_code": "BGD",
  "address": "شارع المنصور، مجمع 15، الطابق الأرضي",
  "latitude": 33.3152,
  "longitude": 44.3661
}
Request Fields:
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
Response Example:
{
  "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 store
  • generated_password: Auto-generated password (save this!)
Usage Notes:
  • ✅ Store is automatically linked to your master customer account
  • ✅ Inherits pickup agent and sharing configuration from master customer
  • ✅ Password is generated once - store it securely for future use
  • ✅ Use store_id when creating shipments to specify which store the shipment belongs to
  • ⚠️ governorate_code must be a valid code from /v2/reference/governorates

Action Codes Reference

Understanding Action Codes vs. Status
Action Codes = What You DO (Verbs)
  • Purpose: Operations to perform on shipments
  • Usage: Send in POST/PUT requests to UPDATE status
  • Example: SUCCESSFUL_DELIVERY - the action of marking as delivered
  • API: POST /v2/shipments/update-status
Step Status = What It IS (Nouns)
  • Purpose: Current state of the shipment
  • Usage: Receive in GET responses (READ-ONLY)
  • Example: DELIVERED - the current status after delivery
  • API: GET /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).

What are Action Codes?

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 /v2/reference/action-codes

Get complete list of all professional action codes with descriptions in English and Arabic.

Response Example (Partial):
{
  "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": []
    }
  ]
}
📋 Most Common Action Codes - Complete Reference

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
Important Notes:
  • Not all actions are available for all shipments. Actions depend on current shipment status.
  • Use /v2/reference/available-actions?shipment_number=XXX to check which actions are currently allowed.
  • Some actions require additional fields (see "Required Fields" column).
  • Action codes are case-sensitive.
  • Status badges show the resulting state after performing the action.
🔍 Get Available Actions for a Specific Shipment
GET /v2/reference/available-actions

Returns only the actions that are currently allowed for a specific shipment.

Request Parameters:
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)
Example Request:
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=SHIP001" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"
Response Example:
{
  "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 description
  • current_stage: Stage name (SORTING_CENTER, RTO, DELIVERED, WITH_AGENT, etc.)
  • current_stage_ar: Arabic stage name
Best Practice

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

Visual Flow Example
How Actions Change Status - Real Example:
1. Initial Status:
Shipment is IN_SC (In Sorting Center)
2. You Perform Action:
POST /v2/shipments/update-status with action_code: "ASSIGN_TO_AGENT" Action: ASSIGN_TO_AGENT
3. Status Changes To:
Shipment becomes OFD (Out For Delivery)
4. Agent Delivers Successfully:
POST /v2/shipments/update-status with action_code: "SUCCESSFUL_DELIVERY" Action: SUCCESSFUL_DELIVERY
5. Final Status:
Shipment becomes DELIVERED (Successfully Delivered)

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.

Step Status Mapping

NEW: Abbreviated Step Codes!

✅ 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.

IN_SC
داخل مركز الفرز
DELIVERED
سلمت بنجاح
OFD
قيد التوصيل
RTO_WH
راجع كلي في المخزن
PRINT_MANIFEST_DA
طباعة المنفيست لمندوبين التوصيل
POSTPONED
مؤجل

Total: 26 step statuses mapped to abbreviated standard names. View complete list in the complete status table or Abbreviations Guide.

Response Format Details

Understanding API Responses

All V2 API responses follow a consistent, standardized format for easy parsing and error handling.

📋 Standard Response Structure
All responses include these base fields:
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
📦 Create Shipment Response Details
{
  "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
  }
}
How to Handle:
  • Accepted: Save shipment_id to your database for future queries
  • Rejected: Display reason to user, log error_code for debugging
  • Summary: Show overall success rate to user
🔍 Query Shipment Response Details
Complete Shipment Information Object:
{
  "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
  }
}
Important Field Notes:
  • Null values: Fields may be null if not applicable (e.g., delivery_agent_name before assignment)
  • Boolean fields: Always true/false, never "Y"/"N" strings
  • Dates: ISO 8601 format: YYYY-MM-DDTHH:mm:ssZ
  • Numbers: Amounts and costs are decimals (Double), IDs are integers (Long/Integer)
🔄 Update Status Response Details
{
  "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"
}
❌ Error Response Format
When validation fails or error occurs:
{
  "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
}
Parsing Example:
// 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 Status Codes
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
🔢 Data Type Reference
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
Common Parsing Mistakes:
  • ❌ Treating booleans as strings: if (fragile == "true") → ✅ if (fragile === true)
  • ❌ Treating IDs as strings: "shipment_id": "12345" → ✅ "shipment_id": 12345
  • ❌ Ignoring null: agent.name crashes → ✅ agent?.name || "Not assigned"
💻 Response Parsing Examples
JavaScript/TypeScript
// 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);
}
Python
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']}")
📊 Field Presence Guide

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
Safe Null Handling:
// 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')
📏 Response Size & Pagination
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)

Complete Code Examples

Organized by Category

Code examples are organized into 9 main categories for easier navigation across 34 APIs:

  • 🔐 Authentication (2 APIs)
  • 📦 Shipment Management (6 APIs)
  • 💾 Reference Data (7 APIs)
  • 📊 Statistics (3 APIs)
  • 📋 Orders (3 APIs)
  • 🏪 Merchants & Management (3 APIs)
  • 🎫 Support & Tickets (9 APIs)
  • 💵 Payments (2 APIs)
  • 📤 Returns (2 APIs)
Select Your Programming Language:

Choose from 5 popular languages - all examples include authentication, error handling, and comments.

cURL Examples - Organized by Category

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:

📌 Query by Shipment IDs:
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]
  }'
📌 Query by Shipment Numbers:
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"]
  }'
📌 Get All User's Shipments (Paginated):
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
  }'
Page is 0-indexed. Max page_size is 100.

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"

Important: This endpoint must be implemented on YOUR server, not on Jenni Logistics server. Jenni Logistics automatically sends shipments to 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": "أحمد علي"
      }
    ]
  }'
Quick Summary

Total APIs: 36 APIs across 10 categories

  • 🔐 Authentication: 2 APIs
  • 📦 Shipments: 6 APIs
  • 💾 Reference Data: 7 APIs
  • 📊 Statistics: 3 APIs
  • 📋 Orders: 3 APIs
  • 🏪 Merchants: 3 APIs
  • 🎫 Tickets: 9 APIs
  • 💵 Payments: 2 APIs
  • 📤 Returns: 2 APIs
  • 🔄 Push & Status Update: 2 APIs
JavaScript Examples - Organized by Category

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:

📌 Query by Shipment IDs:
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]);
📌 Query by Shipment Numbers:
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']);
📌 Get All User's Shipments (Paginated):
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();
}
APIs Summary

Total: 34 APIs organized in 9 categories for easy navigation

  • 🔐 Authentication: 2 APIs
  • 📦 Shipments: 6 APIs
  • 💾 Reference Data: 7 APIs
  • 📊 Statistics: 3 APIs
  • 📋 Orders: 3 APIs
  • 🏪 Merchants: 3 APIs
  • 🎫 Tickets: 9 APIs
  • 💵 Payments: 2 APIs
  • 📤 Returns: 2 APIs
Python Examples - Organized by Category

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()
APIs Summary

Total: 34 APIs organized in 9 categories for easy navigation

  • 🔐 Authentication: 2 APIs
  • 📦 Shipments: 6 APIs
  • 💾 Reference Data: 7 APIs
  • 📊 Statistics: 3 APIs
  • 📋 Orders: 3 APIs
  • 🏪 Merchants: 3 APIs
  • 🎫 Tickets: 9 APIs
  • 💵 Payments: 2 APIs
  • 📤 Returns: 2 APIs
Python Implementation Notes
  • ✅ Install: pip install requests
  • ✅ All functions use async-ready design
  • ✅ Proper error handling included
  • ✅ Type hints available (use mypy for validation)
Java Examples - Organized by Category

Spring Boot RestTemplate implementation. Expand categories to see individual APIs.

Java Spring Boot Implementation

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();
}
Java Dependencies Required
<!-- 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>
PHP Examples - Organized by Category

Using cURL with proper error handling. Expand categories to see individual APIs.

PHP Implementation

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);
}
PHP Requirements
  • ✅ PHP 7.4+ recommended
  • ✅ cURL extension enabled
  • ✅ JSON extension enabled
  • ✅ All examples are procedural (no OOP required)
Quick Tips for Using Code Examples:
  • ✅ Replace YOUR_TOKEN with your actual JWT token from login
  • ✅ Replace YOUR_COMPANY_CODE with your registered system code
  • ✅ All URLs use dynamic base URL - works on any environment
  • ✅ Copy button available for each example
  • ✅ All examples include error handling
Security Notes:
🌍 Public APIs (No Auth):
  • Governorates
  • Cities
  • Action Codes
🔐 Protected APIs (Auth Required):
  • Return Reasons
  • Postponed Reasons
  • All other APIs

Common Integration Scenarios

Real-World Examples

Learn from practical scenarios that demonstrate how to use the V2 API effectively.

Scenario 1: E-commerce Store Integration
Use Case: Your customer places an order on your website. You need to create a shipment automatically.
Step-by-Step Implementation:
Step 1: Customer Places Order on Your Website
// 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 }
  ]
};
Step 2: Map Your Data to V2 API Format
// 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"
    }
  ]
};
Step 3: Send to API
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);
}
Step 4: Track Shipment Status
// 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;
  }
}
Scenario 2: Daily Batch Processing
Use Case: You have 500 orders collected daily. You need to create them in bulk.
Implementation:
# 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)
Scenario 3: Handle Delivery Exceptions
Use Case: Customer is not available, you need to postpone the delivery.
Complete Workflow:
1
Get Available Actions
GET https://jenni.alzaeemexp.com/api/v2/reference/available-actions?shipment_number=SHIP001
Check if POSTPONED action is available for this shipment
2
Get Postponed Reasons
GET https://jenni.alzaeemexp.com/api/v2/reference/postponed-reasons
Show dropdown to user/agent to select reason
3
User Selects Reason & Date
// User selections
const selectedReason = "العميل غير موجود";
const postponeDate = 1;  // Tomorrow
4
Submit Postpone Action
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);
Scenario 4: Handle Returns (Full & Partial)
Full Return

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
}
Partial Return

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": "تم استرجاع قطعة واحدة"
}
Scenario 5: Delivery with Amount Change
Use Case: Customer negotiated and agreed to pay different amount (e.g., 40,000 instead of 50,000).
{
  "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
}
Scenario 6: Customer Tracking Page
Use Case: Customer wants to track all their active orders on your website.
Frontend Implementation Example:
// 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;
}
Scenario 7: Delivery Agent Mobile App
Use Case: Delivery agent needs to update shipment status from mobile app.
Flutter/React Native Example:
// 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("✅ تم تسليم الشحنة بنجاح");
  }
}
Scenario 8: Automatic Status Updates from Jenni Logistics
Use Case: You sent shipments to Jenni Logistics for delivery. Jenni Logistics automatically notifies YOUR server when status changes.

Important Configuration:

You MUST configure your domain in Jenni Logistics system settings:

  • Your Domain/URL: e.g., https://yourstore.com
  • Authentication Method: TOKEN or LOGIN
  • Integration Type: Receive In (You send shipments to Jenni Logistics)

Jenni Logistics will automatically send status updates to: YOUR_DOMAIN/v2/push/update-status

Step 1: Implement the Endpoint on YOUR Server

Create POST /v2/push/update-status endpoint on YOUR server to receive automatic updates from Jenni Logistics:

Configuration:
  • In Jenni Logistics integration settings, set is_connection_url to your domain (e.g., https://yourstore.com)
  • Jenni Logistics automatically appends /v2/push/update-status to your domain
  • Final webhook URL: YOUR_DOMAIN/v2/push/update-status
  • ⚠️ You MUST implement this exact path on your server
// 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);
}
Step 2: Data Format You Will Receive

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
    }
  ]
}
All Fields Explained
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:

  • ✅ Respond with { "success": true } immediately (Jenni expects confirmation)
  • ✅ Process updates asynchronously if heavy operations are needed
  • ✅ Verify system_code matches yours for security
  • ✅ Store all updates in database for history tracking
  • ✅ Send customer notifications based on action codes
  • ✅ Use agent_latitude and agent_longitude to track delivery locations
  • ✅ V2 systems implement automatic retry (up to 3 immediate + 3 persistent retries if endpoint fails)

Common Action Codes You Will Receive:

  • SUCCESSFUL_DELIVERY → Shipment delivered successfully
  • PARTIAL_DELIVERY → Partial delivery or exchange
  • POSTPONED → Delivery postponed by customer
  • RETURNED_WITH_AGENT → Customer refused delivery
  • ASSIGN_TO_AGENT → Assigned to delivery agent
  • MOVE_TO_AGENT → Moved to agent's manifest (out for delivery)
  • RETURN_TO_STORE → Returned to warehouse/sorting center

See full list in Action Codes Reference section

Integration Best Practices Summary
Do:
  • ✅ Validate data locally before API calls (phone format, amounts, etc.)
  • ✅ Cache reference data (governorates, cities, reasons)
  • ✅ Use batch operations (up to 100 shipments)
  • ✅ Check available-actions before showing UI options
  • ✅ Implement token refresh before expiration
  • ✅ Handle rejected shipments gracefully
  • ✅ Log all API requests/responses for debugging
  • ✅ Use any text for reasons (system will auto-create if needed)
Don't:
  • ❌ Don't exceed API limits (100 create, 100 query, 100 per page)
  • ❌ Don't use reason codes instead of text
  • ❌ Don't hardcode governorate/city lists
  • ❌ Don't skip validation (causes rejections)
  • ❌ Don't ignore error codes (they tell you what's wrong)
  • ❌ Don't perform actions without checking availability
  • ❌ Don't store tokens in frontend localStorage (security risk)
  • ❌ Don't retry failed requests without fixing the issue

V2 Error Codes Reference

Understanding V2 Error Codes

All V2 API errors return structured error codes for programmatic handling. Use these codes in your application logic.

📦 Shipment Creation Errors
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
🔄 Status Update Errors
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
⚙️ General API Errors
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
Best Practices for Error Handling
  • ✅ Always check success field in response (true/false)
  • ✅ Use error_code for programmatic error handling
  • ✅ Display message field to users (localized Arabic/English)
  • ✅ Log timestamp for debugging
  • ✅ For batch operations, check failed_shipments array for individual errors
  • ✅ Implement retry logic for RATE_LIMIT_EXCEEDED and INTERNAL_ERROR
  • ✅ See Troubleshooting Section for detailed solutions

Frequently Asked Questions (FAQ)

Common Questions

Quick and clear answers to the most frequently asked questions about V2 Integration

Integration Type Questions

Push Out Mode

Concept: Jenni Logistics sends shipments to you

  • Jenni Logistics creates shipments
  • Jenni Logistics pushes them to your system
  • You deliver them
  • You send status updates to Jenni Logistics

Your Role: Delivery Service Provider

Receive In Mode

Concept: You send shipments to Jenni Logistics → Jenni Logistics delivers → Jenni Logistics sends you automatic status updates

  • You create shipments in YOUR system
  • You send them to Jenni Logistics via POST /v2/shipments/create
  • Jenni Logistics delivers them to customers
  • Jenni Logistics automatically sends real-time status updates to YOUR_DOMAIN/v2/push/update-status
  • Updates include: action, step, stage, agent GPS location, note, and more
  • You can also query manually anytime using POST /v2/shipments/query

Your Role: Merchant/Store/E-commerce/App

Ask yourself this simple question:
Who will deliver the shipments?
You will deliver

→ Use Push Out

Jenni Logistics sends you shipments, and you deliver them with your vehicles and drivers

Jenni will deliver

→ Use Receive In

You send your shipments to Jenni Logistics, and Jenni Logistics delivers them with their vehicles and drivers

Push Out Mode
✅ Yes, server required

Why?

  • Jenni will call your endpoint
  • You need to implement /v2/shipments/create
  • Must be accessible from the internet

Recommended Technologies:
Node.js, Python Flask/Django, PHP Laravel, Java Spring Boot

Receive In Mode
❌ No, server NOT required

Why?

  • You only call Jenni Logistics APIs
  • No need to implement endpoints
  • Just REST API calls

Recommended Technologies:
Any language that supports HTTP requests (cURL, Postman, JavaScript, Python, PHP, Java)

Push Out Mode
You Implement (on YOUR server):
  • IMPLEMENT POST /v2/shipments/create Receive shipments from Jenni Logistics
You Call (on Jenni Logistics server):
  • CALL POST /v2/push/update-status Send status updates to Jenni Logistics
Authentication:

Jenni will use its Token/Login credentials to call your server

Receive In Mode

You send shipments → Jenni Logistics delivers → Jenni Logistics sends you automatic status updates

You Must Implement:
  • REQUIRED POST /v2/push/update-status Receive automatic status updates from Jenni Logistics (includes agent GPS)
You Call (on Jenni Logistics server):
  • AUTH POST /v2/auth/login Get JWT token
  • MAIN POST /v2/shipments/create Create shipments in Jenni
  • OPTIONAL POST /v2/shipments/query Manual status check (automatic updates via webhook)
  • OPTIONAL POST /v2/shipments/update-status Update if needed
  • OPTIONAL PUT /v2/shipments/edit Edit shipment details
Authentication:

You use your username/password to get Token

Yes! You can use both modes together
Real-world Example:

Large Logistics Company:

  • Push Out Receives shipments from Jenni Logistics customers and delivers them
  • Receive In At the same time, has an e-commerce store that sends shipments to Jenni Logistics
Configuration:

Each mode requires separate system configuration. Contact your administrator or see Integration Modes for details.

Push Out Mode - API Flow:
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
Receive In Mode - API Flow:
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):

  • ✅ Jenni Logistics automatically sends status updates to YOUR_DOMAIN/v2/push/update-status
  • ✅ You must implement this endpoint on YOUR server to receive real-time updates
  • ✅ Updates include: action_code, current_step, current_stage, agent GPS location, and more
  • ✅ Sent in V2 format with mapped action codes, stages, and steps (professional names, not internal codes)
  • ✅ You can also use POST /v2/shipments/query for manual checks anytime

See 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.

Push Out Mode Examples:
Local Delivery Company

A logistics company in Basra receives orders from Jenni Logistics customers in Baghdad and delivers them

External Branch

A branch in Sulaymaniyah receives shipments from the main branch in Baghdad

Logistics Partner

An external delivery company specialized in international shipping

Receive In Mode Examples:
E-commerce Store

Online store sends its orders to Jenni Logistics for delivery

Mobile App

Marketplace app uses Jenni Logistics to deliver products

POS System

Point of sale system for retail stores sends sales to Jenni Logistics

Inventory Management

Warehouse management system sends shipments automatically

Technical Questions

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
}
Best Practices:
  • Always check success field first
  • Log error_code for debugging
  • Display message to users
  • Implement retry logic for network errors
  • See Troubleshooting Section for common errors

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
How It Works:
  • ✅ Each user/IP has a separate token bucket
  • ✅ Each request consumes 1 token
  • ✅ Tokens refill gradually (for write operations: 1 token every 2 seconds)
  • ✅ When tokens are exhausted, you receive HTTP 429 (Too Many Requests)
When Rate Limit Exceeded:

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

How Long to Wait:

Important: The wait time is dynamic, not fixed. It depends on when the last token was consumed:

  • Write Operations: Wait time ranges from 0.01 to 2 seconds
    • Maximum wait: 2 seconds (if you just consumed the last token)
    • Minimum wait: Less than 1 second (if a token is about to refill soon)
  • Authenticated Endpoints: Wait time ranges from 0.01 to 1 second (1 token per second)
  • Public Endpoints: Wait time ranges from 0.01 to 0.6 seconds (1 token per 0.6 seconds)

💡 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.

Example for Write Operations (e.g., /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
Best Practices:
  • ✅ Implement exponential backoff when receiving 429 responses
  • ✅ Monitor X-Rate-Limit-Remaining header to track available tokens
  • ✅ Use batch operations when possible (e.g., create up to 100 shipments per request)
  • ✅ For high-volume scenarios, distribute requests across multiple user accounts

3 Testing Methods:
1
Try It Live (on this page)

Use the Try It Live section to test APIs directly from your browser

  • ✅ No code needed
  • ✅ Direct from page
  • ✅ Instant results
2
Postman Collection

Download the complete Postman Collection

  • ✅ All APIs ready
  • ✅ Auto-save token
  • ✅ Examples for each request
3
Code Examples

Copy code examples from Integration Examples

  • ✅ Complete end-to-end examples
  • ✅ 4 programming languages
  • ✅ Ready to run

Testing Strategy: Start with one shipment, verify success, then increase gradually. See Testing Guide for 40+ test cases.

Yes! All codes are standardized and mapped
Mapper System:

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:

  • ✅ No internal codes exposed to consumers
  • ✅ All values mapped (EN + AR)
  • ✅ Consistent across all APIs
  • ✅ Secure and doesn't reveal internal system details

See Action Codes Reference for complete list.

Security Features:
JWT Authentication

Token-based security with 24-hour expiration + 30-day refresh token

Ownership Verification

System verifies shipment ownership for every operation (MASTERCUSTOMER, DLVAGENT, AGGREGATOR, PICKUPAGENT)

HTTPS Only

All communications encrypted via HTTPS (TLS 1.2+)

No Internal Codes Exposed

No internal codes or sensitive information exposed to consumers

Input Validation

All inputs validated before processing

Rate Limiting

Protection from excessive requests (max 100 items per request)

Security Best Practices:

  • 🔒 Never expose your token in client-side code
  • 🔒 Use environment variables for credentials
  • 🔒 Implement token refresh before expiration
  • 🔒 Log all API calls for audit
  • 🔒 Use HTTPS for all communications

Pagination available in:

  • POST /v2/shipments/query (Mode 3: All user shipments)
  • GET /v2/orders/in-process
  • GET /v2/orders/by-step
  • GET /v2/reference/cities
  • GET /v2/payments/settled
  • GET /v2/returns/settled
Response Format:
{
  "shipments": [...],
  "pagination": {
    "current_page": 0,        // 0-indexed
    "page_size": 100,
    "total_records": 2548,
    "total_pages": 26,
    "has_next_page": true,
    "has_previous_page": false
  }
}
Example Code:
// 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 Bidirectional Integration & Advanced Features

Yes! Full Bidirectional Support for All V2 Systems

V2 Integration is designed to support bidirectional integration between ANY two V2-compatible systems (Jenni-to-Jenni, Jenni-to-External, External-to-Jenni):

How it works:
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
Key Fields for Bidirectional V2 Integration:
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
Priority Order for Shipment Lookup (All V2 Systems):
  1. shipment_id - Direct DB lookup (fastest)
  2. external_id via receivedFromCaseId - For bidirectional V2 status updates
  3. external_id via sentToCaseId - For shipments we sent to V2 systems
  4. shipment_number + context - Fallback with security check

Immediate Retry

Purpose: Handle transient network errors

  • Trigger: Network timeout, 5xx, 429
  • Attempts: 3 retries
  • Timing: 1s → 2s → 4s
  • Total Time: ~7 seconds
  • Location: In-memory (not saved)
  • User Impact: Transparent
Persistent Retry

Purpose: Ensure delivery even if system is down

  • Trigger: All immediate retries failed
  • Attempts: 3 retries
  • Timing: 15min → 30min → 60min
  • Total Time: ~2 hours
  • Location: Database table
  • Processing: Background job (every 5 min)
⚠️ Important: Persistent retry is ONLY for status updates. Shipment creation uses immediate retry only.

Timeline:
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
What happens next:
  • ✅ Update remains in integration_pending_update table with status FAILED
  • ✅ System logs error in m_syslog table
  • ✅ Update is kept for 24 hours before being marked as EXPIRED
  • ✅ Administrator can manually review and retry if needed
  • ✅ You can query pending updates via integration monitoring APIs
Monitoring: Contact your administrator to monitor persistent retry queue for prolonged failures.

Why duplicates happen:
  • Your server responds after the 30-second timeout → V2 system retries
  • Network issues cause request to be sent multiple times
  • Immediate retry mechanism (3 attempts in 7 seconds)
Solution: Implement Idempotency
// 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);
}
Best Practice: Use Redis/Cache to track processed shipments with 7-day TTL.

Common Issues

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

Correct Codes:
  • BGD - Baghdad
  • BAS - Basra
  • NIN - Nineveh
  • ARB - Erbil
  • NJF - Najaf
  • KRB - Karbala
  • ANB - Anbar
  • BBL - Babylon
  • DHQ - Duhok
  • DYL - Diyala
  • KRK - Kirkuk
  • ... View All
# Get complete list
curl -X GET "https://jenni.alzaeemexp.com/api/v2/reference/governorates"

No problem! The system handles it automatically

How it's handled:
  1. System searches for the city in the database
  2. If not found, uses "unknown district" for that governorate
  3. City name is automatically added to address
  4. Shipment is created successfully
Example:
// 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!

3 Tracking Methods:
1
POST /v2/shipments/query (Best Performance)

Query up to 100 shipments by ID/Number in single request

{
  "shipment_ids": [12345, 12346, 12347]
}
2
POST /v2/shipments/query (Paginated)

Get all your shipments with pagination (100 per page)

{
  "page": 0,
  "page_size": 100
}
3
Webhooks (Optional - Coming Soon)

Receive automatic notifications when status changes

Performance Tip: Query API optimized with batch loading - 100 shipments in ~50ms with only 5 DB queries!

Troubleshooting & FAQ

Common Issues and Solutions

Quick solutions to the most frequently encountered problems.

🔐 Authentication Issues

Possible Causes:
  • Incorrect credentials
  • Account not activated
  • Wrong API endpoint
Solution:
  1. Verify your credentials are correct
  2. Ensure you're using the correct base URL
  3. Contact support if the issue persists

Cause: JWT token has expired (24 hours validity)

Solution:
  1. Use the refresh token to get a new access token:
    POST /v2/auth/refresh with Authorization: Bearer {refreshToken}
  2. Or login again to get fresh tokens

Cause: You forgot to include the Authorization header

Solution:
# Add this header to all authenticated requests:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Note: Do NOT add "Bearer" prefix twice! The token from login response already includes it.
📦 Shipment Creation Issues

Cause: The shipment_number already exists in the system

Solution:
  1. Use a unique shipment number for each shipment
  2. Query existing shipment: POST /v2/shipments/query
  3. If it's your shipment, you can update it instead of creating a new one

Cause: Invalid governorate_code provided (not found in system mapping)

Solution:
  1. Get valid governorate codes:
    GET /v2/reference/governorates
  2. Use exact governorate code from the reference list (e.g., BGD, BAS, NJF)
  3. Download Excel reference: ?download=excel

Note about Cities: If a city name is not found in the system, the shipment will still be accepted. The system will automatically use an "unknown district" for that governorate and add the city name to the address details.

Cause: Phone number doesn't match required format

Required Format:
  • Must be between 10-15 digits
  • Valid Examples: 07901234567, 07801234567, 009647901234567
Invalid Examples:
  • 079012345 (too short - less than 10 digits)
  • 12345 (too short)
  • ❌ Phone with letters or special characters

Cause: You are an AGGREGATOR user but didn't provide merchant_id

Solution:
  1. If you are an AGGREGATOR: Always include merchant_id in your request
  2. If you are a MERCHANT: merchant_id is automatically set (leave it null)
  3. If you don't know your role, contact support
🔄 Status Update Issues

Cause: The action is not available for the current shipment status

Solution:
  1. Check available actions for the shipment:
    GET /v2/reference/available-actions?shipment_number=XXX
  2. Only use actions returned by this API
  3. Example: You can't deliver a shipment that's already delivered
💡 Pro Tip: Always call available-actions first to know which actions are currently possible!

Good News: You can use any text for postponed and return reasons!

How it works:
  1. ✅ Send any Arabic text for postponed_reason or return_reason
  2. ✅ If the reason exists in our system → We'll use it
  3. ✅ If the reason is NEW → We'll automatically add it as inactive for admin review
  4. ✅ Your request will be accepted either way
✅ Examples:
Existing reason: "العميل غير موجود" → Works immediately
New reason: "سبب جديد مخصص" → Added as inactive, request accepted
Custom reason: "العميل سافر للخارج" → Added as inactive, request accepted
📝 Note: See recommended reasons at:
GET /v2/reference/postponed-reasons
GET /v2/reference/return-reasons
But you're not limited to these - use any text you need!

Cause: Missing or invalid postponed_date_id for POSTPONED action

Solution:
{
  "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
⚙️ General Issues

Possible Causes:
  • Malformed JSON request
  • Unexpected server error
  • Database connection issue
Solution:
  1. Validate your JSON syntax (use a JSON validator)
  2. Check that all required fields are provided
  3. Review the error response for details
  4. If issue persists, contact technical support with the error details

Cause: You exceeded the maximum limit per request

Limits:
  • Create Shipments: Maximum 100 per request
  • Query Shipments (Specific Lookup): Maximum 100 IDs/Numbers per request
  • Query Shipments (Paginated): Maximum 100 per page
Solution:
  1. For specific lookup: Split into multiple requests (100 each)
  2. For all shipments: Use paginated mode with page & page_size
  3. Example: 250 shipments = 3 requests (100 + 100 + 50) OR use pagination
❓ Frequently Asked Questions
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
Still Need Help?

Our technical support team is ready to assist you!

Try It Live

Interactive API Tester
Test the V2 Integration API directly from your browser. All APIs are available below.
API Base URL: https://jenni.alzaeemexp.com/api
34 APIs Available: All endpoints ready for interactive testing below (Query supports 3 flexible modes)

Get JWT access token. Valid for 24 hours.

Refresh your JWT token before it expires (24h validity)

Get a new access token using your refresh token

Login first to enable

Create new shipment. Max 100 shipments per request (bulk).
Use your registered company/system name
Sender's shipment ID for tracking
Required for AGGREGATOR role
Optional for MERCHANT role
Optional: Sender contact information
Use corrected governorate codes
Optional: Detailed address
Optional: Receiver GPS latitude
Optional: Receiver GPS longitude
Used for reverse logistics
Login first to enable

Three Query Modes: Query by IDs, by Numbers, or get all your shipments with pagination (max 100 per page)
Complete History Included: Response contains:
  • shipment_history - All status changes
  • partial_history - Same structure as shipment_history (for partial deliveries only)
  • treatment_history - Call center communications
Separate multiple tracking numbers with commas
Login first to enable

Update shipment status with different actions (Delivered, Postponed, Returned, etc.)
Use shipment_id from create response
Login first to enable

Edit existing shipment (partial update). Only provide fields you want to change.
Use shipment_id from create response
Fields to Update (all optional):
Login first to enable

Generate PDF stickers for printing. Max 100 shipments per request.
Max 100 shipments
Login first to enable

Get list of all governorates (no authentication required)

Get list of all cities (no authentication required)

Get list of active return reasons

Login first to enable

Get list of active postponed reasons

Login first to enable

Get actions available for this specific shipment
Login first to enable

Get 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 enable

Get paginated list of orders in process
Login first to enable

Get orders filtered by step (OFD, IN_SC, DELIVERED, etc.)
Examples: OFD, IN_SC, DELIVERED, RTO_WH
Login first to enable

Warning: 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. Cannot be undone. Ownership is verified.
Login first to enable

Get list of your merchant stores

Login first to enable

Create a new merchant account
Login first to enable

Update existing merchant information
Login first to enable

Create a new support ticket
Use Get Departments API first
Login first to enable

Get all your tickets

Login first to enable

Additional Ticket APIs:
  • POST /v2/tickets/search - Search tickets
  • GET /v2/tickets/statuses - Get ticket statuses
  • GET /v2/tickets/departments - Get departments
  • GET /v2/tickets/causes - Get causes
  • GET /v2/tickets/history/{id} - Get ticket history
  • GET /v2/tickets/initial-status - Get initial status
  • POST /v2/tickets/upload-attachment - Upload file

Refer to the "Complete Code Examples" section for full implementation details

Get payment manifest details by payment ID
Login first to enable

Get list of settled payments
Login first to enable

Get return manifest details by return ID
Login first to enable

Get list of settled returns
Login first to enable
Utility Tools: All main APIs are now organized in their respective tabs above.
Quick Access

All 34 APIs are available in the tabs above, organized by category:

  • Auth - Login & Refresh Token (2 APIs)
  • Shipments - Create, Query (3 modes), Update, Edit, Stickers (5 APIs)
  • Reference - Governorates, Cities, Reasons, Actions, Countries (7 APIs)
  • Orders - Statistics, In Process, By Step, Delete (4 APIs)
  • Merchants - My Stores, Create, Update (3 APIs)
  • Tickets - Create, Query, Search, and more (9 APIs)
  • Financial - Payments & Returns Info (4 APIs)

For complete code examples in all languages (cURL, JavaScript, Python, Java, PHP), check the "Complete Code Examples" section.

Postman Collection

Quick Testing with Postman

Import our Postman collection to test all APIs instantly! All endpoints, examples, and environments pre-configured.

Pre-configured Requests

All API endpoints ready to use with sample data

Auto Token Management

Automatic token extraction and injection in headers

Environment Variables

Switch between development, staging, and production

Code Generation

Generate code in any language from Postman

📥 How to Import Collection
Method 1: Import from URL
  1. Open Postman
  2. Click "Import"
  3. Select "Link" tab
  4. Paste this URL:
    https://jenni.alzaeemexp.com/api/v2/docs/postman-collection
  5. Click "Continue" → "Import"
Method 2: Download & Import
  1. Download the collection:
  2. Open Postman
  3. Click "Import"
  4. Drag & drop the downloaded file
  5. Click "Import"
🌍 Environment Setup

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
Auto Token Setup:

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!

📂 Collection Structure
Authentication Folder:
  • ✅ Login
  • ✅ Refresh Token
Shipments Folder:
  • ✅ Create Shipments (Single)
  • ✅ Create Shipments (Bulk)
  • ✅ Query Shipments
  • ✅ Update Status
  • ✅ Edit Shipment
  • ✅ Generate Stickers
Reference Data Folder:
  • ✅ Get Governorates
  • ✅ Get Cities
  • ✅ Get Return Reasons
  • ✅ Get Postponed Reasons
  • ✅ Get Action Codes
  • ✅ Get Available Actions
Statistics Folder:
  • ✅ Orders by Status
  • ✅ Orders in Process
  • ✅ Payments Settled
⚡ Postman Quick Start
1
Import Collection

Import the collection using one of the methods above

2
Create Environment

Click "Environments" → "Create Environment" → Add baseUrl variable → Set value to https://jenni.alzaeemexp.com/api

3
Update Login Credentials

Open "Login" request → Update username and password in request body

4
Send Login Request

Click "Send" → Token will be automatically saved to environment

5
Test Other APIs

All requests will now use the saved token automatically. Start testing!

Video Tutorial

Watch our video tutorial on how to set up and use the Postman collection: Coming Soon

Testing Guide

Complete Testing Checklist

Follow this guide to thoroughly test your integration before going to production.

📝 Phase 1: Basic Functionality Testing
✅ Authentication Tests
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
✅ Shipment Creation Tests
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
✅ Query Tests
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
✅ Status Update Tests
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
🔍 Phase 2: Edge Cases & Negative Testing
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)
⚡ Phase 3: Performance & Load Testing
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
🔗 Phase 4: End-to-End Integration Testing
Complete Workflow Test:
  1. Create shipment via your application
  2. Verify shipment appears in Jenni system
  3. Track shipment through delivery lifecycle
  4. Update status (postpone, return, etc.)
  5. Query final status and verify data
  6. Download settlement reports
💡 Recommended Test Scenarios:
  • ✅ Happy Path: Create → Deliver → Query settlement
  • ⚠️ Postponed Path: Create → Postpone → Deliver later
  • 🔄 Return Path: Create → Return → Confirm return
  • 💰 Amount Change: Create → Deliver with changed amount
  • 📦 Partial Delivery: Create (qty=3) → Partial delivery (qty=2)
🎯 Sample Test Data

Use this test data for comprehensive testing:

Valid Test Shipments:
{
  "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": "عينة مجانية"
    }
  ]
}
Invalid Test Cases (for negative testing):
// 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
  }
]
🤖 Automated Testing
Postman Collection Tests:

Our Postman collection includes automated tests that verify:

  • ✅ Response status codes (200, 401, 400, etc.)
  • ✅ Response structure (required fields present)
  • ✅ Data types (numbers, strings, booleans)
  • ✅ Token extraction and auto-injection
Run Tests in Postman:
  1. Import our Postman collection
  2. Set up environment variables
  3. Click "Runner" button
  4. Select the collection
  5. Click "Run" - all tests will execute automatically
🚀 Go-Live Checklist
Technical Readiness
All test cases passed
Error handling implemented
Token refresh logic working
Reference data cached locally
Logging configured
Retry mechanism in place
Validation rules applied
Business Readiness
Team trained on V2 API
Support contact established
Production credentials received
Monitoring/alerts configured
Rollback plan prepared
Documentation reviewed
Go-live date scheduled
🛠️ Recommended Testing Tools
Postman

Best for: Manual testing, exploring APIs

Get Collection
cURL/HTTPie

Best for: Quick command-line testing

View Examples

Complete Step Status Reference

Understanding Step Status vs. Action Codes
Step Status = What It IS (Nouns)
  • Purpose: Represents the current state of the shipment
  • Usage: You RECEIVE these in GET responses
  • Example: DELIVERED - shipment is currently delivered
  • Type: READ-ONLY - You cannot directly set status
Action Codes = What You DO (Verbs)
  • Purpose: Operations to change the shipment status
  • Usage: You SEND these in POST/PUT requests
  • Example: SUCCESSFUL_DELIVERY - perform delivery
  • Type: WRITABLE - You choose which action to perform

🔄 Workflow:

  1. Query Status: GET /v2/shipments/query → Receive current_step = "OFD" (Out For Delivery)
  2. Get Available Actions: GET /v2/reference/available-actions → See what you can DO
  3. Perform Action: POST /v2/shipments/update-status with action_code: "SUCCESSFUL_DELIVERY"
  4. Query Again: GET /v2/shipments/query → Receive current_step = "DELIVERED" (new status!)
What are Step Statuses?

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
How to Use Status Information:
  • Query API: When you call GET /v2/shipments/query, you receive current_step field with these status codes
  • Track Progress: Use these statuses to show shipment location/progress in your UI
  • Cannot Set Directly: You cannot POST a status. Instead, you POST an action code (see Action Codes) which results in a status change
  • Status Flow: Statuses follow a workflow: IN_SCOFDDELIVERED or RTO_WH
Quick Reference:
🟢 Delivery Statuses:
  • DELIVERED
  • PARTIALLY_DELIVERED
  • DELIVERED_PRICE_CHANGED
🔴 Return Statuses:
  • RTO_WH (In Warehouse)
  • RTO_WITH_DA (With Agent)
  • RTO_ARCHIVED
🟡 In-Process Statuses:
  • IN_SC (Sorting Center)
  • OFD (Out For Delivery)
  • POSTPONED

Governorate Codes Reference

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)