Table of Contents

Mobile Rental API – End-to-End Postman Testing Guide

Complete testing guide for mobile rental workflow (create draft → upload documents → finalize booking), user authentication / profile management endpoints, and payment integration via EsnekPOS.

Language Support (Accept-Language): All API endpoints read the standard Accept-Language HTTP header. When provided (e.g. Accept-Language: ar or Accept-Language: tr), all translatable response fields — product names, country names, location names, service descriptions, etc. — are returned in the requested language. The language must be installed in Odoo; otherwise the API falls back to the website's default language. Add Accept-Language: {{lang}} to any request below to receive localized data.

Part 1 – Booking & Car Management

1. Collection & Environment Setup

1.1 Environment Variables

VariableDescriptionExample Value
base_urlOdoo instance URLhttp://localhost:8069
api_keyAPI key from Odoo Settings → Rental → APIyour_api_key
api_secretAPI secret for HMAC signaturesyour_api_secret
langAccept-Language header value for localized responsesen
draft_order_idSet automatically by draft endpoint test(auto)
license_photo_idAttachment ID from upload(auto)
license_photo_2_idAttachment ID from upload(auto)
passport_photo_idAttachment ID from upload(auto)
id_photo_1_idAttachment ID from upload (TC front)(auto)
id_photo_2_idAttachment ID from upload (TC back)(auto)
turkey_entrance_stamp_idAttachment ID from upload(auto)
findex_report_idAttachment ID from upload(auto)
turkish_residency_permit_idAttachment ID from upload(auto)
turkish_residency_permit_2_idAttachment ID from upload(auto)
file_base64Base64-encoded file for upload(manual or script)
selected_car_idCar ID from /api/cars(auto or manual)
pickup_location_idPickup location ID(manual)
return_location_idReturn location ID(manual)
insurance_idInsurance type ID(manual)
nationality_idCountry ID for driver nationality(manual)
session_tokenSession token from login/register(auto)
user_emailTest user email[email protected]
user_passwordTest user passwordSecureP@ss1

1.2 Pre-request Script (Collection Level)

if (pm.request.method === 'POST' && pm.request.headers.get('Content-Type')?.includes('application/json')) {
    const body = pm.request.body ? pm.request.body.raw : '{}';
    const secret = pm.environment.get('api_secret');
    if (body && secret) {
        const data = JSON.parse(body);
        const sortedJson = JSON.stringify(data, Object.keys(data).sort());
        const signature = CryptoJS.HmacSHA256(sortedJson, secret).toString();
        pm.environment.set('request_signature', signature);
    }
}

2. Helper Endpoints (Prerequisites)

2.1 GET /api/cars

Method: GET   URL: {{base_url}}/api/cars?from_date=2026-07-01&to_date=2026-07-05&pickup_location={{pickup_location_id}}

Headers:

X-API-Key: {{api_key}}
Content-Type: application/json
Accept-Language: {{lang}}

Tests:

pm.test("Status 200", () => pm.response.to.have.status(200));
pm.test("Has cars list", () => {
    const json = pm.response.json();
    pm.expect(json).to.have.property('products').that.is.an('array');
    if (json.products.length > 0) {
        pm.environment.set('selected_car_id', json.products[0].id);
    }
});

2a. Rental Configuration Data

GET /api/data

Returns all rental configuration data needed by the mobile app: pickup/return locations, insurance products, additional services, ID types, and countries. Optionally filter locations by stock location when a pickup_location_id is provided.

Method: GET   URL: {{base_url}}/api/data

Query Parameters:

ParameterTypeRequiredDescription
pickup_location_idintegerNoWhen provided, filters pickup_locations and return_locations to only include locations sharing the same stock location as the specified pickup location. Mirrors the website's location filtering behavior.

Headers:

X-API-Key: {{api_key}}
Accept-Language: {{lang}}
Auth: Requires API key only. No HMAC signature and no session token required.

Example Requests:

# Get all configuration data (unfiltered)
GET /api/data

# Get configuration data with locations filtered by stock location
GET /api/data?pickup_location_id=5

Response Fields:

FieldTypeDescription
id_typesarray[object]Available ID document types: {id, name}
countriesarray[object]All countries: {id, name, code}
pickup_locationsarray[object]Available pickup locations: {id, name, price, country}. Filtered by stock location when pickup_location_id is provided.
return_locationsarray[object]Available return locations: {id, name, price, country}. Filtered by stock location when pickup_location_id is provided.
insurance_servicesarray[object]Insurance products: {id, name, price_percent, description, country}
additional_servicesarray[object]Add-on services: {id, price_percent, price, name, is_one_time_fee, is_driver_service, is_young_driver_service, is_open_km_service, allow_multiple_additions, description, country}

Example Response:

{
    "id_types": [
        {"id": 1, "name": "Passport"},
        {"id": 2, "name": "Driver's License"}
    ],
    "countries": [
        {"id": 1, "name": "Turkey", "code": "TR"},
        {"id": 2, "name": "Syria", "code": "SY"}
    ],
    "pickup_locations": [
        {"id": 5, "name": "Airport Office", "price": 0.0, "country": "TR"},
        {"id": 6, "name": "Downtown Branch", "price": 25.0, "country": "TR"}
    ],
    "return_locations": [
        {"id": 5, "name": "Airport Office", "price": 0.0, "country": "TR"},
        {"id": 6, "name": "Downtown Branch", "price": 25.0, "country": "TR"}
    ],
    "insurance_services": [
        {"id": 1, "name": "Basic Coverage", "price_percent": 10.0, "description": "...", "country": "TR"}
    ],
    "additional_services": [
        {"id": 1, "price_percent": 0.0, "price": 15.0, "name": "GPS Navigator", "is_one_time_fee": true, "is_driver_service": false, "is_young_driver_service": false, "is_open_km_service": false, "allow_multiple_additions": false, "description": "...", "country": "TR"}
    ]
}
Location Filtering: When pickup_location_id is provided, the endpoint resolves the stock location associated with that pickup location and filters both pickup_locations and return_locations to only return locations linked to the same stock location. This ensures consistent location availability across web and mobile interfaces. If the parameter is omitted or invalid, all locations are returned unfiltered.

Test:

pm.test("Rental configuration data returned", () => {
    const json = pm.response.json();
    pm.expect(json).to.have.property('pickup_locations').that.is.an('array');
    pm.expect(json).to.have.property('return_locations').that.is.an('array');
    pm.expect(json).to.have.property('insurance_services').that.is.an('array');
    pm.expect(json).to.have.property('additional_services').that.is.an('array');
    pm.expect(json).to.have.property('id_types').that.is.an('array');
    pm.expect(json).to.have.property('countries').that.is.an('array');
    if (json.pickup_locations.length > 0) {
        pm.environment.set('pickup_location_id', json.pickup_locations[0].id);
    }
});

2b. Checkout Configuration

GET /api/checkout/config

Returns per-website checkout configuration settings — specifically the nationality lists that drive conditional validation rules during the booking flow. Mobile apps should call this endpoint before initiating checkout to determine which documents and fields are required for each nationality.

Method: GET   URL: {{base_url}}/api/checkout/config

Headers:

X-API-Key: {{api_key}}
Accept-Language: {{lang}}
Auth: Requires API key only. No HMAC signature and no session token required.

Response Fields:

FieldTypeDescription
successbooltrue on success
findex_nationality_idsarray[int]Country IDs that require a Findex report with "Excellent" rating
residency_nationality_idsarray[int]Country IDs that require a Turkish Residency Permit and TC Number
no_residency_nationality_idsarray[int]Country IDs exempt from residency & entrance info requirements
findex_nationalitiesarray[object]Full country details for Findex nationalities: {id, name, code}
residency_nationalitiesarray[object]Full country details for residency nationalities: {id, name, code}
no_residency_nationalitiesarray[object]Full country details for exempt nationalities: {id, name, code}

Example Response:

{
    "success": true,
    "findex_nationality_ids": [1, 2],
    "residency_nationality_ids": [3],
    "no_residency_nationality_ids": [4, 5],
    "findex_nationalities": [
        {"id": 1, "name": "Country A", "code": "CA"},
        {"id": 2, "name": "Country B", "code": "CB"}
    ],
    "residency_nationalities": [
        {"id": 3, "name": "Country C", "code": "CC"}
    ],
    "no_residency_nationalities": [
        {"id": 4, "name": "Country D", "code": "CD"},
        {"id": 5, "name": "Country E", "code": "CE"}
    ]
}
Usage: Use findex_nationality_ids / residency_nationality_ids / no_residency_nationality_ids (integer arrays) for quick lookups by ID. Use the full findex_nationalities / residency_nationalities / no_residency_nationalities arrays when you need to display country names or codes to the user (e.g. in a dropdown).

