openapi: 3.0.3 info: title: Carrier Schedule Query API version: 1.0.0 description: Phase-4 carrier tracking from domestic outbound confirmation through actual empty return. paths: /api/platform/v1/carrier-schedule-queries/carriers: get: summary: List official carrier sources and compliant access modes x-permission: platform:firstleg:port-schedule-query:list responses: '200': {description: 'MATSON, SML, ZIM, COSCO, EMC and HEDE descriptors'} /api/platform/v1/carrier-schedule-queries/container-actual-facts: get: summary: Batch query actual carrier-node facts for container workbenches description: Returns at most one row for each container that has a carrier-schedule query. Missing rows mean that no query exists. Estimated facts are never returned or inferred. x-permission: platform:firstleg:port-schedule-query:list parameters: - name: containerBookingIds in: query required: true schema: type: array minItems: 1 maxItems: 200 uniqueItems: true items: {type: integer, format: int64, minimum: 1} responses: '200': description: Actual-only summaries ordered by containerBookingId content: {application/json: {schema: {$ref: '#/components/schemas/ContainerActualFactsListEnvelope'}}} /api/platform/v1/carrier-schedule-queries: get: summary: Query carrier schedule tasks x-permission: platform:firstleg:port-schedule-query:list parameters: - {name: customerId, in: query, schema: {type: integer, format: int64, minimum: 1}} - {name: oceanBookingId, in: query, schema: {type: integer, format: int64, minimum: 1}} - {name: containerBookingId, in: query, schema: {type: integer, format: int64, minimum: 1}} - {name: carrierCode, in: query, schema: {$ref: '#/components/schemas/TaskCarrierCode'}} - {name: status, in: query, schema: {$ref: '#/components/schemas/QueryStatus'}} - {name: expired, in: query, schema: {type: boolean}} - {name: keyword, in: query, schema: {type: string, maxLength: 128}} - {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}} - {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 20}} responses: '200': {description: Query tasks with source and freshness fields; normal execution uses fixed Shanghai slots while retryable technical failures may retry within the current slot} post: summary: Create a query task linked to a real booking and allocated container x-permission: platform:firstleg:port-schedule-query:create parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/CreateRequest'} responses: '200': {description: Created task or idempotent replay} /api/platform/v1/carrier-schedule-queries/{queryId}: parameters: [{$ref: '#/components/parameters/QueryId'}] get: summary: Get task, standardized node facts and immutable business history x-permission: platform:firstleg:port-schedule-query:list responses: '200': description: Stable task detail content: {application/json: {schema: {$ref: '#/components/schemas/DetailEnvelope'}}} /api/platform/v1/carrier-schedule-queries/{queryId}/history: parameters: [{$ref: '#/components/parameters/QueryId'}] get: summary: Get immutable task history x-permission: platform:firstleg:port-schedule-query:list responses: '200': {description: Immutable business history; scheduler attempts are not exposed} /api/platform/v1/carrier-schedule-queries/{queryId}/manual-results: parameters: [{$ref: '#/components/parameters/QueryId'}] post: summary: Link a validated manual port-schedule observation without pretending automated success description: This legacy port-schedule association does not append a standard node and never stops tracking; phase-4 manual facts use /manual-nodes. x-permission: platform:firstleg:port-schedule-query:manual parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ManualResultRequest'} responses: '200': description: Successful manual fallback and next query time content: {application/json: {schema: {$ref: '#/components/schemas/DetailEnvelope'}}} /api/platform/v1/carrier-schedule-queries/{queryId}/manual-nodes: parameters: [{$ref: '#/components/parameters/QueryId'}] post: summary: Append one immutable manual estimated or actual standard-node fact x-permission: platform:firstleg:port-schedule-query:manual parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ManualNodeRequest'} responses: '200': description: Idempotent fact append; estimated facts never advance actual progress, while a manual actual fact must have a strictly greater standard-node sequence than latestActualNode and actual EMPTY_RETURNED stops '409': description: A0409 when expectedVersion is stale or a manual actual node is equal to or earlier than latestActualNode content: {application/json: {schema: {$ref: '#/components/schemas/DetailEnvelope'}}} components: parameters: QueryId: {name: queryId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}} IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, minLength: 1, maxLength: 128}} schemas: CarrierCode: {type: string, enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE]} TaskCarrierCode: type: string enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE, OTHER] description: OTHER is frozen on the ocean booking and creates a manual-only task after outbound confirmation. TrackingType: {type: string, enum: [CONTAINER_NO], description: "Only the normalized physical containerNo frozen by Firstleg is accepted; cabinetNo, orderNo, bookingNo and bill of lading are never fallbacks."} QueryStatus: {type: string, enum: [READY, MANUAL_REQUIRED, STOPPED]} StandardNode: type: string enum: [DEPARTED_WAREHOUSE, AT_ORIGIN_PORT, LOADED_ON_VESSEL, DEPARTED_ORIGIN_PORT, IN_TRANSIT, ARRIVED_DESTINATION_PORT, DISCHARGED_FROM_VESSEL, PICKUP_READY, PICKED_UP, EMPTY_RETURNED] ExceptionCode: {type: string, enum: [CUSTOMS_HOLD, TERMINAL_HOLD, DOCUMENT_HOLD, UNKNOWN_HOLD]} CreateRequest: type: object required: [expectedVersion, customerId, oceanBookingId, containerBookingId, carrierCode, trackingType, trackingNo] properties: expectedVersion: {type: integer, minimum: 0, maximum: 0} customerId: {type: integer, format: int64, minimum: 1} oceanBookingId: {type: integer, format: int64, minimum: 1} containerBookingId: {type: integer, format: int64, minimum: 1} carrierCode: {$ref: '#/components/schemas/CarrierCode'} trackingType: {$ref: '#/components/schemas/TrackingType'} trackingNo: {type: string, minLength: 1, maxLength: 128, description: Normalized physical containerNo; must equal the linked Firstleg container snapshot.} remark: {type: string, nullable: true, maxLength: 1000} ManualResultRequest: type: object required: [expectedVersion, portScheduleId, reason] properties: expectedVersion: {type: integer, minimum: 0} portScheduleId: {type: integer, format: int64, minimum: 1} reason: {type: string, minLength: 1, maxLength: 1000} ManualNodeRequest: type: object required: [expectedVersion, standardNode, observedAt, reason] description: estimatedAt and actualAt cannot both be null; estimatedAt never advances the latest actual node; when actualAt is present, standardNode must have a strictly greater sequence than latestActualNode. properties: expectedVersion: {type: integer, minimum: 0} standardNode: {$ref: '#/components/schemas/StandardNode'} observedAt: {type: string, format: date-time} estimatedAt: {type: string, format: date-time, nullable: true} actualAt: {type: string, format: date-time, nullable: true} locationCode: {type: string, maxLength: 32, nullable: true} locationName: {type: string, maxLength: 128, nullable: true} reason: {type: string, minLength: 1, maxLength: 1000} Task: type: object required: [id, queryNo, customerId, oceanBookingId, containerBookingId, carrierCode, trackingType, trackingNo, status, triggerSource, version] properties: id: {type: integer, format: int64} queryNo: {type: string} customerId: {type: integer, format: int64} oceanBookingId: {type: integer, format: int64} containerBookingId: {type: integer, format: int64} carrierCode: {$ref: '#/components/schemas/TaskCarrierCode'} trackingType: {$ref: '#/components/schemas/TrackingType'} trackingNo: {type: string} officialUrl: {type: string, nullable: true} accessMode: type: string enum: [PUBLIC_FORM, PUBLIC_WEB_PROXY, PUBLIC_HTML, HCAPTCHA, OFFICIAL_API, MANUAL] nullable: true description: MANUAL tasks preserve physical outbound confirmation and accept platform manual node facts without scheduler/provider calls. status: {$ref: '#/components/schemas/QueryStatus'} triggerSource: {type: string} latestActualNode: {allOf: [{$ref: '#/components/schemas/StandardNode'}], nullable: true} latestActualAt: {type: string, format: date-time, nullable: true} lastSuccessAt: {type: string, format: date-time, nullable: true} sourceUpdatedAt: {type: string, format: date-time, nullable: true} expiresAt: {type: string, format: date-time, nullable: true} resultPortScheduleId: {type: integer, format: int64, nullable: true} manualReason: type: string nullable: true description: Explicit provider-disabled or unsupported-carrier reason; unsupported outbound carriers retain the booking shippingCompanyId/name here and through oceanBookingId. lastErrorCode: {type: string, nullable: true} lastErrorMessage: {type: string, nullable: true} stoppedAt: {type: string, format: date-time, nullable: true} stopReason: {type: string, nullable: true} version: {type: integer, minimum: 0} QueryView: type: object required: [query, expired] properties: query: {$ref: '#/components/schemas/Task'} expired: {type: boolean} ContainerActualFacts: type: object required: [containerBookingId, queryId, queryVersion, queryStatus] description: Actual facts only. All node times are null until an immutable fact with actualAt exists; estimatedAt is intentionally absent. properties: containerBookingId: {type: integer, format: int64} queryId: {type: integer, format: int64} queryVersion: {type: integer, minimum: 0} queryStatus: {$ref: '#/components/schemas/QueryStatus'} latestActualNode: {allOf: [{$ref: '#/components/schemas/StandardNode'}], nullable: true} latestActualAt: {type: string, format: date-time, nullable: true} departedWarehouseAt: {type: string, format: date-time, nullable: true} departedOriginPortAt: {type: string, format: date-time, nullable: true} arrivedDestinationPortAt: {type: string, format: date-time, nullable: true} pickupReadyAt: {type: string, format: date-time, nullable: true} pickedUpAt: {type: string, format: date-time, nullable: true} emptyReturnedAt: {type: string, format: date-time, nullable: true} ContainerActualFactsListEnvelope: type: object required: [code, data] properties: code: {type: string, example: '200'} message: {type: string, nullable: true} data: {type: array, items: {$ref: '#/components/schemas/ContainerActualFacts'}} NodeFact: type: object required: [id, providerCode, providerEventId, providerNodeCode, standardNode, nodeSequence, observedAt, rawPayloadHash, createdAt] properties: id: {type: integer, format: int64} providerCode: {type: string, maxLength: 32} providerEventId: {type: string, maxLength: 128} providerNodeCode: {type: string, maxLength: 64} standardNode: {$ref: '#/components/schemas/StandardNode'} nodeSequence: {type: integer, enum: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]} observedAt: {type: string, format: date-time} estimatedAt: {type: string, format: date-time, nullable: true} actualAt: {type: string, format: date-time, nullable: true} locationCode: {type: string, maxLength: 32, nullable: true} locationName: {type: string, maxLength: 128, nullable: true} sourceReference: {type: string, maxLength: 500, nullable: true} rawPayloadHash: {type: string, pattern: '^[0-9a-f]{64}$'} createdAt: {type: string, format: date-time} ExceptionFact: type: object required: [id, providerCode, providerEventId, providerStatus, exceptionCode, active, observedAt, rawPayloadHash, createdAt] properties: id: {type: integer, format: int64} providerCode: {type: string, maxLength: 32} providerEventId: {type: string, maxLength: 128} providerStatus: {type: string, maxLength: 128} exceptionCode: {$ref: '#/components/schemas/ExceptionCode'} active: {type: boolean, description: true means hold occurred; false means released} observedAt: {type: string, format: date-time} locationCode: {type: string, maxLength: 32, nullable: true} locationName: {type: string, maxLength: 128, nullable: true} sourceReference: {type: string, maxLength: 500, nullable: true} rawPayloadHash: {type: string, pattern: '^[0-9a-f]{64}$'} createdAt: {type: string, format: date-time} History: type: object required: [id, action, createdAt] properties: id: {type: integer, format: int64} action: {type: string, enum: [CREATE, AUTO_CREATE, MANUAL_RESULT, MANUAL_NODE, NODE_OBSERVED, EXCEPTION_OBSERVED, PROVIDER_QUERY]} fromVersion: {type: integer, nullable: true} toVersion: {type: integer, nullable: true} reason: {type: string, nullable: true} operatorId: {type: integer, format: int64, nullable: true} operatorName: {type: string, nullable: true} createdAt: {type: string, format: date-time} Detail: type: object required: [query, nodes, exceptions, histories] description: Scheduler lease, attempt and internal execution fields are intentionally absent. properties: query: {$ref: '#/components/schemas/QueryView'} nodes: {type: array, items: {$ref: '#/components/schemas/NodeFact'}} exceptions: {type: array, items: {$ref: '#/components/schemas/ExceptionFact'}} histories: {type: array, items: {$ref: '#/components/schemas/History'}} DetailEnvelope: type: object required: [code, data] properties: code: {type: string, example: '200'} message: {type: string, nullable: true} data: {$ref: '#/components/schemas/Detail'}