Inbound Work Orders¶
Endpoint¶
text
POST /api/v1/inbound/work-orders
POST /api/v1/inbound/work-orders/test
POST /api/v1/inbound/work-orders accepts ERP work-order wrapper payloads, validates them, and persists accepted data to MongoDB. Behavior includes product upsert, project/production/stage merge, machine and employee resolution, and project-level partial processing. It is not validation-only.
POST /api/v1/inbound/work-orders/test is a public format-validation endpoint. It checks JSON/XML syntax and the request contract only. It does not require a Bearer token, does not use Idempotency-Key, does not read or write MongoDB, and does not create a request archive record.
The live endpoint’s authentication, idempotency, archive, and persistence behavior is unchanged.
See Work-Order Persistence for live merge and resolution rules.
Authentication¶
Live endpoint¶
When AUTH_ENABLED=true, live requests must include a JWT Bearer access token:
http
Authorization: Bearer <access_token>
Obtain the token from POST /api/v1/auth/token. See Authentication.
When AUTH_ENABLED=false, the live endpoint remains open without a Bearer token for local development, but persistence still writes to MongoDB.
Missing tokens return 401 AUTHENTICATION_REQUIRED. Invalid or expired tokens return 401 INVALID_TOKEN. Authentication error responses honor JSON and XML Accept negotiation.
Test endpoint¶
POST /api/v1/inbound/work-orders/test is public. Authentication is not required even when AUTH_ENABLED=true. A token may be sent but is ignored for authorization.
Tenant fields (company_id, organization_id) on the live endpoint are taken from the request root or from configured environment defaults. They are not derived from token claims in this release. The test endpoint does not perform tenant environment fallback or database tenant checks; when present, tenant fields are validated only as contract strings (ObjectID hex format).
Contract references in the repository:
- OpenAPI:
api/openapi.yaml - XML Schema (XSD):
api/schemas/inbound-work-orders.xsd - Published OpenAPI on GitHub: api/openapi.yaml
- Published XSD on GitHub: api/schemas/inbound-work-orders.xsd
Content types¶
Request Content-Type¶
| Value | Description |
|---|---|
application/json |
JSON payload |
application/xml |
XML payload |
text/xml |
XML payload |
Parameters such as charset=utf-8 are supported.
Unsupported request Content-Type values return 415 Unsupported Media Type.
Response Accept¶
| Value | Description |
|---|---|
application/json |
JSON response |
application/xml |
XML response |
text/xml |
XML response |
*/* |
Match request format |
When Accept is omitted, the response uses the same format as the request Content-Type.
Unsupported Accept values return 406 Not Acceptable.
Tenant fields¶
Root fields company_id and organization_id are an optional pair in the request body.
| Situation | Result |
|---|---|
| Both request values present | Use the request pair |
| Neither request value present | Use DEFAULT_COMPANY_ID and DEFAULT_ORGANIZATION_ID when both environment values are configured |
| Exactly one request value present | 400 TENANT_CONTEXT_INCOMPLETE (environment defaults are not applied) |
| Neither request nor configured defaults | 400 TENANT_CONTEXT_REQUIRED |
| Invalid ObjectID in a provided value | 400 INVALID_TENANT_CONTEXT |
When both fields are present in the request, each must be a 24-character hexadecimal MongoDB ObjectID string (^[A-Fa-f0-9]{24}$).
XML payloads follow the same rules. The XSD marks both elements as optional (minOccurs="0") because tenant resolution may use environment defaults when both are omitted.
See Configuration for DEFAULT_* startup validation.
Request examples¶
JSON¶
json
{
"company_id": "507f1f77bcf86cd799439011",
"organization_id": "507f191e810c19729de860ea",
"projects": [
{
"project_name": "Proje Adı",
"project_code": "PROJE-001",
"start_date": "2026-08-06T08:00:00Z",
"estimated_end_date": "2026-08-30T17:00:00Z",
"description": "Proje açıklaması",
"productions": [
{
"production_name": "Üretim Emri Adı",
"production_code": "URETIM-001",
"planned_start_datetime": "2026-08-06T08:00:00Z",
"planned_finish_datetime": "2026-08-10T17:00:00Z",
"remarks": "Üretim emri açıklaması",
"production_stages": [
{
"production_stage_name": "Kesim",
"production_stage_code": "ASAMA-001",
"stage_number": 1,
"stage_type": "yeni",
"planning_start_time": "2026-08-06T08:00:00Z",
"planning_end_time": "2026-08-07T17:00:00Z",
"machines": [
{
"machine_code": "MACHINE-001",
"machine_name": "CNC Makinesi",
"output_products": [
{
"product_name": "Makine Çıktı Ürünü",
"product_code": "URUN-001",
"category": "Yarı Mamul",
"unit": "adet",
"quantity": 30,
"ideal_cycle_time": 1700
}
]
}
],
"employees": [
{
"username": "operator.user"
}
],
"input_products": [
{
"product_name": "Aşama Girdi Ürünü",
"product_code": "HAM-001",
"category": "Hammadde",
"unit": "adet",
"quantity": 30,
"ideal_cycle_time": 0
}
],
"output_products": [
{
"product_name": "Aşama Çıktı Ürünü",
"product_code": "YARI-001",
"category": "Yarı Mamul",
"unit": "adet",
"quantity": 30,
"ideal_cycle_time": 1700
}
]
}
],
"input_products": [
{
"product_name": "Üretim Girdi Ürünü",
"product_code": "HAM-001",
"category": "Hammadde",
"unit": "adet",
"quantity": 30,
"ideal_cycle_time": 0
}
],
"final_products": [
{
"product_name": "Nihai Ürün",
"product_code": "NIHAI-001",
"category": "Nihai Mamul",
"unit": "adet",
"quantity": 30,
"ideal_cycle_time": 1700
}
]
}
]
}
]
}
XML¶
```xml
Response statuses¶
| HTTP | data.status / error code |
When |
|---|---|---|
| 200 | success |
All received projects were processed |
| 200 | partial_success |
At least one project processed; one or more projects, productions, or stages skipped |
| 409 | NO_PROJECT_PROCESSED |
Structurally valid request, but no project could be persisted |
| 409 | INTEGRATION_ACTOR_USER_NOT_FOUND |
Configured integration actor user does not exist |
| 409 | INTEGRATION_ACTOR_USER_INACTIVE |
Configured integration actor user is not active |
| 409 | INTEGRATION_ACTOR_TENANT_MISMATCH |
Configured actor does not belong to the request tenant |
| 503 | DATABASE_UNAVAILABLE |
MongoDB connection, server selection, or timeout failure |
JSON success / partial success¶
json
{
"success": true,
"request_id": "uuid",
"data": {
"message": "Work-order payload processed.",
"status": "partial_success",
"summary": {
"received_projects": 3,
"processed_projects": 2,
"skipped_projects": 1,
"created_projects": 1,
"updated_projects": 1,
"received_productions": 4,
"processed_productions": 3,
"skipped_productions": 1,
"received_stages": 6,
"processed_stages": 5,
"skipped_stages": 1,
"product_occurrences": 20,
"created_products": 2,
"updated_products": 4,
"resolved_machines": 3,
"resolved_employees": 2,
"unresolved_employees": 1
},
"results": [
{
"project_code": "PRJ-001",
"status": "processed",
"action": "created",
"warnings": []
},
{
"project_code": "PRJ-002",
"status": "skipped",
"action": "none",
"warnings": [
{
"code": "PROJECT_NAME_CONFLICT",
"project_code": "PRJ-002",
"message": "The project_code exists with a different project_name."
}
]
}
]
},
"error": null
}
Per-project result fields¶
| Field | Values |
|---|---|
status |
processed, processed_with_warnings, skipped |
action |
created, updated, none |
warnings |
Array of warning objects (code, optional context fields, message) |
Persistence behavior (summary)¶
| Area | Behavior |
|---|---|
| Products | Upsert by company_id + organization_id + product_code |
| Projects | Create or merge by tenant + project_code; name mismatch skips the project |
| Productions / stages | Merge by code within the parent; omitted existing children are preserved |
| Machines | Read-only catalog resolution; missing machine skips the whole stage |
| Employees | Read-only user resolution; missing username skips only that assignment |
| Partial processing | Projects are independent; one skip does not stop others |
Operational fields such as production_stage_operation, stage files, and production files are preserved on update.
Collection tenant type differences¶
Existing MongoDB schemas differ by collection and are preserved:
| Collection | company_id / organization_id type |
|---|---|
projects, machines |
ObjectID |
products, users |
string |
Integration actor¶
Audit fields use INTEGRATION_ACTOR_USER_ID (24-character ObjectID hex of an existing active user). It must reference a real MongoDB user — never a generated random ObjectID. Config load checks required + format only. Live POST additionally checks exists, active, and tenant match. Responses never include the actor ObjectID. See Configuration and Error Codes.
Idempotency and archive¶
Live endpoint requires Idempotency-Key when idempotency is enabled/required. Same key + same canonical payload replays the stored logical response without re-running ApplyPlan. Same key + different payload returns 409 IDEMPOTENCY_KEY_CONFLICT.
Every live attempt is archived in api_request_archive when archive is enabled. The test endpoint writes business collections: 0, archive: 0, and never writes api_idempotency_records.
See:
Standalone MongoDB¶
Persistence is compatible with standalone MongoDB. Multi-document transactions are not required. Product master writes are independent of the project document write; if a project write fails after products were upserted, those product rows may remain.
Required fields¶
| Level | Required fields |
|---|---|
| Request | company_id, organization_id, projects (min 1 item) |
| Project | project_name, project_code, start_date, estimated_end_date, productions |
| Production | production_name, production_code, planned_start_datetime, planned_finish_datetime, production_stages, input_products, final_products |
| Production stage | production_stage_name, production_stage_code, stage_number, stage_type, planning_start_time, planning_end_time, input_products, output_products |
| Product | product_name, product_code, category, unit, quantity, ideal_cycle_time |
| Machine (when present) | machine_code, output_products (min 1 item) |
| Employee (when present) | username |
Optional fields¶
| Field | Notes |
|---|---|
project.description |
Optional string |
production.remarks |
Optional string |
production_stage.description |
Optional string |
machine.machine_name |
Optional string (informational; canonical name comes from MongoDB) |
production_stage.machines |
Optional array; may be omitted or empty |
production_stage.employees |
Optional array; may be omitted or empty |
Array rules¶
Required arrays must contain at least one item:
projects,productions,production_stagesproduction.input_products,production.final_productsproduction_stage.input_products,production_stage.output_productsmachine.output_productswhen a machine entry is present
machines and employees may be omitted or sent as empty arrays.
Single objects are not coerced into arrays; missing or empty required arrays return validation errors.
Within one request, duplicate structural codes are rejected (DUPLICATE_PROJECT_CODE, DUPLICATE_PRODUCTION_CODE, DUPLICATE_PRODUCTION_STAGE_CODE, DUPLICATE_MACHINE_CODE, DUPLICATE_EMPLOYEE_USERNAME). Repeated product_code across occurrences is allowed.
UTC Z date rule¶
All date-time fields must be RFC 3339 UTC values ending with Z.
Accepted examples:
2026-08-06T08:30:00Z2026-08-06T08:30:00.123Z
Rejected examples:
2026-08-06T11:30:00+03:002026-08-06T08:30:00+00:002026-08-06 08:30:00
Date ordering checks:
estimated_end_datemust not be beforestart_dateplanned_finish_datetimemust not be beforeplanned_start_datetimeplanning_end_timemust not be beforeplanning_start_time
Equal start/end timestamps are allowed.
Integer rules¶
| Field | Rule |
|---|---|
stage_number |
Integer, must be > 0 |
quantity |
Integer, must be >= 0 |
ideal_cycle_time |
Integer, must be >= 0 |
JSON floats, strings, booleans, and null values are rejected for integer fields.
stage_type normalization¶
Accepted values after normalization:
yenirevize
Case variants such as YENİ or Revize are normalized using Turkish casing rules. Other values are rejected.
Strict unknown fields¶
- JSON: unknown properties are rejected (
DisallowUnknownFields). - XML: unknown elements are rejected with a field path in error details.
- XML DOCTYPE/DTD declarations are rejected.
Work-order test endpoint¶
text
POST /api/v1/inbound/work-orders/test
Public format-validation endpoint for the same JSON/XML request contract as the live endpoint.
| Behavior | Detail |
|---|---|
| Authentication | Not required (public even when AUTH_ENABLED=true) |
| Idempotency-Key | Not required; ignored if sent |
| MongoDB | No reads and no writes |
| Request archive | No archive document |
| Business checks | No machine/employee/project existence checks |
| Formats | Same Content-Type / Accept / body limit / strict unknown-field rules |
| Response | status: valid, write_performed: false, database_accessed: false, archive_written: false |
HTTP status meanings for format failures: 200 / 400 / 406 / 413 / 415. This endpoint does not return UNAUTHORIZED, idempotency archive/store codes, REQUEST_ARCHIVE_UNAVAILABLE, MACHINE_NOT_FOUND, EMPLOYEE_NOT_FOUND, or PROJECT_CONFLICT.
A 200 response means the payload format and contract are valid. It does not mean a later live write will succeed.
Postman / curl JSON example¶
http
POST /api/v1/inbound/work-orders/test
Content-Type: application/json
Accept: application/json
No Authorization or Idempotency-Key header is required. Use the same JSON body shape as the live request examples above.
Postman / curl XML example¶
http
POST /api/v1/inbound/work-orders/test
Content-Type: application/xml
Accept: application/xml
JSON test success example¶
json
{
"success": true,
"request_id": "uuid",
"data": {
"message": "Work-order payload format is valid.",
"status": "valid",
"content_type": "application/json",
"write_performed": false,
"database_accessed": false,
"archive_written": false
},
"error": null
}
Body limit¶
Configure with INBOUND_MAX_BODY_BYTES (default: 10 MiB / 10485760 bytes). Exceeding the limit returns 413 Payload Too Large.
Contract validation error details¶
JSON/XML syntax errors and contract errors are classified separately. Contract details always include a full nested path with array indexes so an ERP operator can locate the exact field, for example:
projects[0].productions[0].production_stages[0].machines[0].output_products[0].ideal_cycle_time
Invalid integer type (not malformed JSON)¶
json
{
"success": false,
"request_id": "uuid",
"data": null,
"error": {
"code": "INVALID_FIELD_TYPE",
"message": "Request contains a field with an invalid type.",
"details": [
{
"field": "projects[0].productions[0].production_stages[0].machines[0].output_products[0].ideal_cycle_time",
"code": "INVALID_TYPE",
"message": "Field 'ideal_cycle_time' must be an integer.",
"expected_type": "integer",
"actual_type": "string",
"received_value": "asd"
}
]
}
}
Sending "ideal_cycle_time": "asd" must not return MALFORMED_JSON.
Other contract examples¶
| Scenario | Top-level code | Detail code |
|---|---|---|
True JSON syntax break ({"projects":[) |
MALFORMED_JSON |
JSON_SYNTAX_ERROR (+ line/column/byte_offset) |
| Unknown nested field | UNKNOWN_FIELD |
UNKNOWN_FIELD |
| Missing required field | REQUIRED_FIELD_MISSING |
REQUIRED |
Invalid stage_type |
VALIDATION_ERROR |
INVALID_ENUM_VALUE |
| Invalid RFC3339 date | VALIDATION_ERROR |
INVALID_DATETIME |
Negative quantity |
VALIDATION_ERROR |
VALUE_OUT_OF_RANGE |
Multiple contract errors in one syntactically valid body are returned together under VALIDATION_ERROR (capped at 100 details). received_value is limited to safe primitives and long strings are truncated.
Error and warning codes¶
| Code | HTTP | Meaning |
|---|---|---|
MALFORMED_JSON |
400 | JSON syntax cannot be parsed |
MALFORMED_XML |
400 | XML syntax cannot be parsed or DOCTYPE rejected |
INVALID_FIELD_TYPE |
400 | Field type mismatch (for example string instead of integer) |
REQUIRED_FIELD_MISSING |
400 | One or more required fields are missing |
UNKNOWN_FIELD |
400 | Unknown JSON property or XML element |
VALIDATION_ERROR |
400 | One or more contract validation errors |
VALIDATION_FAILED |
400 | Legacy alias retained for some validation failures |
DUPLICATE_* |
400 | Duplicate structural codes inside one request (see Error Codes) |
AUTHENTICATION_REQUIRED / INVALID_TOKEN |
401 | Auth failures when enabled (live endpoint) |
NOT_ACCEPTABLE |
406 | Unsupported Accept header |
NO_PROJECT_PROCESSED |
409 | No project could be persisted (live) |
INTEGRATION_ACTOR_USER_NOT_FOUND |
409 | Configured actor user was not found (live) |
INTEGRATION_ACTOR_USER_INACTIVE |
409 | Configured actor user is not active (live) |
INTEGRATION_ACTOR_TENANT_MISMATCH |
409 | Configured actor does not belong to the request tenant (live) |
PAYLOAD_TOO_LARGE |
413 | Body exceeds inbound limit |
UNSUPPORTED_MEDIA_TYPE |
415 | Unsupported Content-Type |
INTERNAL_ERROR |
500 | Unexpected server error |
DATABASE_UNAVAILABLE |
503 | MongoDB unavailable (live) |
Result/warning codes (in live results[].warnings or 409 details): PROJECT_NAME_CONFLICT, MACHINE_NOT_FOUND, EMPLOYEE_NOT_FOUND, ALL_STAGES_SKIPPED, ALL_PRODUCTIONS_SKIPPED.
See Error Codes for the full list.