Test:

pm.test("Checkout config returned", () => {
    const json = pm.response.json();
    pm.expect(json.success).to.be.true;
    pm.expect(json).to.have.property('findex_nationality_ids').that.is.an('array');
    pm.expect(json).to.have.property('residency_nationality_ids').that.is.an('array');
    pm.expect(json).to.have.property('no_residency_nationality_ids').that.is.an('array');
});

3. Step 1 – Create Draft Order

3.1 POST /api/create-book-car-draft

Method: POST   URL: {{base_url}}/api/create-book-car-draft

Headers:

X-API-Key: {{api_key}}
X-API-Signature: {{request_signature}}
Content-Type: application/json
Auth: Requires API key and HMAC signature. Optional user authentication via X-Session-Token.

Body (raw JSON):

{
    "pickupDate": "2026-07-01T10:00:00.000000",
    "returnDate": "2026-07-05T10:00:00.000000",
    "selectedCarId": 1,
    "selectedPickupLocationId": 1,
    "selectedReturnLocationId": 1,
    "selectedInsuranceId": 1,
    "selectedServices": [],
    "driverInfo": {
        "fullName": "Ahmet Yılmaz",
        "mobile": "+905551234567",
        "email": "[email protected]",
        "birthDate": "1990-05-15T00:00:00.000000",
        "nationalityId": 224,
        "licenseIssuingDate": "2018-01-10T00:00:00.000000",
        "licenseExpiryDate": "2028-01-10T00:00:00.000000",
        "hasTurkishDrivingLicense": true,
        "drivingLicenseNumber": "A12345678",
        "idType": 1,
        "idNumber": "12345678901"
    }
}
Note: driverInfo fields are stored on the partner during draft creation. When /api/book-car is called later, any missing fields in its payload will fall back to the partner data created here.

Expected Response:

{ "success": true, "order_id": 42, "order_no": "S00042" }

Tests:

pm.test("Draft created", () => {
    const json = pm.response.json();
    pm.expect(json.success).to.be.true;
    pm.environment.set('draft_order_id', json.order_id);
});

4. Step 2 – Upload Documents

4.1 POST /api/upload/driver-document

Method: POST   URL: {{base_url}}/api/upload/driver-document

Headers:

X-API-Key: {{api_key}}
Content-Type: application/json
Important: This endpoint does NOT require X-API-Signature. Only the API key is needed.

Body (raw JSON with base64-encoded file):

{
    "order_id": {{draft_order_id}},
    "name": "license_front.jpg",
    "file_data": "<base64-encoded file content>"
}

Expected Response:

{
    "success": true,
    "attachments": [
        { "id": "101", "filename": "license_front.jpg", "url": "/web/content/101?access_token=abc123" }
    ]
}

Tests:

pm.test("Attachment created", () => {
    const json = pm.response.json();
    pm.expect(json.success).to.be.true;
    pm.environment.set('license_photo_id', json.attachments[0].id);
});
Repeat for each document: license_photo_2_id, id_photo_1_id, id_photo_2_id, passport_photo_id, turkey_entrance_stamp_id, findex_report_id, turkish_residency_permit_id, turkish_residency_permit_2_id.

5. Step 3 – Finalize Booking

5.1 POST /api/book-car (Full Validation Flow)

Method: POST   URL: {{base_url}}/api/book-car

Headers:

X-API-Key: {{api_key}}
X-API-Signature: {{request_signature}}
Content-Type: application/json
Auth: Requires API key and HMAC signature.
Partner data fallback: Fields not provided in driverInfo will fall back to the partner data stored during draft creation (Step 3.1). Contact fields (name, phone, email) and core driver fields (birth_date, nationality_id, etc.) are all supported. Document attachment IDs must always be provided explicitly.

Body (raw JSON):

{
    "order_id": "{{draft_order_id}}",
    "pickupDate": "2026-07-01T10:00:00.000000",
    "returnDate": "2026-07-05T10:00:00.000000",
    "selectedCarId": 1,
    "selectedPickupLocationId": 1,
    "selectedReturnLocationId": 1,
    "selectedInsuranceId": 1,
    "selectedServices": [],
    "driverInfo": {
        "fullName": "Ahmet Yılmaz",
        "mobile": "+905551234567",
        "email": "[email protected]",
        "birthDate": "1990-05-15T00:00:00",
        "nationalityId": 224,
        "idType": 1,
        "idNumber": "12345678901",
        "drivingLicenseNumber": "A12345678",
        "licenseIssuingDate": "2018-01-10T00:00:00",
        "licenseExpiryDate": "2028-01-10T00:00:00",
        "hasTurkishDrivingLicense": true,
        "licensePhoto": "{{license_photo_id}}",
        "licensePhoto2": "{{license_photo_2_id}}",
        "idPhoto1": "{{id_photo_1_id}}",
        "idPhoto2": "{{id_photo_2_id}}",
        "tcNumber": "12345678901",
        "turkishResidencyPermit": "{{turkish_residency_permit_id}}",
        "turkishResidencyPermit2": "{{turkish_residency_permit_2_id}}"
    }
}

Expected Response (full order details from _api_order_details):

{
    "order_id": 42,
    "amount_total": 1200.00,
    "car_name": "Toyota Camry",
    "car_id": 5,
    "currency": "TRY",
    "currency_id": 94,
    "currency_symbol": "₺",
    "order_no": "S00042",
    "discount": 0,
    "duration_days": 4,
    "pickup_location": { "id": 1, "name": "Istanbul Airport", "price": 50.0 },
    "return_location": { "id": 1, "name": "Istanbul Airport", "price": 50.0 },
    "insurance": { "id": 1, "name": "Full Insurance", "price_percent": 15.0 },
    "customer": "Ahmet Yılmaz",
    "pickup_date": "2026-07-01 10:00:00",
    "return_date": "2026-07-05 10:00:00",
    "status": "draft",
    "order_lines": [...],
    "added_services": [...]
}

6. Test Scenarios

Scenario 1: Turkish License Holder (TC ID Type)

Pre-conditions: Nationality: Turkish (224), hasTurkishDrivingLicense: true, idType: 1 (TC Kimlik). Upload: license front/back, TC ID front/back.

FieldWhy Required
name, phone, emailBilling address fields (from fullName, mobile, email or partner fallback)
birth_dateCore driver field
id_type, id_numberCore driver field
driving_license_noCore driver field
driving_license_issuing_date / _expiry_dateCore driver field
nationality_idCore driver field
license_photo, license_photo_2License front + back
id_photo_1, id_photo_2TC ID type → front + back
tc_numberTurkish license holder (non-exempt nationality)
turkish_residency_permit, _2Turkish license holder (non-exempt nationality)

Scenario 2: Turkish License Holder (Passport ID Type)

Same as Scenario 1, except idType: 2 (Passport). Replace idPhoto1/idPhoto2 with passportPhoto.

Scenario 3: Non-Turkish License + Turkey Entrance Date

hasTurkishDrivingLicense: false. Must provide turkeyEntranceDate + turkeyEntranceStampPhoto.

Scenario 4: Findex Nationality

Nationality in website.findex_nationality_ids. findexRating must be "excellent". findexReport must be an attachment ID.

Scenario 5: Residency Nationality (TC + Residency Permit)

Nationality in website.residency_nationality_ids and NOT in no_residency_nationality_ids. Must provide tcNumber, turkishResidencyPermit, turkishResidencyPermit2.

Scenario 6: Exempt Nationality

Nationality in website.no_residency_nationality_ids. turkeyEntranceDate, turkeyEntranceStamp, tcNumber, turkishResidencyPermit are NOT required.

7. Error Handling Test Cases

7.1 Missing order_id

{ "success": false, "message": "order_id is required (create a draft first via /api/create-book-car-draft)" }

7.2 Invalid order_id

{ "success": false, "message": "Invalid or non-rental order_id" }

7.3 Missing required fields

{ "success": false, "message": "Validation error", "missing_fields": ["id_photo_1", "id_photo_2"], "invalid_fields": [], "messages": ["Some required fields are empty."] }

7.4 Findex rating not excellent

{ "success": false, "message": "Validation error", "missing_fields": [], "invalid_fields": ["findex_rating"], "messages": ["Findex rating must be Excellent for your nationality."] }

7.5 Invalid date format

{ "success": false, "message": "Invalid pickupDate/returnDate format" }

7.6 Missing API key

{ "success": false, "message": "Invalid or missing API key", "error_code": "AUTH_ERROR" }

7.7 Invalid signature

