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.
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
| Variable | Description | Example Value |
|---|---|---|
base_url | Odoo instance URL | http://localhost:8069 |
api_key | API key from Odoo Settings → Rental → API | your_api_key |
api_secret | API secret for HMAC signatures | your_api_secret |
lang | Accept-Language header value for localized responses | en |
draft_order_id | Set automatically by draft endpoint test | (auto) |
license_photo_id | Attachment ID from upload | (auto) |
license_photo_2_id | Attachment ID from upload | (auto) |
passport_photo_id | Attachment ID from upload | (auto) |
id_photo_1_id | Attachment ID from upload (TC front) | (auto) |
id_photo_2_id | Attachment ID from upload (TC back) | (auto) |
turkey_entrance_stamp_id | Attachment ID from upload | (auto) |
findex_report_id | Attachment ID from upload | (auto) |
turkish_residency_permit_id | Attachment ID from upload | (auto) |
turkish_residency_permit_2_id | Attachment ID from upload | (auto) |
file_base64 | Base64-encoded file for upload | (manual or script) |
selected_car_id | Car ID from /api/cars | (auto or manual) |
pickup_location_id | Pickup location ID | (manual) |
return_location_id | Return location ID | (manual) |
insurance_id | Insurance type ID | (manual) |
nationality_id | Country ID for driver nationality | (manual) |
session_token | Session token from login/register | (auto) |
user_email | Test user email | [email protected] |
user_password | Test user password | SecureP@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
{{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.
{{base_url}}/api/dataQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
pickup_location_id | integer | No | When 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}}
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:
| Field | Type | Description |
|---|---|---|
id_types | array[object] | Available ID document types: {id, name} |
countries | array[object] | All countries: {id, name, code} |
pickup_locations | array[object] | Available pickup locations: {id, name, price, country}. Filtered by stock location when pickup_location_id is provided. |
return_locations | array[object] | Available return locations: {id, name, price, country}. Filtered by stock location when pickup_location_id is provided. |
insurance_services | array[object] | Insurance products: {id, name, price_percent, description, country} |
additional_services | array[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"}
]
}
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.
{{base_url}}/api/checkout/configHeaders:
X-API-Key: {{api_key}}
Accept-Language: {{lang}}
Response Fields:
| Field | Type | Description |
|---|---|---|
success | bool | true on success |
findex_nationality_ids | array[int] | Country IDs that require a Findex report with "Excellent" rating |
residency_nationality_ids | array[int] | Country IDs that require a Turkish Residency Permit and TC Number |
no_residency_nationality_ids | array[int] | Country IDs exempt from residency & entrance info requirements |
findex_nationalities | array[object] | Full country details for Findex nationalities: {id, name, code} |
residency_nationalities | array[object] | Full country details for residency nationalities: {id, name, code} |
no_residency_nationalities | array[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"}
]
}
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
{{base_url}}/api/create-book-car-draftHeaders:
X-API-Key: {{api_key}}
X-API-Signature: {{request_signature}}
Content-Type: application/json
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"
}
}
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
{{base_url}}/api/upload/driver-documentHeaders:
X-API-Key: {{api_key}}
Content-Type: application/json
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);
});
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)
{{base_url}}/api/book-carHeaders:
X-API-Key: {{api_key}}
X-API-Signature: {{request_signature}}
Content-Type: application/json
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.
| Field | Why Required |
|---|---|
name, phone, email | Billing address fields (from fullName, mobile, email or partner fallback) |
birth_date | Core driver field |
id_type, id_number | Core driver field |
driving_license_no | Core driver field |
driving_license_issuing_date / _expiry_date | Core driver field |
nationality_id | Core driver field |
license_photo, license_photo_2 | License front + back |
id_photo_1, id_photo_2 | TC ID type → front + back |
tc_number | Turkish license holder (non-exempt nationality) |
turkish_residency_permit, _2 | Turkish 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 |
|---|---|---|
fullName | name | string (billing partner name) |
mobile | phone | string (billing partner phone) |
email | email | string (billing partner email) |
birthDate | birth_date | datetime string |
nationalityId | nationality_id | int (country ID) |
idType | id_type | int (1=TC, 2=Passport) |
idNumber | id_number | string |
drivingLicenseNumber | driving_license_no | string |
licenseIssuingDate | driving_license_issuing_date | datetime string |
licenseExpiryDate | driving_license_expiry_date | datetime string |
hasTurkishDrivingLicense | has_turkish_license | bool |
turkeyEntranceDate | turkey_entrance_date | datetime string |
licensePhoto | license_photo | string (attachment ID) |
licensePhoto2 | license_photo_2 | string (attachment ID) |
turkeyEntranceStampPhoto | turkey_entrance_stamp | string (attachment ID) |
turkeyEntranceStampPhoto2 | turkey_entrance_stamp_2 | string (attachment ID) |
passportPhoto | passport_photo | string (attachment ID) |
passportStampPhoto | passport_stamp_photo | string (attachment ID) |
idPhoto1 | id_photo_1 | string (attachment ID) |
idPhoto2 | id_photo_2 | string (attachment ID) |
findexReport | findex_report | string (attachment ID) |
findexRating | findex_rating | string ("excellent") |
tcNumber / tc_number | tc_number | string |
turkishResidencyPermit | turkish_residency_permit | string (attachment ID) |
turkishResidencyPermit2 | turkish_residency_permit_2 | string (attachment ID) |
secondDriverIdNumber | second_driver_id_number | string |
secondDriverIdType | second_driver_id_type | int |
secondDriverLicenseNo | second_driver_license_no | string |
secondDriverLicenseIssuingDate | second_driver_license_issuing_date | datetime string |
secondDriverBirthDate | second_driver_birth_date | datetime string |
secondDriverName | second_driver_name | string |
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
/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
| Condition | Extra Required Fields |
|---|---|
| All drivers | birth_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 list | tc_number, turkish_residency_permit, turkish_residency_permit_2 |
has_turkish_license = false AND nationality NOT in exempt list | turkey_entrance_date, turkey_entrance_stamp |
Nationality in findex_nationality_ids | findex_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 = true | second_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.
{{base_url}}/api/rental/add-service12.2 Headers
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
X-API-Signature | Yes | {{request_signature}} |
Content-Type | Yes | application/json |
12.3 Request Body
{
"order_id": 42,
"services": {
"5": 2,
"8": 1
}
}
| Field | Type | Required | Description |
|---|---|---|---|
order_id | int | Yes | Sale order ID (must be a valid rental order) |
services | object | Yes | Dictionary mapping service ID (string) to quantity to add (int) |
•
/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 Type | Field | Behavior |
|---|---|---|
| Single Addition Services | allow_multiple_additions = false | If service already exists, recalculates price. If not, creates new service line. |
| Multi-Addition Services | allow_multiple_additions = true | Creates 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
}
]
}
| Field | Type | Description |
|---|---|---|
success | bool | true on success |
message | string | Success message |
order_id | int | Sale order ID |
order_total | float | Updated order total amount |
added_services | array | List of services added with details |
service_id | int | Service record ID |
service_name | string | Service display name |
quantity_added | int | Quantity added in this request |
total_instances | int | Total instances now on the order |
allow_multiple_additions | bool | Whether service supports multiple additions |
12.6 Error Responses
| Condition | Response |
|---|---|
| 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
/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
| Step | Action | Endpoint | Notes |
|---|---|---|---|
| 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()
services object) is serialized to sorted JSON and signed with HMAC-SHA256 using your api_secret.
12.13 Field Mapping: services Object
| Mobile Payload | Server Interpretation | Description |
|---|---|---|
"services": {"5": 2} | Service ID 5, quantity 2 | Add 2 instances of service #5 |
"services": {"8": 1} | Service ID 8, quantity 1 | Add 1 instance of service #8 |
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.
{{base_url}}/api/rental/update-serviceHeaders
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
X-API-Signature | Yes | {{request_signature}} |
Content-Type | Yes | application/json |
Request Body
{
"order_id": 42,
"services": {
"5": 2,
"8": 1,
"12": 0
}
}
| Field | Type | Required | Description |
|---|---|---|---|
order_id | int | Yes | Sale order ID (must be a valid rental order) |
services | object | Yes | Dictionary mapping service ID (string) to desired quantity. Use 0 to remove a service. |
/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
- All service IDs with quantity > 0 are collected into a list
- The Many2many field
added_services_idsis replaced with this exact list using[(6, 0, service_ids)] - For each service with quantity > 0, the system calls
update_added_services_with_quantities()to create/recalculate sale order lines - 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
| Condition | Response |
|---|---|
| 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
| Scenario | Recommended Endpoint | Reason |
|---|---|---|
| User selects services from scratch | /api/rental/update-service | Replacement semantics ensure exact match with UI selection | User wants to add one more service | /api/rental/add-service | Additive semantics won't remove existing services |
| Syncing with external service catalog | /api/rental/update-service | Replacement ensures order matches external system state |
| Adding accessories during rental period | /api/rental/add-service | Additive semantics preserve existing services |
| Removing specific services | /api/rental/update-service | Can set quantity to 0 to remove specific services |
12.15 Important: selectedServices Format in Booking Endpoints
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": { ... }
}
| Characteristic | Value |
|---|---|
| Type | Array (list) |
| Element Type | Integer (service ID) |
| Duplicates | Each service ID appears once regardless of quantity |
| Quantity Support | NO - each service gets quantity 1 |
| Multi-Addition | NO - only creates one instance per service |
| Semantics | REPLACEMENT - replaces all previously selected services |
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
}
}
| Characteristic | Value |
|---|---|
| Type | Object (dictionary/map) |
| Key Type | String (service ID as string) |
| Value Type | Integer (quantity to add or desired quantity) |
| Duplicates | N/A (keys are unique) |
| Quantity Support | YES - specify exact quantity |
| Multi-Addition | YES - creates multiple instances if allow_multiple_additions = true |
| Semantics | add-service: ADDITIVE, update-service: REPLACEMENT |
Format Comparison Summary
| Feature | selectedServices (Booking) | services (Add/Update) |
|---|---|---|
| Data Structure | Array [5, 8, 12] | Object {"5": 2, "8": 1} |
| Quantities | ❌ No (always 1) | ✅ Yes (customizable) |
| Multi-Addition | ❌ No | ✅ Yes |
| When to Use | Initial booking only | After order exists |
| Behavior | Replacement | Additive or Replacement |
Recommended Workflow for Services
- Step 1: Create draft order with
selectedServices: [](empty array or omit services) - Step 2: Upload documents
- Step 3: Use
/api/rental/add-serviceto add services with quantities:{"services": {"5": 2, "8": 1}} - Step 4: Finalize booking with
/api/book-car - Step 5: (Optional) Add more services later using
/api/rental/add-service
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
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
Content-Type | Yes | application/json |
X-Register | No (Method 1) | Base64-encoded credentials JSON |
13.2 Credential Submission Methods
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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Full name |
email | string | Yes | Email address (used as login) |
password | string | Yes | Password |
phone | string | No | Phone number |
mobile | string | No | Mobile 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
| Condition | Response |
|---|---|
| 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: ..."} |
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
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
Content-Type | Yes | application/json |
X-Login | No (Method 1) | Base64-encoded credentials JSON |
14.2 Credential Submission Methods
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
| Condition | Response |
|---|---|
| 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
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
X-Session-Token | Yes | {{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
| Condition | Response |
|---|---|
| Missing session token | {"success": false, "message": "Missing session token"} |
| Expired / invalid session | {"success": false, "message": "An error occurred: ..."} |
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
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
X-Session-Token | Yes | {{session_token}} |
Body
None.
Success Response
{
"success": true,
"message": "Logout successful"
}
Error Responses
| Condition | Response |
|---|---|
| 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
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
X-Session-Token | Yes | {{session_token}} |
Accept-Language | No | {{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
| Field | Type | Description |
|---|---|---|
id | int | Partner record ID |
name | string | Full name |
email | string | Login email |
phone / mobile / mobile_2 | string | Contact numbers |
birth_date | string (ISO) | Birth date or null |
nationality_id | int | res.country ID |
nationality_name / nationality_code | string | Country name and ISO code |
driving_license_no | string | License number |
driving_license_issuing_date | string (ISO) | Issue date or null |
driving_license_expiry_date | string (ISO) | Expiry date or null |
has_turkish_license | bool | Whether user holds a Turkish license |
id_type | int | custom.id.type record ID |
id_type_name | string | Display name of ID type |
id_number | string | ID / passport number |
turkey_entrance_date | string (ISO) | Date of entry or null |
tc_number | string | TC identity number |
findex_rating | string | excellent / good / average / poor or null |
has_* flags | bool | Document upload presence indicators (9 flags) |
16.5 Error Responses
| Condition | Response |
|---|---|
| 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.
POST, not PUT. Binary document fields are not accepted here – use POST /api/upload/driver-document instead.
17.1 Headers
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
X-Session-Token | Yes | {{session_token}} |
Content-Type | Yes | application/json |
17.2 Updatable Fields (all optional)
| JSON Key | Backend Field | Type | Notes |
|---|---|---|---|
name | name | string | Full name |
mobile | mobile | string | Mobile number |
mobile2 | mobile_2 | string | Secondary mobile |
drivingLicenseNo | driving_license_no | string | License number |
idNumber | id_number | string | ID / passport number |
tcNumber | tc_number | string | TC identity number |
hasTurkishDrivingLicense | has_turkish_license | bool | Turkish license flag |
birthDate | birth_date | string / null | YYYY-MM-DD or null to clear |
licenseIssuingDate | driving_license_issuing_date | string / null | YYYY-MM-DD or null |
licenseExpiryDate | driving_license_expiry_date | string / null | YYYY-MM-DD or null |
turkeyEntranceDate | turkey_entrance_date | string / null | YYYY-MM-DD or null |
nationalityId | nationality_id | int / null | res.country record ID |
idType | id_type | int / null | custom.id.type record ID |
findexRating | findex_rating | string / null | One 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
| Condition | Response |
|---|---|
| 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
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
X-Session-Token | Yes | {{session_token}} |
Accept-Language | No | {{lang}} (e.g. en, ar, tr) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 10 | Number of orders to return |
offset | int | 0 | Pagination 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
}
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
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
X-Session-Token | Yes | {{session_token}} |
Path Parameters
| Parameter | Type | Description |
|---|---|---|
order_id | int | Sale 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
| Condition | Response |
|---|---|
| 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.
POST, not DELETE. The endpoint path is /api/auth/delete-account, not /api/user/account.
19.1 Headers
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | {{api_key}} |
X-Session-Token | Yes | {{session_token}} |
X-Delete-Account | No (Method 1) | Base64-encoded JSON with optional reason |
Content-Type | If body | application/json |
19.2 Data Submission Methods
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
| Condition | Response |
|---|---|
| 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:
| Step | Action | Endpoint | Key Headers | Notes |
|---|---|---|---|---|
| 0 | Register or Login | POST /api/auth/registeror 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 |
•
/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
| Step | Actor | Action | Endpoint / URL |
|---|---|---|---|
| 1 | Mobile App | Create rental booking | POST /api/book-car |
| 2 | Mobile App | Initiate payment with order_id | POST /api/payment/esnekpos/initiate |
| 3 | Odoo | Creates payment.transaction, calls EsnekPOS CommonPaymentDealer | Server-side |
| 4 | Mobile App | Opens 3DS payment URL in browser / in-app webview | payment_url from step 2 response |
| 5 | User | Enters card details on EsnekPOS page, completes 3DS | EsnekPOS hosted page |
| 6 | EsnekPOS | Redirects back to Odoo return URL | /payment/esnekpos/return |
| 7 | Odoo | Processes notification, sets transaction state to done or error | Server-side |
| 8 | Mobile App | Polls payment status until final state | POST /api/payment/esnekpos/status |
payment_esnekpos |
Provider Code: esnekpos |
Controller: MobilePaymentController (controllers/mobile_api.py)
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
{{base_url}}/api/payment/esnekpos/initiateCreates a payment.transaction linked to the given sale order, calls the EsnekPOS CommonPaymentDealer API, and returns the 3DS payment URL for user redirection.
Headers
| Header | Required | Value |
|---|---|---|
Content-Type | Yes | application/json |
auth='none' — No API key, session token, or HMAC signature required.Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
order_id | int | Yes | Sale order ID to pay for |
Postman Request
POST {{base_url}}/api/payment/esnekpos/initiate
Content-Type: application/json
{
"order_id": {{draft_order_id}}
}
- 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
| Field | Type | Description |
|---|---|---|
success | bool | true when the request succeeded |
payment_url | string | EsnekPOS 3DS payment page URL — redirect/open the user here |
transaction_id | int | Internal Odoo payment.transaction ID — save for status polling |
hash | string | Payment session hash from EsnekPOS |
error | string | Empty on success; contains EsnekPOS error message if the gateway returned a non-SUCCESS status |
Error Responses
| Condition | Response |
|---|---|
| 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
{{base_url}}/api/payment/esnekpos/statusReturns the current state of a payment transaction. Use this endpoint to poll for payment completion after redirecting the user to EsnekPOS.
Headers
| Header | Required | Value |
|---|---|---|
Content-Type | Yes | application/json |
auth='none' — No authentication required.Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
transaction_id | int | Yes | The 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
| Field | Type | Description |
|---|---|---|
success | bool | true when the query succeeded |
status | string | Transaction state (see table below) |
provider_reference | string | EsnekPOS reference number (REFNO); empty until EsnekPOS confirms |
amount | float | Transaction amount |
currency | string | Currency code (e.g. TRY) |
error | string | Empty on success |
Transaction Status Values
| Status | Meaning | Final? |
|---|---|---|
draft | Transaction created, not yet sent to EsnekPOS | No |
pending | Payment is being processed by EsnekPOS | No |
done | Payment completed successfully (STATUS=SUCCESS, RETURN_CODE=0) | Yes ✓ |
cancel | Payment cancelled by user or provider | Yes ✓ |
error | Payment failed (non-zero RETURN_CODE or non-SUCCESS STATUS) | Yes ✓ |
Error Responses
| Condition | Response |
|---|---|
| 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
{{base_url}}/payment/esnekpos/returnAfter 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)
| Parameter | Type | Description |
|---|---|---|
ORDER_REF_NUMBER | string | Order reference matching the transaction reference (e.g. ESK-SO00042-a1b2c3d4) |
STATUS | string | SUCCESS or error status |
RETURN_CODE | string | 0 for success |
RETURN_MESSAGE | string | Human-readable result message (e.g. SUCCESS) |
REFNO | string | EsnekPOS internal reference number |
HASH | string | Transaction hash |
POST /api/payment/esnekpos/status to poll for payment completion after the user returns from the EsnekPOS page.
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
| Step | Action | Details |
|---|---|---|
| 1 | Create rental booking | Call POST /api/book-car to finalize the order. Save the order_id. |
| 2 | Initiate payment | Call POST /api/payment/esnekpos/initiate with {"order_id": N}. Save transaction_id and payment_url. |
| 3 | Redirect to EsnekPOS | Open payment_url in an in-app browser or system browser. The user enters card details and completes 3DS authentication. |
| 4 | Detect return | Listen for the browser to redirect back to /payment/esnekpos/return. On detection, close the webview. |
| 5 | Poll for status | Call POST /api/payment/esnekpos/status with {"transaction_id": N} every 2–3 seconds. |
| 6 | Handle result | When status is done → show success. When error or cancel → show failure. Stop polling after a timeout (e.g. 60 seconds). |
EsnekPOS Provider Configuration
| Setting | Field | Test Value | Production Value |
|---|---|---|---|
| API Base URL | esnekpos_base_domain | Your Odoo site URL (used for BACK_URL) | |
| Merchant Name | esnekpos_merchant_name | TEST1234 | Your merchant name |
| Merchant Key | esnekpos_merchant_key | Test key | Production 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
| Field | Type | Description |
|---|---|---|
URL_3DS | string | 3DS payment page URL → returned as payment_url |
HASH | string | Payment session hash → returned as hash |
STATUS | string | SUCCESS or error indicator |
RETURN_MESSAGE | string | Error detail when STATUS ≠ SUCCESS |
REFNO | string | EsnekPOS 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 Message | HTTP Status | Cause | Solution |
|---|---|---|---|
No JSON data provided | 200 | Request body is empty or not valid JSON | Send a valid JSON body with Content-Type: application/json |
Missing required parameter: order_id is required | 200 | order_id not in request body | Include "order_id": <int> in the body |
Order not found | 200 | No sale.order with the given ID | Verify order exists via /api/user/orders |
EsnekPOS payment provider is not configured | 200 | No active EsnekPOS provider found | Configure provider in Odoo: Settings → Payment Providers |
No payment method configured for EsnekPOS provider | 200 | Provider has no payment methods | Add a payment method to the EsnekPOS provider |
Missing required parameter: transaction_id | 200 | transaction_id not in request body | Include "transaction_id": <int> in the body |
Transaction not found | 200 | No payment.transaction with the given ID | Use the transaction_id from the initiate response |
Invalid payment provider | 200 | Transaction's provider is not esnekpos | Only query transactions created via the initiate endpoint |
The communication with the API failed... | 200 | EsnekPOS API returned an HTTP error | Check EsnekPOS credentials and network connectivity |
Could not establish the connection to the API | 200 | Connection error or timeout reaching EsnekPOS | Verify EsnekPOS service availability and firewall rules |
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
| Topic | Details |
|---|---|
| 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
- Set the EsnekPOS payment provider state to Test in Odoo (Settings → Payment Providers → EsnekPOS).
- Verify test credentials: Merchant Name =
TEST1234, Merchant Key = test key. - Ensure the Odoo server is accessible via HTTPS (or HTTP for local testing).
- Create a confirmed sale order via
/api/book-carto get a validorder_id.
Verification Steps
| # | Check | Expected Result |
|---|---|---|
| 1 | Call initiate with valid order_id | success: true, payment_url is a valid EsnekPOS URL, transaction_id is a number |
| 2 | Verify response contains hash | Non-empty string |
| 3 | Call status with the returned transaction_id | status is draft or pending |
| 4 | Complete payment on EsnekPOS test page | User redirected back to /payment/esnekpos/return |
| 5 | Poll status after successful payment | status transitions to done, provider_reference is populated |
| 6 | Call initiate with invalid order_id | success: false, error: "Order not found" |
| 7 | Call initiate without order_id | success: false, error: "Missing required parameter..." |
| 8 | Call status with invalid transaction_id | success: false, error: "Transaction not found" |
| 9 | Verify transaction in Odoo backend | Sale order shows linked payment transaction in transaction_ids |
| 10 | Verify amount matches order's amount_total | Amounts 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", ...}