{ "success": false, "message": "Invalid or missing request signature", "error_code": "AUTH_ERROR" }

7.8 Upload without order_id

{ "success": false, "message": "order_id is required" }

7.9 Upload with non-numeric order_id

{ "success": false, "message": "order_id must be numeric" }

7.10 Upload with no file_data

{ "success": false, "message": "No file uploaded. Provide file_data as base64 string." }

7.10.1 Upload with invalid base64

{ "success": false, "message": "Invalid base64 file_data" }

7.11 Booking failure (uncaught exception)

{ "success": false, "message": "Booking failed: <error detail>" }

8. Field Reference: camelCase → snake_case Mapping

Mobile Payload Key (camelCase)Server Field (snake_case)Type
fullNamenamestring (billing partner name)
mobilephonestring (billing partner phone)
emailemailstring (billing partner email)
birthDatebirth_datedatetime string
nationalityIdnationality_idint (country ID)
idTypeid_typeint (1=TC, 2=Passport)
idNumberid_numberstring
drivingLicenseNumberdriving_license_nostring
licenseIssuingDatedriving_license_issuing_datedatetime string
licenseExpiryDatedriving_license_expiry_datedatetime string
hasTurkishDrivingLicensehas_turkish_licensebool
turkeyEntranceDateturkey_entrance_datedatetime string
licensePhotolicense_photostring (attachment ID)
licensePhoto2license_photo_2string (attachment ID)
turkeyEntranceStampPhototurkey_entrance_stampstring (attachment ID)
turkeyEntranceStampPhoto2turkey_entrance_stamp_2string (attachment ID)
passportPhotopassport_photostring (attachment ID)
passportStampPhotopassport_stamp_photostring (attachment ID)
idPhoto1id_photo_1string (attachment ID)
idPhoto2id_photo_2string (attachment ID)
findexReportfindex_reportstring (attachment ID)
findexRatingfindex_ratingstring ("excellent")
tcNumber / tc_numbertc_numberstring
turkishResidencyPermitturkish_residency_permitstring (attachment ID)
turkishResidencyPermit2turkish_residency_permit_2string (attachment ID)
secondDriverIdNumbersecond_driver_id_numberstring
secondDriverIdTypesecond_driver_id_typeint
secondDriverLicenseNosecond_driver_license_nostring
secondDriverLicenseIssuingDatesecond_driver_license_issuing_datedatetime string
secondDriverBirthDatesecond_driver_birth_datedatetime string
secondDriverNamesecond_driver_namestring
Partner data fallback: Fields like name, phone, email, birth_date, nationality_id, id_type, id_number, driving_license_no, driving_license_issuing_date, driving_license_expiry_date, has_turkish_license, turkey_entrance_date, findex_rating, and tc_number fall back to existing partner data when not provided. Document attachment IDs must always be provided explicitly.

9. Date Format

All datetime fields use ISO 8601 format. The server normalizes T separators to spaces automatically.

%Y-%m-%dT%H:%M:%S  or  %Y-%m-%dT%H:%M:%S.%f

Examples: 2026-07-01T10:00:00, 1990-05-15T00:00:00, 2026-07-01T10:00:00.000000

Note: The draft creation endpoint (/api/create-book-car-draft) requires microseconds (.%f) for pickupDate and returnDate. The book-car endpoint (/api/book-car) accepts both with and without microseconds.

10. Signature Generation (Manual)

For endpoints requiring X-API-Signature, compute HMAC-SHA256 on the sorted JSON body:

Python Example:

import hmac, hashlib, json
data = { ... }
secret = "your_api_secret"
sorted_json = json.dumps(data, sort_keys=True)
signature = hmac.new(secret.encode(), sorted_json.encode(), hashlib.sha256).hexdigest()

11. Conditional Validation Rules Summary

ConditionExtra Required Fields
All driversbirth_date, id_type, id_number, driving_license_no, driving_license_issuing_date, driving_license_expiry_date, nationality_id, license_photo, license_photo_2
id_type = 1 (TC)id_photo_1, id_photo_2
id_type != 1 (Passport)passport_photo
has_turkish_license = true AND nationality NOT in exempt listtc_number, turkish_residency_permit, turkish_residency_permit_2
has_turkish_license = false AND nationality NOT in exempt listturkey_entrance_date, turkey_entrance_stamp
Nationality in findex_nationality_idsfindex_report, findex_rating (must be "excellent")
Nationality in residency_nationality_ids (NOT exempt)tc_number, turkish_residency_permit, turkish_residency_permit_2
Nationality in no_residency_nationality_ids(exempt from residency + entrance requirements)
order.show_second_driver_data = truesecond_driver_id_number, second_driver_id_type, second_driver_name, second_driver_license_no, second_driver_license_issuing_date, second_driver_birth_date

12. Add Services to Rental Order (Additive)

12.1 POST /api/rental/add-service

Adds services to an existing rental order with additive semantics — meaning it adds to existing quantities/instances rather than replacing them. This is different from /api/rental/update-service which uses replacement semantics.

Method: POST   URL: {{base_url}}/api/rental/add-service
Auth: Requires API key and HMAC signature.

12.2 Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
X-API-SignatureYes{{request_signature}}
Content-TypeYesapplication/json

12.3 Request Body

{
    "order_id": 42,
    "services": {
        "5": 2,
        "8": 1
    }
}
FieldTypeRequiredDescription
order_idintYesSale order ID (must be a valid rental order)
servicesobjectYesDictionary mapping service ID (string) to quantity to add (int)
Additive vs. Replacement Semantics:
• /api/rental/add-service — ADDITIVE: Adds to existing quantities. If order already has 2x service #5, sending "5": 1 results in 3x service #5.
• /api/rental/update-service — REPLACEMENT: Replaces all services. Sending "services": [5, 5, 8] removes all existing services and sets exactly 2x service #5 and 1x service #8.

12.4 Service Types and Behavior

Service TypeFieldBehavior
Single Addition Servicesallow_multiple_additions = falseIf service already exists, recalculates price. If not, creates new service line.
Multi-Addition Servicesallow_multiple_additions = trueCreates new instances starting from max_instance + 1. Each instance gets its own sale order line with unique service_instance_id.

12.5 Success Response

{
    "success": true,
    "message": "Services added successfully",
    "order_id": 42,
    "order_total": 1500.00,
    "added_services": [
        {
            "service_id": 5,
            "service_name": "GPS Navigation",
            "quantity_added": 2,
            "total_instances": 3,
            "allow_multiple_additions": true
        },
        {
            "service_id": 8,
            "service_name": "Child Seat",
            "quantity_added": 1,
            "total_instances": 1,
            "allow_multiple_additions": false
        }
    ]
}
FieldTypeDescription
successbooltrue on success
messagestringSuccess message
order_idintSale order ID
order_totalfloatUpdated order total amount
added_servicesarrayList of services added with details
service_idintService record ID
service_namestringService display name
quantity_addedintQuantity added in this request
total_instancesintTotal instances now on the order
allow_multiple_additionsboolWhether service supports multiple additions

12.6 Error Responses

ConditionResponse
Missing API key{"success": false, "message": "Invalid or missing API key", "error_code": "AUTH_ERROR"}
Invalid signature{"success": false, "message": "Invalid or missing request signature", "error_code": "AUTH_ERROR"}
Missing order_id{"success": false, "message": "order_id is required"}
Invalid order_id{"success": false, "message": "Invalid order_id"}
Order not found{"success": false, "message": "Order not found"}
No car line in order{"success": false, "message": "No car line found in order"}
Invalid service_id{"success": false, "message": "Service 999 not found"}
Invalid quantity{"success": false, "message": "Quantity must be a positive integer"}
Server error{"success": false, "message": "An error occurred: <detail>"}

12.7 Example: Adding Multi-Addition Service

Scenario: Order has 2x GPS Navigation (instances 1 and 2). Add 1 more.

Request:
POST /api/rental/add-service
X-API-Key: your_api_key
X-API-Signature: abc123...
Content-Type: application/json

{
    "order_id": 42,
    "services": {
        "5": 1
    }
}
Response:
{
    "success": true,
    "message": "Services added successfully",
    "order_id": 42,
    "order_total": 1550.00,
    "added_services": [
        {
            "service_id": 5,
            "service_name": "GPS Navigation",
            "quantity_added": 1,
            "total_instances": 3,
            "allow_multiple_additions": true
        }
    ]
}

The new GPS instance is created with service_instance_id = 3.

12.8 Example: Adding Single-Addition Service

Scenario: Order does not have Child Seat. Add it.

Request:
POST /api/rental/add-service
X-API-Key: your_api_key
X-API-Signature: def456...
Content-Type: application/json

{
    "order_id": 42,
    "services": {
        "8": 1
    }
}
Response:
{
    "success": true,
    "message": "Services added successfully",
    "order_id": 42,
    "order_total": 1600.00,
    "added_services": [
        {
            "service_id": 8,
            "service_name": "Child Seat",
            "quantity_added": 1,
            "total_instances": 1,
            "allow_multiple_additions": false
        }
    ]
}

12.9 Example: Adding Multiple Services at Once

{
    "order_id": 42,
    "services": {
        "5": 2,
        "8": 1,
        "12": 3
    }
}

This adds 2x GPS Navigation, 1x Child Seat, and 3x Wi-Fi Hotspot in a single request.

12.10 Integration with Booking Flow

When to use /api/rental/add-service:
• After creating a draft order via /api/create-book-car-draft
• Before or after finalizing booking via /api/book-car
• Any time during the rental period to add additional services

When to use /api/rental/update-service:
• When you need to replace all services with a new set
• When syncing service list from an external system
• When you want exact control over which services and quantities are on the order

12.11 Updated End-to-End Sequence with Add-Service

StepActionEndpointNotes
0 Register or Login POST /api/auth/register or POST /api/auth/login Save sessionToken
1 Fetch available cars GET /api/cars?pickup_date=...&return_date=... Returns car list with pricing
2 Create draft order POST /api/create-book-car-draft Save order_id. Can include initial selectedServices: []
3 Upload documents (×N) POST /api/upload/driver-document Upload license, ID, passport, etc.
4 Add services (optional) POST /api/rental/add-service Add services additively. Can be called multiple times.
5 Finalize booking POST /api/book-car Confirms order. Can still add services after this step.
6 Add more services (optional) POST /api/rental/add-service Can add services even after booking is finalized.
7 View order GET /api/user/order/{{order_id}} Verify all services and total

12.12 Signature Requirements

The /api/rental/add-service endpoint requires HMAC-SHA256 signature, same as other booking endpoints:

import hmac, hashlib, json

data = {
    "order_id": 42,
    "services": {"5": 2, "8": 1}
}
secret = "your_api_secret"
sorted_json = json.dumps(data, sort_keys=True)
signature = hmac.new(secret.encode(), sorted_json.encode(), hashlib.sha256).hexdigest()
Signature calculation: The entire request body (including nested services object) is serialized to sorted JSON and signed with HMAC-SHA256 using your api_secret.

12.13 Field Mapping: services Object

Mobile PayloadServer InterpretationDescription
"services": {"5": 2}Service ID 5, quantity 2Add 2 instances of service #5
"services": {"8": 1}Service ID 8, quantity 1Add 1 instance of service #8
Note: Service IDs are passed as strings in the JSON object keys, but are converted to integers server-side. Quantities must be positive integers.

12.14 POST /api/rental/update-service (Replacement Semantics)

Updates services on an existing rental order with replacement semantics — meaning it REPLACES all existing services with the new set. This is fundamentally different from /api/rental/add-service which ADDS to existing services.

Method: POST   URL: {{base_url}}/api/rental/update-service
Auth: Requires API key and HMAC signature.

Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
X-API-SignatureYes{{request_signature}}
Content-TypeYesapplication/json

Request Body

{
    "order_id": 42,
    "services": {
        "5": 2,
        "8": 1,
        "12": 0
    }
}
FieldTypeRequiredDescription
order_idintYesSale order ID (must be a valid rental order)
servicesobjectYesDictionary mapping service ID (string) to desired quantity. Use 0 to remove a service.
Replacement Behavior: The /api/rental/update-service endpoint uses [(6, 0, service_ids)] M2M command, which REPLACES all existing services on the order. Any services not in the services object will be removed. This is NOT additive.

How It Works

  1. All service IDs with quantity > 0 are collected into a list
  2. The Many2many field added_services_ids is replaced with this exact list using [(6, 0, service_ids)]
  3. For each service with quantity > 0, the system calls update_added_services_with_quantities() to create/recalculate sale order lines
  4. Services with quantity = 0 are removed from the order entirely

Success Response

{
    "success": true,
    "message": "Services updated successfully",
    "order_id": 42,
    "order_total": 1500.00
}

Error Responses

ConditionResponse
Missing API key{"success": false, "message": "Invalid or missing API key", "error_code": "AUTH_ERROR"}
Invalid signature{"success": false, "message": "Invalid or missing request signature", "error_code": "AUTH_ERROR"}
Missing order_id{"success": false, "message": "order_id is required"}
Order not found{"success": false, "message": "Order not found"}
No car line{"success": false, "message": "No car line found on order"}
Server error{"success": false, "message": "An error occurred: <detail>"}

Example: Replacing All Services

Scenario: Order currently has GPS (2x), Child Seat (1x), Wi-Fi (1x). Replace with GPS (3x) and Wi-Fi (2x). Child Seat will be removed.

Request:
POST /api/rental/update-service
X-API-Key: your_api_key
X-API-Signature: abc123...
Content-Type: application/json

{
    "order_id": 42,
    "services": {
        "5": 3,
        "12": 2,
        "8": 0
    }
}
Response:
{
    "success": true,
    "message": "Services updated successfully",
    "order_id": 42,
    "order_total": 1650.00
}

Example: Removing All Services

{
    "order_id": 42,
    "services": {}
}

Or set all quantities to 0:

{
    "order_id": 42,
    "services": {
        "5": 0,
        "8": 0,
        "12": 0
    }
}

Both examples remove all added services from the order.

When to Use update-service vs add-service

ScenarioRecommended EndpointReason
User selects services from scratch/api/rental/update-serviceReplacement semantics ensure exact match with UI selection
User wants to add one more service/api/rental/add-serviceAdditive semantics won't remove existing services
Syncing with external service catalog/api/rental/update-serviceReplacement ensures order matches external system state
Adding accessories during rental period/api/rental/add-serviceAdditive semantics preserve existing services
Removing specific services/api/rental/update-serviceCan set quantity to 0 to remove specific services

12.15 Important: selectedServices Format in Booking Endpoints

Critical Distinction: The selectedServices field in booking endpoints (/api/create-book-car-draft and /api/book-car) uses a DIFFERENT format than the services parameter in /api/rental/add-service and /api/rental/update-service.

Booking Endpoints Format

In /api/create-book-car-draft and /api/book-car, the selectedServices field is an array of service IDs (integers):

{
    "pickupDate": "2026-07-01T10:00:00",
    "returnDate": "2026-07-05T10:00:00",
    "selectedCarId": 1,
    "selectedServices": [5, 8, 12],
    "driverInfo": { ... }
}
CharacteristicValue
TypeArray (list)
Element TypeInteger (service ID)
DuplicatesEach service ID appears once regardless of quantity
Quantity SupportNO - each service gets quantity 1
Multi-AdditionNO - only creates one instance per service
SemanticsREPLACEMENT - replaces all previously selected services
Limitation: The selectedServices array in booking endpoints does NOT support quantities. It's a simple presence indicator. If you need multiple instances of a service, use /api/rental/add-service after creating the draft order.

Add/Update Service Endpoints Format

In /api/rental/add-service and /api/rental/update-service, the services field is a dictionary mapping service IDs to quantities:

{
    "order_id": 42,
    "services": {
        "5": 2,
        "8": 1,
        "12": 3
    }
}
CharacteristicValue
TypeObject (dictionary/map)
Key TypeString (service ID as string)
Value TypeInteger (quantity to add or desired quantity)
DuplicatesN/A (keys are unique)
Quantity SupportYES - specify exact quantity
Multi-AdditionYES - creates multiple instances if allow_multiple_additions = true
Semanticsadd-service: ADDITIVE, update-service: REPLACEMENT

Format Comparison Summary

FeatureselectedServices (Booking)services (Add/Update)
Data StructureArray [5, 8, 12]Object {"5": 2, "8": 1}
Quantities❌ No (always 1)✅ Yes (customizable)
Multi-Addition❌ No✅ Yes
When to UseInitial booking onlyAfter order exists
BehaviorReplacementAdditive or Replacement

Recommended Workflow for Services

  1. Step 1: Create draft order with selectedServices: [] (empty array or omit services)
  2. Step 2: Upload documents
  3. Step 3: Use /api/rental/add-service to add services with quantities: {"services": {"5": 2, "8": 1}}
  4. Step 4: Finalize booking with /api/book-car
  5. Step 5: (Optional) Add more services later using /api/rental/add-service
Best Practice: Even though selectedServices is supported in booking endpoints, it has limitations (no quantities, no multi-addition). For full control over services, use the dedicated /api/rental/add-service endpoint after creating the draft order.

Part 2 – Authentication & User Management

13. User Registration

POST /api/auth/register

Creates a new user account and returns a session token. No HMAC signature required – only X-API-Key.

13.1 Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
Content-TypeYesapplication/json
X-RegisterNo (Method 1)Base64-encoded credentials JSON

13.2 Credential Submission Methods

Method 1 – Encoded header (recommended):
Set header X-Register to base64('{"name":"...","email":"...","password":"...","phone":"...","mobile":"..."}').
Body can be empty.

Method 2 – Plain JSON body (backward compatible):
Omit X-Register header and send credentials directly in the JSON body.

13.3 Registration Fields

FieldTypeRequiredDescription
namestringYesFull name
emailstringYesEmail address (used as login)
passwordstringYesPassword
phonestringNoPhone number
mobilestringNoMobile number

13.4 Example Request Body (Method 2)

{
    "name": "Ahmet Yılmaz",
    "email": "[email protected]",
    "password": "SecurePass123!",
    "phone": "+905551234567",
    "mobile": "+905551234567"
}

13.5 Success Response

{
    "success": true,
    "message": "Registration successful",
    "sessionToken": "abc123def456...",
    "expiryDate": "2026-07-12T10:00:00",
    "customerId": 123,
    "userId": 456,
    "customerName": "Ahmet Yılmaz",
    "customerEmail": "[email protected]",
    "customerPhone": "+905551234567"
}

13.6 Error Responses

ConditionResponse
Missing API key{"success": false, "message": "Missing API key"}
Invalid API key{"success": false, "message": "Invalid API key"}
Invalid request data{"success": false, "message": "Invalid request data"}
Email already registered{"success": false, "message": "An error occurred: ..."}
Save sessionToken – it is required for all subsequent authenticated requests (profile, orders, logout, etc.). Store it in a Postman environment variable: session_token.

14. User Login

POST /api/auth/login

Authenticates an existing user and returns a session token. No HMAC signature required.

14.1 Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
Content-TypeYesapplication/json
X-LoginNo (Method 1)Base64-encoded credentials JSON

14.2 Credential Submission Methods

Method 1 – Encoded header (recommended):
Set header X-Login to base64('{"email":"...","password":"..."}').

Method 2 – Plain JSON body (backward compatible):
Omit X-Login header and send credentials in the JSON body.

14.3 Example Request Body (Method 2)

{
    "email": "[email protected]",
    "password": "SecurePass123!"
}

14.4 Success Response

{
    "success": true,
    "message": "Login successful",
    "sessionToken": "xyz789abc123...",
    "expiryDate": "2026-07-12T10:00:00",
    "customerId": 123,
    "userId": 456,
    "customerName": "Ahmet Yılmaz",
    "customerEmail": "[email protected]",
    "customerPhone": "+905551234567"
}

14.5 Error Responses

ConditionResponse
Invalid credentials{"success": false, "message": "An error occurred: ..."}
Missing API key{"success": false, "message": "Missing API key"}
Empty body{"success": false, "message": "Invalid request data"}

15. Session Management

15.1 Refresh Session – POST /api/auth/refresh

Extends the current session and returns a new session token. No HMAC signature required.

Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
X-Session-TokenYes{{session_token}}

Body

None (no request body required).

Success Response

{
    "success": true,
    "message": "Session refreshed successfully",
    "sessionToken": "new_token_abc123...",
    "expiryDate": "2026-07-13T10:00:00"
}

Error Responses

ConditionResponse
Missing session token{"success": false, "message": "Missing session token"}
Expired / invalid session{"success": false, "message": "An error occurred: ..."}
Important: The refresh endpoint returns a new sessionToken. Update your session_token environment variable after every refresh.

15.2 Logout – POST /api/auth/logout

Terminates the current session. No HMAC signature required.

Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
X-Session-TokenYes{{session_token}}

Body

None.

Success Response

{
    "success": true,
    "message": "Logout successful"
}

Error Responses

ConditionResponse
Missing session token{"success": false, "message": "Missing session token"}
Invalid session{"success": false, "message": "An error occurred: ..."}

16. View User Profile

GET /api/user/profile

Returns the authenticated user's full profile including document-presence flags. Requires session authentication.

16.1 Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
X-Session-TokenYes{{session_token}}
Accept-LanguageNo{{lang}} (e.g. en, ar, tr)

16.2 Body

None (GET request).

16.3 Success Response

{
    "success": true,
    "message": "Profile retrieved successfully",
    "customer": {
        "id": 123,
        "name": "Ahmet Yılmaz",
        "email": "[email protected]",
        "phone": "+905551234567",
        "mobile": "+905551234567",
        "mobile_2": "",
        "birth_date": "1990-05-15",
        "nationality_id": 224,
        "nationality_name": "Turkey",
        "nationality_code": "TR",
        "driving_license_no": "A12345678",
        "driving_license_issuing_date": "2018-01-10",
        "driving_license_expiry_date": "2028-01-10",
        "has_turkish_license": false,
        "id_type": 1,
        "id_type_name": "TC Kimlik",
        "id_number": "12345678901",
        "turkey_entrance_date": null,
        "tc_number": "",
        "findex_rating": null,
        "has_passport_photo": false,
        "has_id_photo_1": true,
        "has_id_photo_2": true,
        "has_license_photo": true,
        "has_license_photo_2": false,
        "has_turkey_entrance_stamp": false,
        "has_findex_report": false,
        "has_turkish_residency_permit": false,
        "has_turkish_residency_permit_2": false
    }
}

16.4 Profile Field Reference

FieldTypeDescription
idintPartner record ID
namestringFull name
emailstringLogin email
phone / mobile / mobile_2stringContact numbers
birth_datestring (ISO)Birth date or null
nationality_idintres.country ID
nationality_name / nationality_codestringCountry name and ISO code
driving_license_nostringLicense number
driving_license_issuing_datestring (ISO)Issue date or null
driving_license_expiry_datestring (ISO)Expiry date or null
has_turkish_licenseboolWhether user holds a Turkish license
id_typeintcustom.id.type record ID
id_type_namestringDisplay name of ID type
id_numberstringID / passport number
turkey_entrance_datestring (ISO)Date of entry or null
tc_numberstringTC identity number
findex_ratingstringexcellent / good / average / poor or null
has_* flagsboolDocument upload presence indicators (9 flags)

16.5 Error Responses

ConditionResponse
Missing / invalid session{"success": false, "message": "Invalid or expired session"}
Missing API key{"success": false, "message": "Missing API key"}

17. Update User Profile

POST /api/user/profile/update

Updates the authenticated user's profile. Only supplied fields are updated; omit fields you don't want to change. No HMAC signature required.

HTTP Method: This is POST, not PUT. Binary document fields are not accepted here – use POST /api/upload/driver-document instead.

17.1 Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
X-Session-TokenYes{{session_token}}
Content-TypeYesapplication/json

17.2 Updatable Fields (all optional)

JSON KeyBackend FieldTypeNotes
namenamestringFull name
mobilemobilestringMobile number
mobile2mobile_2stringSecondary mobile
drivingLicenseNodriving_license_nostringLicense number
idNumberid_numberstringID / passport number
tcNumbertc_numberstringTC identity number
hasTurkishDrivingLicensehas_turkish_licenseboolTurkish license flag
birthDatebirth_datestring / nullYYYY-MM-DD or null to clear
licenseIssuingDatedriving_license_issuing_datestring / nullYYYY-MM-DD or null
licenseExpiryDatedriving_license_expiry_datestring / nullYYYY-MM-DD or null
turkeyEntranceDateturkey_entrance_datestring / nullYYYY-MM-DD or null
nationalityIdnationality_idint / nullres.country record ID
idTypeid_typeint / nullcustom.id.type record ID
findexRatingfindex_ratingstring / nullOne of: excellent, good, average, poor

17.3 Example Request Body

{
    "name": "Ahmet Yılmaz",
    "mobile": "+905551234567",
    "birthDate": "1990-05-15",
    "nationalityId": 224,
    "drivingLicenseNo": "A12345678",
    "licenseIssuingDate": "2018-01-10",
    "licenseExpiryDate": "2028-01-10",
    "hasTurkishDrivingLicense": false,
    "idType": 1,
    "idNumber": "12345678901",
    "tcNumber": "12345678901",
    "findexRating": "excellent"
}

17.4 Success Response

{
    "success": true,
    "message": "Profile updated successfully",
    "customer": {
        // ... same full profile payload as GET /api/user/profile ...
    }
}

17.5 Error Responses

ConditionResponse
Invalid body{"success": false, "message": "Request body must be valid JSON"}
No valid fields{"success": false, "message": "No valid fields provided for update"}
Invalid nationalityId{"success": false, "message": "Invalid nationalityId: 999"}
Invalid idType{"success": false, "message": "Invalid idType: 999"}
Invalid findexRating{"success": false, "message": "Invalid findexRating. Must be one of: excellent, good, average, poor"}
Invalid date format{"success": false, "message": "Invalid date format for birthDate. Expected YYYY-MM-DD."}
Missing session{"success": false, "message": "Invalid or expired session"}

18. User Orders

18.1 List Orders – GET /api/user/orders

Returns paginated list of the authenticated user's rental orders.

Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
X-Session-TokenYes{{session_token}}
Accept-LanguageNo{{lang}} (e.g. en, ar, tr)

Query Parameters

ParameterTypeDefaultDescription
limitint10Number of orders to return
offsetint0Pagination offset

Success Response

{
    "success": true,
    "message": "Orders retrieved successfully",
    "orders": [
        {
            "id": 123,
            "name": "SO123",
            "status": "draft",
            "pickup_date": "2026-07-10T10:00:00",
            "return_date": "2026-07-13T10:00:00",
            "car_name": "Toyota Camry",
            "amount_total": 1500.00,
            "currency": "USD",
            "currency_id": 1
        }
    ],
    "count": 5
}
Currency conversion: If the user's session has a preferred_currency_id that differs from the order's currency, amounts are automatically converted for display. The original order is not modified.

18.2 Order Details – GET /api/user/order/<order_id>

Returns full details for a single order. The order must belong to the authenticated user.

Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
X-Session-TokenYes{{session_token}}

Path Parameters

ParameterTypeDescription
order_idintSale order record ID

Success Response

{
    "success": true,
    "message": "Order details retrieved successfully",
    "order": {
        // Full order details from _api_order_details()
        // Includes: id, name, status, car info, dates, pricing,
        // driver data, documents, etc.
    }
}

Error Responses

ConditionResponse
Order not found / not owned{"success": false, "message": "Order not found or access denied"}
Missing session{"success": false, "message": "Invalid or expired session"}

19. Account Deletion

POST /api/auth/delete-account

Archives (soft-deletes) the authenticated user's account. No HMAC signature required.

HTTP Method: This is POST, not DELETE. The endpoint path is /api/auth/delete-account, not /api/user/account.

19.1 Headers

HeaderRequiredValue
X-API-KeyYes{{api_key}}
X-Session-TokenYes{{session_token}}
X-Delete-AccountNo (Method 1)Base64-encoded JSON with optional reason
Content-TypeIf bodyapplication/json

19.2 Data Submission Methods

Method 1 – Encoded header (recommended):
X-Delete-Account: base64('{"reason":"No longer needed"}')

Method 2 – Plain JSON body:
Omit the header and send {"reason": "No longer needed"} in the body.

The reason field is optional in both methods.

19.3 Example Request Body (Method 2)

{
    "reason": "No longer needed"
}

19.4 Success Response

{
    "success": true,
    "message": "Account deleted successfully",
    "deleteDate": "2026-07-11T10:00:00"
}

19.5 Error Responses

ConditionResponse
Missing session token{"success": false, "message": "Missing session token"}
Invalid / expired session{"success": false, "message": "Invalid or expired session"}
User not found{"success": false, "message": "User not found"}

20. Full End-to-End Sequence with User Auth

Complete workflow combining registration/login with the booking flow:

StepActionEndpointKey HeadersNotes
0 Register or Login POST /api/auth/register
or
POST /api/auth/login
X-API-Key Save sessionToken → {{session_token}}
1 (Optional) View profile GET /api/user/profile X-API-Key, X-Session-Token Check existing driver data before booking
2 (Optional) Update profile POST /api/user/profile/update X-API-Key, X-Session-Token Pre-fill driver fields to leverage partner fallback
3 Fetch available cars GET /api/cars?pickup_date=...&return_date=... X-API-Key Returns car list with pricing
4 Create draft order POST /api/create-book-car-draft X-API-Key, X-Signature, X-Timestamp Save order_id → {{order_id}}
5 Upload documents (×N) POST /api/upload/driver-document X-API-Key, X-Signature, X-Timestamp JSON body with base64-encoded file content
6 Finalize booking POST /api/book-car X-API-Key, X-Signature, X-Timestamp Partner data fills missing fields automatically
7 (Optional) View order GET /api/user/order/{{order_id}} X-API-Key, X-Session-Token Verify confirmed booking details
8 (Optional) List all orders GET /api/user/orders?limit=10&offset=0 X-API-Key, X-Session-Token Paginated order history
9 (Optional) Refresh session POST /api/auth/refresh X-API-Key, X-Session-Token Update {{session_token}} with new token
10 Logout POST /api/auth/logout X-API-Key, X-Session-Token Session invalidated server-side
Signature requirements summary:
• /api/create-book-car-draft – HMAC signature required
• /api/upload/driver-document – HMAC signature required
• /api/book-car – HMAC signature required
• /api/rental/add-service – HMAC signature required
• /api/rental/update-service – HMAC signature required
• /api/auth/* – API key only (no signature)
• /api/user/* – API key + session token (no signature)
• GET /api/cars – API key only (no signature)
• GET /api/checkout/config – API key only (no signature)
• /api/payment/esnekpos/* – No authentication required (auth='none'); HTTPS mandatory in production

Part 3 – Payment Integration (EsnekPOS)

21. Payment Integration Overview

The payment_esnekpos module integrates the EsnekPOS payment gateway into the mobile rental API. After a booking is finalized via /api/book-car, the mobile app can initiate a payment, redirect the user to EsnekPOS for card processing (3DS), and verify completion by polling the status endpoint.

Payment Flow Summary

StepActorActionEndpoint / URL
1Mobile AppCreate rental bookingPOST /api/book-car
2Mobile AppInitiate payment with order_idPOST /api/payment/esnekpos/initiate
3OdooCreates payment.transaction, calls EsnekPOS CommonPaymentDealerServer-side
4Mobile AppOpens 3DS payment URL in browser / in-app webviewpayment_url from step 2 response
5UserEnters card details on EsnekPOS page, completes 3DSEsnekPOS hosted page
6EsnekPOSRedirects back to Odoo return URL/payment/esnekpos/return
7OdooProcesses notification, sets transaction state to done or errorServer-side
8Mobile AppPolls payment status until final statePOST /api/payment/esnekpos/status
Module: payment_esnekpos  |  Provider Code: esnekpos  |  Controller: MobilePaymentController (controllers/mobile_api.py)
Prerequisite: A confirmed sale order (sale.order) must exist before initiating payment. Use /api/book-car (Step 3 in Part 1) to create one. The order's amount_total and currency_id are used automatically.

22. Initiate Payment

POST /api/payment/esnekpos/initiate

Method: POST   URL: {{base_url}}/api/payment/esnekpos/initiate

Creates a payment.transaction linked to the given sale order, calls the EsnekPOS CommonPaymentDealer API, and returns the 3DS payment URL for user redirection.

Headers

HeaderRequiredValue
Content-TypeYesapplication/json
Auth: auth='none' — No API key, session token, or HMAC signature required.

Request Body

ParameterTypeRequiredDescription
order_idintYesSale order ID to pay for

Postman Request

POST {{base_url}}/api/payment/esnekpos/initiate
Content-Type: application/json

{
    "order_id": {{draft_order_id}}
}
Implementation Details:
  • The amount and currency are taken from the sale order automatically — do NOT send them separately.
  • A unique transaction reference is generated in the format ESK-{order_name}-{uuid_prefix}.
  • The transaction is linked to the sale order via sale_order_ids.
  • The EsnekPOS provider is selected automatically (first enabled provider with code = 'esnekpos').

Success Response

{
    "success": true,
    "payment_url": "https://pos.esnekpos.com/Pages/CommonPayment.aspx?hash=9f9bde...",
    "transaction_id": 890,
    "hash": "9f9bde8bafc7389395aa24f47e58905d1063cd77268b791dbc0bcf87a01c2148",
    "error": ""
}

Response Fields

FieldTypeDescription
successbooltrue when the request succeeded
payment_urlstringEsnekPOS 3DS payment page URL — redirect/open the user here
transaction_idintInternal Odoo payment.transaction ID — save for status polling
hashstringPayment session hash from EsnekPOS
errorstringEmpty on success; contains EsnekPOS error message if the gateway returned a non-SUCCESS status

Error Responses

ConditionResponse
No JSON body{"success": false, "error": "No JSON data provided"}
Missing order_id{"success": false, "error": "Missing required parameter: order_id is required"}
Order not found{"success": false, "error": "Order not found"}
EsnekPOS provider not configured / disabled{"success": false, "error": "EsnekPOS payment provider is not configured"}
No payment method on provider{"success": false, "error": "No payment method configured for EsnekPOS provider"}
EsnekPOS API failure{"success": false, "error": "<exception message>"}

Postman Test

pm.test("Payment initiated", () => {
    const json = pm.response.json();
    pm.expect(json.success).to.be.true;
    pm.expect(json).to.have.property('payment_url').that.is.a('string');
    pm.expect(json).to.have.property('transaction_id').that.is.a('number');
    pm.expect(json).to.have.property('hash').that.is.a('string');
    pm.environment.set('payment_transaction_id', json.transaction_id);
});

23. Check Payment Status

POST /api/payment/esnekpos/status

Method: POST   URL: {{base_url}}/api/payment/esnekpos/status

Returns the current state of a payment transaction. Use this endpoint to poll for payment completion after redirecting the user to EsnekPOS.

Headers

HeaderRequiredValue
Content-TypeYesapplication/json
Auth: auth='none' — No authentication required.

Request Body

ParameterTypeRequiredDescription
transaction_idintYesThe payment.transaction ID from the initiate response

Postman Request

POST {{base_url}}/api/payment/esnekpos/status
Content-Type: application/json

{
    "transaction_id": {{payment_transaction_id}}
}

Success Response

{
    "success": true,
    "status": "done",
    "provider_reference": "25057",
    "amount": 1500.00,
    "currency": "TRY",
    "error": ""
}

Response Fields

FieldTypeDescription
successbooltrue when the query succeeded
statusstringTransaction state (see table below)
provider_referencestringEsnekPOS reference number (REFNO); empty until EsnekPOS confirms
amountfloatTransaction amount
currencystringCurrency code (e.g. TRY)
errorstringEmpty on success

Transaction Status Values

StatusMeaningFinal?
draftTransaction created, not yet sent to EsnekPOSNo
pendingPayment is being processed by EsnekPOSNo
donePayment completed successfully (STATUS=SUCCESS, RETURN_CODE=0)Yes ✓
cancelPayment cancelled by user or providerYes ✓
errorPayment failed (non-zero RETURN_CODE or non-SUCCESS STATUS)Yes ✓

Error Responses

ConditionResponse
No JSON body{"success": false, "error": "No JSON data provided"}
Missing transaction_id{"success": false, "error": "Missing required parameter: transaction_id"}
Transaction not found{"success": false, "error": "Transaction not found"}
Transaction belongs to a different provider{"success": false, "error": "Invalid payment provider"}

Postman Test

pm.test("Payment status retrieved", () => {
    const json = pm.response.json();
    pm.expect(json.success).to.be.true;
    pm.expect(json).to.have.property('status').that.is.a('string');
    pm.expect(['draft','pending','done','cancel','error']).to.include(json.status);
    pm.expect(json).to.have.property('amount').that.is.a('number');
    pm.expect(json).to.have.property('currency').that.is.a('string');
});

24. Payment Callback & Return URL

Web Return URL: /payment/esnekpos/return

Method: GET, POST   URL: {{base_url}}/payment/esnekpos/return

After the user completes (or cancels) payment on the EsnekPOS page, EsnekPOS redirects the browser back to this URL. The web controller (EsnekPosController.esnekpos_return_from_checkout) processes the notification data and redirects to /payment/status.

Parameters (from EsnekPOS)

ParameterTypeDescription
ORDER_REF_NUMBERstringOrder reference matching the transaction reference (e.g. ESK-SO00042-a1b2c3d4)
STATUSstringSUCCESS or error status
RETURN_CODEstring0 for success
RETURN_MESSAGEstringHuman-readable result message (e.g. SUCCESS)
REFNOstringEsnekPOS internal reference number
HASHstringTransaction hash
Mobile Apps: This URL is handled by the web controller automatically. Mobile apps should not call this endpoint directly. Instead, use POST /api/payment/esnekpos/status to poll for payment completion after the user returns from the EsnekPOS page.
Notification Processing: When STATUS = "SUCCESS" and RETURN_CODE = "0", the transaction is set to done via _set_done(). Otherwise, it is set to error with the status and return message recorded on the transaction.

25. Integration Workflow

Step-by-Step Sequence

StepActionDetails
1Create rental bookingCall POST /api/book-car to finalize the order. Save the order_id.
2Initiate paymentCall POST /api/payment/esnekpos/initiate with {"order_id": N}. Save transaction_id and payment_url.
3Redirect to EsnekPOSOpen payment_url in an in-app browser or system browser. The user enters card details and completes 3DS authentication.
4Detect returnListen for the browser to redirect back to /payment/esnekpos/return. On detection, close the webview.
5Poll for statusCall POST /api/payment/esnekpos/status with {"transaction_id": N} every 2–3 seconds.
6Handle resultWhen status is done → show success. When error or cancel → show failure. Stop polling after a timeout (e.g. 60 seconds).

EsnekPOS Provider Configuration

SettingFieldTest ValueProduction Value
API Base URLesnekpos_base_domainYour Odoo site URL (used for BACK_URL)
Merchant Nameesnekpos_merchant_nameTEST1234Your merchant name
Merchant Keyesnekpos_merchant_keyTest keyProduction key (server-only)
API Endpoint (test)—https://posservicetest.esnekpos.com/api/pay/CommonPaymentDealer
API Endpoint (prod)—https://posservice.esnekpos.com/api/pay/CommonPaymentDealer

EsnekPOS API Payload (Server-Side)

The server sends the following payload to CommonPaymentDealer when _get_specific_rendering_values is called:

{
    "Config": {
        "MERCHANT": "<esnekpos_merchant_name>",
        "MERCHANT_KEY": "<esnekpos_merchant_key>",
        "BACK_URL": "<base_domain>/payment/esnekpos/return",
        "PRICES_CURRENCY": "TRY",
        "ORDER_REF_NUMBER": "ESK-SO00042-a1b2c3d4",
        "ORDER_AMOUNT": 1500.00
    },
    "Customer": {
        "FIRST_NAME": "Ahmet",
        "LAST_NAME": "Yılmaz",
        "MAIL": "[email protected]",
        "PHONE": "+905551234567",
        "CLIENT_IP": "203.0.113.42"
    },
    "Product": []
}

EsnekPOS API Response Fields

FieldTypeDescription
URL_3DSstring3DS payment page URL → returned as payment_url
HASHstringPayment session hash → returned as hash
STATUSstringSUCCESS or error indicator
RETURN_MESSAGEstringError detail when STATUS ≠ SUCCESS
REFNOstringEsnekPOS internal reference

26. Code Examples

26.1 JavaScript / React Native

// ── Payment Service ──────────────────────────────────────────────
class EsnekPosService {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  async initiatePayment(orderId) {
    const res = await fetch(`${this.baseUrl}/api/payment/esnekpos/initiate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ order_id: orderId }),
    });
    const data = await res.json();
    if (!data.success) throw new Error(data.error || 'Payment initiation failed');
    return {
      paymentUrl: data.payment_url,
      transactionId: data.transaction_id,
      hash: data.hash,
    };
  }

  async checkStatus(transactionId) {
    const res = await fetch(`${this.baseUrl}/api/payment/esnekpos/status`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ transaction_id: transactionId }),
    });
    const data = await res.json();
    if (!data.success) throw new Error(data.error || 'Status check failed');
    return {
      status: data.status,
      providerReference: data.provider_reference,
      amount: data.amount,
      currency: data.currency,
    };
  }

  async pollUntilComplete(transactionId, maxAttempts = 30, intervalMs = 2000) {
    for (let i = 0; i < maxAttempts; i++) {
      const { status } = await this.checkStatus(transactionId);
      if (['done', 'cancel', 'error'].includes(status)) return status;
      await new Promise(r => setTimeout(r, intervalMs));
    }
    throw new Error('Payment status polling timed out');
  }
}

// ── React Native Usage ───────────────────────────────────────────
import { Linking, WebView } from 'react-native';

async function handlePayment(orderId) {
  const svc = new EsnekPosService('https://your-odoo.com');
  try {
    const { paymentUrl, transactionId } = await svc.initiatePayment(orderId);

    // Option A: Open in system browser
    await Linking.openURL(paymentUrl);

    // Option B: Navigate to in-app WebView, detect redirect to /payment/esnekpos/return

    const status = await svc.pollUntilComplete(transactionId);
    if (status === 'done') {
      // Show success screen
    } else {
      // Show failure / retry screen
    }
  } catch (err) {
    console.error('Payment error:', err.message);
  }
}

26.2 React Native WebView Integration

import React, { useState } from 'react';
import { WebView } from 'react-native-webview';
import { View, ActivityIndicator, Text } from 'react-native';

function EsnekPaymentPage({ paymentUrl, transactionId, onComplete }) {
  const [polling, setPolling] = useState(false);
  const [result, setResult] = useState(null);
  const svc = new EsnekPosService('https://your-odoo.com');

  const startPolling = async () => {
    setPolling(true);
    try {
      const status = await svc.pollUntilComplete(transactionId);
      setResult(status);
      onComplete?.(status);
    } catch (e) {
      setResult('error');
    } finally {
      setPolling(false);
    }
  };

  return (
    <View style={{ flex: 1 }}>
      {!polling && !result && (
        <WebView
          source={{ uri: paymentUrl }}
          onNavigationStateChange={(navState) => {
            if (navState.url.includes('/payment/esnekpos/return')) {
              startPolling();
            }
          }}
        />
      )}
      {polling && (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
          <ActivityIndicator size="large" />
          <Text>Verifying payment...</Text>
        </View>
      )}
      {result === 'done' && <Text>Payment successful!</Text>}
      {result && result !== 'done' && <Text>Payment {result}.</Text>}
    </View>
  );
}

26.3 Flutter / Dart

import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

// ── Payment Service ──────────────────────────────────────────────
class EsnekPosService {
  final String baseUrl;
  final Client _http = Client();

  EsnekPosService(this.baseUrl);

  Future<Map<String, dynamic>> initiatePayment(int orderId) async {
    final res = await _http.post(
      Uri.parse('$baseUrl/api/payment/esnekpos/initiate'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'order_id': orderId}),
    );
    final data = jsonDecode(res.body);
    if (data['success'] != true) {
      throw Exception(data['error'] ?? 'Payment initiation failed');
    }
    return data; // {payment_url, transaction_id, hash}
  }

  Future<Map<String, dynamic>> checkStatus(int transactionId) async {
    final res = await _http.post(
      Uri.parse('$baseUrl/api/payment/esnekpos/status'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'transaction_id': transactionId}),
    );
    return jsonDecode(res.body);
  }

  Future<String> pollUntilComplete(
    int transactionId, {
    int maxAttempts = 30,
    Duration interval = const Duration(seconds: 2),
  }) async {
    for (var i = 0; i < maxAttempts; i++) {
      final data = await checkStatus(transactionId);
      final status = data['status'] as String;
      if (['done', 'cancel', 'error'].contains(status)) return status;
      await Future.delayed(interval);
    }
    throw TimeoutException('Payment status polling timed out');
  }
}

// ── Flutter WebView Payment Page ─────────────────────────────────
class EsnekPaymentPage extends StatefulWidget {
  final String paymentUrl;
  final int transactionId;
  const EsnekPaymentPage({
    Key? key,
    required this.paymentUrl,
    required this.transactionId,
  }) : super(key: key);

  @override
  State<EsnekPaymentPage> createState() => _EsnekPaymentPageState();
}

class _EsnekPaymentPageState extends State<EsnekPaymentPage> {
  final _svc = EsnekPosService('https://your-odoo.com');
  bool _polling = false;
  String? _result;

  Future<void> _startPolling() async {
    setState(() => _polling = true);
    try {
      final status = await _svc.pollUntilComplete(widget.transactionId);
      setState(() { _result = status; _polling = false; });
    } catch (e) {
      setState(() { _result = 'error'; _polling = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_polling) {
      return const Center(child: CircularProgressIndicator());
    }
    if (_result == 'done') {
      return const Center(child: Text('Payment Successful!',
          style: TextStyle(fontSize: 20, color: Colors.green)));
    }
    if (_result != null) {
      return Center(child: Text('Payment $_result',
          style: const TextStyle(fontSize: 20, color: Colors.red)));
    }
    return WebViewWidget(
      controller: WebViewController()
        ..setNavigationDelegate(
          NavigationDelegate(
            onPageStarted: (url) {
              if (url.contains('/payment/esnekpos/return')) {
                _startPolling();
              }
            },
          ),
        )
        ..loadRequest(Uri.parse(widget.paymentUrl)),
    );
  }
}

27. Error Handling & Status Reference

Response Envelope

All payment API endpoints return JSON with a consistent envelope:

// Success
{ "success": true, ... }

// Failure
{ "success": false, "error": "Human-readable message" }

Common Error Reference

Error MessageHTTP StatusCauseSolution
No JSON data provided200Request body is empty or not valid JSONSend a valid JSON body with Content-Type: application/json
Missing required parameter: order_id is required200order_id not in request bodyInclude "order_id": <int> in the body
Order not found200No sale.order with the given IDVerify order exists via /api/user/orders
EsnekPOS payment provider is not configured200No active EsnekPOS provider foundConfigure provider in Odoo: Settings → Payment Providers
No payment method configured for EsnekPOS provider200Provider has no payment methodsAdd a payment method to the EsnekPOS provider
Missing required parameter: transaction_id200transaction_id not in request bodyInclude "transaction_id": <int> in the body
Transaction not found200No payment.transaction with the given IDUse the transaction_id from the initiate response
Invalid payment provider200Transaction's provider is not esnekposOnly query transactions created via the initiate endpoint
The communication with the API failed...200EsnekPOS API returned an HTTP errorCheck EsnekPOS credentials and network connectivity
Could not establish the connection to the API200Connection error or timeout reaching EsnekPOSVerify EsnekPOS service availability and firewall rules
HTTP Status Codes: Both endpoints use auth='none' and return HTTP 200 for all responses — including errors. Always check the success boolean field in the JSON body, not the HTTP status code, to determine success or failure.

28. Security Considerations

TopicDetails
HTTPS All payment endpoints must be served over HTTPS in production. Payment URLs, transaction IDs, and hashes are transmitted in the response body.
No Client Authentication Both endpoints use auth='none' to allow EsnekPOS callbacks and mobile app access without session tokens. This is by design — the payment flow relies on the EsnekPOS-side authentication of the cardholder.
Merchant Key Confidentiality esnekpos_merchant_key is restricted to base.group_system (Settings admin). Never expose it in mobile app code or API responses.
Provider Verification The status endpoint verifies transaction.provider_code == 'esnekpos' before returning data, preventing cross-provider information leakage.
Transaction Reference Each transaction uses a UUID-prefixed reference (ESK-{name}-{uuid8}) making it difficult to guess valid transaction IDs.
Input Validation Both endpoints validate the presence of required parameters and the existence of referenced records before processing.

29. Testing Checklist

Setup

  1. Set the EsnekPOS payment provider state to Test in Odoo (Settings → Payment Providers → EsnekPOS).
  2. Verify test credentials: Merchant Name = TEST1234, Merchant Key = test key.
  3. Ensure the Odoo server is accessible via HTTPS (or HTTP for local testing).
  4. Create a confirmed sale order via /api/book-car to get a valid order_id.

Verification Steps

#CheckExpected Result
1Call initiate with valid order_idsuccess: true, payment_url is a valid EsnekPOS URL, transaction_id is a number
2Verify response contains hashNon-empty string
3Call status with the returned transaction_idstatus is draft or pending
4Complete payment on EsnekPOS test pageUser redirected back to /payment/esnekpos/return
5Poll status after successful paymentstatus transitions to done, provider_reference is populated
6Call initiate with invalid order_idsuccess: false, error: "Order not found"
7Call initiate without order_idsuccess: false, error: "Missing required parameter..."
8Call status with invalid transaction_idsuccess: false, error: "Transaction not found"
9Verify transaction in Odoo backendSale order shows linked payment transaction in transaction_ids
10Verify amount matches order's amount_totalAmounts are identical

Postman End-to-End Sequence

# 1. Initiate payment → save transaction_id
POST {{base_url}}/api/payment/esnekpos/initiate
Content-Type: application/json

{"order_id": {{draft_order_id}}}

# Response: {"success": true, "payment_url": "...", "transaction_id": 890, ...}
# → Set environment variable: payment_transaction_id = 890

# 2. Check status immediately
POST {{base_url}}/api/payment/esnekpos/status
Content-Type: application/json

{"transaction_id": {{payment_transaction_id}}}

# Response: {"success": true, "status": "draft", ...}

# 3. (User completes payment on EsnekPOS page)

# 4. Poll status until done
POST {{base_url}}/api/payment/esnekpos/status
Content-Type: application/json

{"transaction_id": {{payment_transaction_id}}}

# Response: {"success": true, "status": "done", "provider_reference": "25057", ...}