Developer Support
Complete documentation for the Logistics Platform API, including order ingestion, management, and SDKs.
Order Ingestion API
Base URL
https://api.logisai.com/api/v1Authentication
All API requests require a valid Bearer token in the Authorization header.
Authorization: Bearer <your-api-token>API Summary
| Endpoint | Method | Permission | Description |
|---|---|---|---|
| /orders/ingest | POST | orders:create | Submit orders (async, max 500) |
| /orders/batch/{batchId}/status | GET | orders:read | Check batch status |
| /orders/rejected | GET | orders:read | Query rejected orders |
| /orders/{orderId}/cancel | POST | orders:cancel | Cancel single order by ID |
| /orders/cancel/bulk | POST | orders:cancel | Bulk cancel by order_no (max 100) |
Cancel Order
Cancel an existing order. If assigned, the route will be automatically modified.
POST
/api/v1/orders/{orderId}/cancelRequest Body
{
"reason": "Customer requested cancellation"
}Bulk Cancel Orders
Cancel multiple orders in a single request using order numbers.
POST
/api/v1/orders/cancel/bulkRequest Body
{
"orders": [
{ "order_no": "ORD-001", "order_status": "cancelled" },
{ "order_no": "ORD-002", "order_status": "customer_cancelled" }
]
}Submit Orders
Submit one or more service orders for asynchronous processing.
POST
/api/v1/orders/ingestRequest Body (Simplified)
{
"orders": [
{
"company_id": "uuid",
"service_group_id": "uuid",
"order_no": "ORD-123",
"order_date": "2025-01-15T10:00:00Z",
"to_location": { ... },
"to_contact": { ... },
"service_tasks": [ ... ]
}
]
}Batch Status
Check the processing status of a submitted batch.
GET
/api/v1/orders/batch/{batchId}/statusRejected Orders
Query orders that failed validation during ingestion.
GET
/api/v1/orders/rejectedAsync Processing Workflow
- 1
Submit Batch
POST /orders/ingest returns a batch_id and "queued" status.
- 2
Poll Status
Poll GET /orders/batch/{id}/status until "completed" or "failed".
- 3
Handle Rejections
If rejected_count > 0, fetch details via GET /orders/rejected.
SDKs & Libraries
TypeScript SDK
client.ts
/**
* Logistics Platform API Client
* TypeScript SDK for order ingestion
*/
// ============================================================================
// Type Definitions (matching Go models)
// ============================================================================
export interface LocationInput {
street_address1: string;
street_address2?: string;
city: string;
state: string;
zip: string;
country_code: string;
latitude?: number;
longitude?: number;
}
export interface ContactInput {
entity_identity?: string;
entity_name?: string;
contact_name?: string;
contact_phone?: string;
contact_email?: string;
unit?: string;
floor?: string;
instructions?: string;
}
export interface ProductInput {
identity: string;
description: string;
is_included?: boolean;
metadata?: Record<string, unknown>;
}
export interface PackageInput {
tracking_number?: string;
description?: string;
weight_kg?: number;
volume_cubic_meters?: number;
products?: ProductInput[];
metadata?: Record<string, unknown>;
}
export interface ServiceTaskInput {
service_type_id: string; // UUID
direction: 'from' | 'to';
action_type: string; // 'DELIVER', 'PICKUP', 'INSTALL', etc.
instructions?: string;
notes?: string;
package_index?: number; // 0-based index into packages array
}
export interface OrderInput {
company_id: string; // UUID
service_group_id: string; // UUID
order_no: string;
order_date: string; // ISO 8601 format
from_location?: LocationInput;
to_location: LocationInput;
from_contact?: ContactInput;
to_contact: ContactInput;
is_depot_based_route?: boolean;
process_automatically?: boolean;
priority?: number; // 1-10
category?: 'single' | 'multiple';
service_tasks: ServiceTaskInput[]; // Required, min 1
packages?: PackageInput[];
metadata?: Record<string, unknown>;
}
export interface IngestRequest {
orders: OrderInput[]; // Required, min 1, max 500
}
export interface IngestResponse {
batch_id: string;
order_count: number;
status: string; // 'queued', 'processing', 'completed', 'failed'
queued_at: string;
}
export interface BatchStatus {
batch_id: string;
status: 'queued' | 'processing' | 'completed' | 'failed';
total_orders: number;
accepted_count: number;
rejected_count: number;
queued_at: string;
started_at?: string;
completed_at?: string;
}
export interface RejectedOrder {
rejected_order_id: string;
batch_id: string;
company_id: string;
original_payload: Record<string, unknown>;
rejection_code: string;
rejection_reason: string;
rejection_details?: Record<string, unknown>;
received_at: string;
processed_at: string;
reprocess_status: 'pending' | 'reprocessed' | 'abandoned';
}
export interface RejectedOrdersQuery {
batch_id?: string;
rejection_code?: string;
limit?: number;
offset?: number;
}
export interface CancelOrderRequest {
reason: string; // Required, 1-500 characters
}
export interface CancelOrderResponse {
order_id: string;
status: string;
was_assigned: boolean;
route_id?: string;
route_modified: boolean;
}
export interface BulkCancelOrderInput {
order_no: string;
order_status: 'cancelled' | 'customer_cancelled';
}
export interface BulkCancelRequest {
orders: BulkCancelOrderInput[];
}
export interface BulkCancelResult {
order_no: string;
order_id: string;
status: string;
success: boolean;
was_assigned: boolean;
route_id?: string;
route_modified: boolean;
}
export interface BulkCancelFailure {
order_no: string;
error_code: string;
error_message: string;
}
export interface BulkCancelResponse {
total_count: number;
cancelled_count: number;
failed_count: number;
results: BulkCancelResult[];
failures: BulkCancelFailure[];
}
export interface APIError {
code: string;
msg: string;
details?: Record<string, unknown>;
}
export interface APIResponse<T> {
success: boolean;
payload?: T[];
error?: APIError;
meta?: Record<string, unknown>;
}
// ============================================================================
// Custom Error Class
// ============================================================================
export class LogisticsAPIError extends Error {
code: string;
details?: Record<string, unknown>;
constructor(code: string, message: string, details?: Record<string, unknown>) {
super(message);
this.name = 'LogisticsAPIError';
this.code = code;
this.details = details;
}
}
// ============================================================================
// Client Class
// ============================================================================
export class LogisticsClient {
private baseURL: string;
private token: string;
constructor(baseURL: string, token: string) {
this.baseURL = baseURL.replace(//$/, '');
this.token = token;
}
/** Update JWT token (for token refresh) */
setToken(token: string): void {
this.token = token;
}
/** Submit orders for ingestion (max 500 per request) */
async ingestOrders(orders: OrderInput[]): Promise<IngestResponse> {
if (orders.length > 500) {
throw new LogisticsAPIError(
'VALIDATION_ERROR',
`Max 500 orders per request, got ${orders.length}`
);
}
const response = await fetch(`${this.baseURL}/api/v1/orders/ingest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.token}`,
},
body: JSON.stringify({ orders } as IngestRequest),
});
const data: APIResponse<IngestResponse> = await response.json();
if (!data.success) {
throw new LogisticsAPIError(
data.error?.code ?? 'UNKNOWN_ERROR',
data.error?.msg ?? 'An unknown error occurred',
data.error?.details
);
}
if (!data.payload || data.payload.length === 0) {
throw new LogisticsAPIError('EMPTY_RESPONSE', 'Empty payload in response');
}
return data.payload[0];
}
/** Submit a single order (convenience method) */
async ingestOrder(order: OrderInput): Promise<IngestResponse> {
return this.ingestOrders([order]);
}
/** Get batch processing status */
async getBatchStatus(batchId: string): Promise<BatchStatus> {
const response = await fetch(
`${this.baseURL}/api/v1/orders/batch/${batchId}/status`,
{
method: 'GET',
headers: { Authorization: `Bearer ${this.token}` },
}
);
const data: APIResponse<BatchStatus> = await response.json();
if (!data.success) {
throw new LogisticsAPIError(
data.error?.code ?? 'UNKNOWN_ERROR',
data.error?.msg ?? 'Failed to get batch status',
data.error?.details
);
}
if (!data.payload || data.payload.length === 0) {
throw new LogisticsAPIError('NOT_FOUND', 'Batch not found');
}
return data.payload[0];
}
/** Get rejected orders with optional filters */
async getRejectedOrders(query: RejectedOrdersQuery = {}): Promise<RejectedOrder[]> {
const params = new URLSearchParams();
if (query.batch_id) params.set('batch_id', query.batch_id);
if (query.rejection_code) params.set('rejection_code', query.rejection_code);
params.set('limit', String(query.limit ?? 100));
params.set('offset', String(query.offset ?? 0));
const response = await fetch(
`${this.baseURL}/api/v1/orders/rejected?${params.toString()}`,
{
method: 'GET',
headers: { Authorization: `Bearer ${this.token}` },
}
);
const data: APIResponse<RejectedOrder> = await response.json();
if (!data.success) {
throw new LogisticsAPIError(
data.error?.code ?? 'UNKNOWN_ERROR',
data.error?.msg ?? 'Failed to get rejected orders',
data.error?.details
);
}
return data.payload ?? [];
}
/** Wait for batch completion with polling */
async waitForBatchCompletion(
batchId: string,
options: { pollIntervalMs?: number; timeoutMs?: number } = {}
): Promise<BatchStatus> {
const { pollIntervalMs = 5000, timeoutMs = 300000 } = options;
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
const status = await this.getBatchStatus(batchId);
if (status.status === 'completed' || status.status === 'failed') {
return status;
}
await new Promise((r) => setTimeout(r, pollIntervalMs));
}
throw new LogisticsAPIError('TIMEOUT', 'Batch processing timeout');
}
/** Cancel an order by ID */
async cancelOrder(orderId: string, reason: string): Promise<CancelOrderResponse> {
if (!orderId) {
throw new LogisticsAPIError('VALIDATION_ERROR', 'Order ID is required');
}
if (!reason || reason.length < 1 || reason.length > 500) {
throw new LogisticsAPIError('VALIDATION_ERROR', 'Reason is required (1-500 characters)');
}
const response = await fetch(`${this.baseURL}/api/v1/orders/${orderId}/cancel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.token}`,
},
body: JSON.stringify({ reason } as CancelOrderRequest),
});
const data: APIResponse<CancelOrderResponse> = await response.json();
if (!data.success) {
throw new LogisticsAPIError(
data.error?.code ?? 'UNKNOWN_ERROR',
data.error?.msg ?? 'Failed to cancel order',
data.error?.details
);
}
if (!data.payload || data.payload.length === 0) {
throw new LogisticsAPIError('EMPTY_RESPONSE', 'Empty response');
}
return data.payload[0];
}
/** Bulk cancel multiple orders by order_no */
async bulkCancelOrders(orders: BulkCancelOrderInput[]): Promise<BulkCancelResponse> {
if (orders.length === 0) {
throw new LogisticsAPIError('VALIDATION_ERROR', 'At least one order is required');
}
if (orders.length > 100) {
throw new LogisticsAPIError('VALIDATION_ERROR', `Max 100 orders per request, got ${orders.length}`);
}
// Validate inputs
for (let i = 0; i < orders.length; i++) {
const o = orders[i];
if (!o.order_no) {
throw new LogisticsAPIError('VALIDATION_ERROR', `order_no is required at index ${i}`);
}
if (o.order_status !== 'cancelled' && o.order_status !== 'customer_cancelled') {
throw new LogisticsAPIError(
'VALIDATION_ERROR',
`Invalid order_status '${o.order_status}' at index ${i}: must be 'cancelled' or 'customer_cancelled'`
);
}
}
const response = await fetch(`${this.baseURL}/api/v1/orders/cancel/bulk`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.token}`,
},
body: JSON.stringify({ orders } as BulkCancelRequest),
});
const data: APIResponse<BulkCancelResponse> = await response.json();
if (!data.success) {
throw new LogisticsAPIError(
data.error?.code ?? 'UNKNOWN_ERROR',
data.error?.msg ?? 'Failed to bulk cancel orders',
data.error?.details
);
}
if (!data.payload || data.payload.length === 0) {
throw new LogisticsAPIError('EMPTY_RESPONSE', 'Empty response');
}
return data.payload[0];
}
}Go SDK
main.go
package logistics
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Client handles communication with the Logistics Platform API
type Client struct {
baseURL string
token string
httpClient *http.Client
}
// NewClient creates a new API client with JWT token
func NewClient(baseURL, token string) *Client {
return &Client{
baseURL: baseURL,
token: token,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// SetToken updates the JWT token (for token refresh)
func (c *Client) SetToken(token string) {
c.token = token
}
// LocationInput represents location data from external system
type LocationInput struct {
StreetAddress1 string `json:"street_address1"`
StreetAddress2 string `json:"street_address2,omitempty"`
City string `json:"city"`
State string `json:"state"`
Zip string `json:"zip"`
CountryCode string `json:"country_code"`
Latitude float64 `json:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty"`
}
// ContactInput represents contact/recipient data
type ContactInput struct {
EntityIdentity string `json:"entity_identity,omitempty"`
EntityName string `json:"entity_name,omitempty"`
ContactName string `json:"contact_name,omitempty"`
ContactPhone string `json:"contact_phone,omitempty"`
ContactEmail string `json:"contact_email,omitempty"`
Unit string `json:"unit,omitempty"`
Floor string `json:"floor,omitempty"`
Instructions string `json:"instructions,omitempty"`
}
// ProductInput represents a product from external system
type ProductInput struct {
Identity string `json:"identity"`
Description string `json:"description"`
IsIncluded bool `json:"is_included,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// PackageInput represents a package from external system
type PackageInput struct {
TrackingNumber string `json:"tracking_number,omitempty"`
Description string `json:"description,omitempty"`
WeightKg *float64 `json:"weight_kg,omitempty"`
VolumeCubicMeters *float64 `json:"volume_cubic_meters,omitempty"`
Products []ProductInput `json:"products,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// ServiceTaskInput represents a service task from external system
type ServiceTaskInput struct {
ServiceTypeID string `json:"service_type_id"`
Direction string `json:"direction"` // "from" or "to"
ActionType string `json:"action_type"`
Instructions string `json:"instructions,omitempty"`
Notes string `json:"notes,omitempty"`
PackageIndex *int `json:"package_index,omitempty"`
}
// OrderInput represents a single order from external system
type OrderInput struct {
CompanyID string `json:"company_id"`
ServiceGroupID string `json:"service_group_id"`
OrderNo string `json:"order_no"`
OrderDate string `json:"order_date"`
FromLocation *LocationInput `json:"from_location,omitempty"`
ToLocation *LocationInput `json:"to_location"`
FromContact *ContactInput `json:"from_contact,omitempty"`
ToContact *ContactInput `json:"to_contact"`
IsDepotBasedRoute bool `json:"is_depot_based_route,omitempty"`
ProcessAutomatically bool `json:"process_automatically,omitempty"`
Priority int `json:"priority,omitempty"`
Category string `json:"category,omitempty"`
ServiceTasks []ServiceTaskInput `json:"service_tasks"`
Packages []PackageInput `json:"packages,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// IngestRequest represents the incoming order ingestion request
type IngestRequest struct {
Orders []OrderInput `json:"orders"`
}
// IngestResponse represents the API response for order ingestion
type IngestResponse struct {
BatchID string `json:"batch_id"`
OrderCount int `json:"order_count"`
Status string `json:"status"`
QueuedAt string `json:"queued_at"`
}
// APIError represents an API error response
type APIError struct {
Code string `json:"code"`
Message string `json:"msg"`
Details map[string]interface{} `json:"details,omitempty"`
}
// APIResponse is the standard API response wrapper
type APIResponse struct {
Success bool `json:"success"`
Payload []IngestResponse `json:"payload,omitempty"`
Error *APIError `json:"error,omitempty"`
Meta map[string]interface{} `json:"meta,omitempty"`
}
// BatchStatus represents the status of a processing batch
type BatchStatus struct {
BatchID string `json:"batch_id"`
Status string `json:"status"`
TotalOrders int `json:"total_orders"`
AcceptedCount int `json:"accepted_count"`
RejectedCount int `json:"rejected_count"`
QueuedAt string `json:"queued_at"`
StartedAt string `json:"started_at,omitempty"`
CompletedAt string `json:"completed_at,omitempty"`
}
// RejectedOrder represents a rejected order from ingestion
type RejectedOrder struct {
RejectedOrderID string `json:"rejected_order_id"`
BatchID string `json:"batch_id"`
CompanyID string `json:"company_id"`
OriginalPayload map[string]interface{} `json:"original_payload"`
RejectionCode string `json:"rejection_code"`
RejectionReason string `json:"rejection_reason"`
RejectionDetails map[string]interface{} `json:"rejection_details,omitempty"`
ReceivedAt string `json:"received_at"`
ProcessedAt string `json:"processed_at"`
ReprocessStatus string `json:"reprocess_status"`
}
// RejectedOrdersQuery contains query parameters for rejected orders
type RejectedOrdersQuery struct {
BatchID string
RejectionCode string
Limit int
Offset int
}
// CancelOrderRequest request to cancel an order
type CancelOrderRequest struct {
Reason string `json:"reason"`
}
// CancelOrderResponse response from cancelling an order
type CancelOrderResponse struct {
OrderID string `json:"order_id"`
Status string `json:"status"`
WasAssigned bool `json:"was_assigned"`
RouteID *string `json:"route_id,omitempty"`
RouteModified bool `json:"route_modified"`
}
// BulkCancelOrderInput represents a single order to cancel in bulk
type BulkCancelOrderInput struct {
OrderNo string `json:"order_no"`
OrderStatus string `json:"order_status"` // "cancelled" or "customer_cancelled"
}
// BulkCancelRequest request for bulk order cancellation
type BulkCancelRequest struct {
Orders []BulkCancelOrderInput `json:"orders"`
}
// BulkCancelResult represents the result for a single cancelled order
type BulkCancelResult struct {
OrderNo string `json:"order_no"`
OrderID string `json:"order_id"`
Status string `json:"status"`
Success bool `json:"success"`
WasAssigned bool `json:"was_assigned"`
RouteID *string `json:"route_id,omitempty"`
RouteModified bool `json:"route_modified"`
}
// BulkCancelFailure represents a failed cancellation
type BulkCancelFailure struct {
OrderNo string `json:"order_no"`
ErrorCode string `json:"error_code"`
ErrorMessage string `json:"error_message"`
}
// BulkCancelResponse response from bulk cancellation
type BulkCancelResponse struct {
TotalCount int `json:"total_count"`
CancelledCount int `json:"cancelled_count"`
FailedCount int `json:"failed_count"`
Results []BulkCancelResult `json:"results"`
Failures []BulkCancelFailure `json:"failures"`
}
// IngestOrders submits orders to the logistics platform
func (c *Client) IngestOrders(ctx context.Context, orders []OrderInput) (*IngestResponse, error) {
if len(orders) > 500 {
return nil, fmt.Errorf("max 500 orders per request, got %d", len(orders))
}
req := IngestRequest{Orders: orders}
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/api/v1/orders/ingest", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+c.token)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
var apiResp APIResponse
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
if !apiResp.Success {
if apiResp.Error != nil {
return nil, fmt.Errorf("API error [%s]: %s", apiResp.Error.Code, apiResp.Error.Message)
}
return nil, fmt.Errorf("API error: unknown")
}
if len(apiResp.Payload) == 0 {
return nil, fmt.Errorf("empty payload in response")
}
return &apiResp.Payload[0], nil
}
// GetBatchStatus retrieves the processing status of a batch
func (c *Client) GetBatchStatus(ctx context.Context, batchID string) (*BatchStatus, error) {
url := fmt.Sprintf("%s/api/v1/orders/batch/%s/status", c.baseURL, batchID)
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.token)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
var apiResp struct {
Success bool `json:"success"`
Payload []BatchStatus `json:"payload,omitempty"`
Error *APIError `json:"error,omitempty"`
}
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
if !apiResp.Success {
if apiResp.Error != nil {
return nil, fmt.Errorf("API error [%s]: %s", apiResp.Error.Code, apiResp.Error.Message)
}
return nil, fmt.Errorf("API error: unknown")
}
if len(apiResp.Payload) == 0 {
return nil, fmt.Errorf("batch not found")
}
return &apiResp.Payload[0], nil
}
// GetRejectedOrders retrieves rejected orders with optional filters
func (c *Client) GetRejectedOrders(ctx context.Context, query RejectedOrdersQuery) ([]RejectedOrder, error) {
url := fmt.Sprintf("%s/api/v1/orders/rejected?limit=%d&offset=%d", c.baseURL, query.Limit, query.Offset)
if query.BatchID != "" {
url += "&batch_id=" + query.BatchID
}
if query.RejectionCode != "" {
url += "&rejection_code=" + query.RejectionCode
}
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.token)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
var apiResp struct {
Success bool `json:"success"`
Payload []RejectedOrder `json:"payload,omitempty"`
Error *APIError `json:"error,omitempty"`
}
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
if !apiResp.Success {
if apiResp.Error != nil {
return nil, fmt.Errorf("API error [%s]: %s", apiResp.Error.Code, apiResp.Error.Message)
}
return nil, fmt.Errorf("API error: unknown")
}
return apiResp.Payload, nil
}
// CancelOrder cancels an order by ID with a reason
func (c *Client) CancelOrder(ctx context.Context, orderID string, reason string) (*CancelOrderResponse, error) {
if orderID == "" {
return nil, fmt.Errorf("order ID is required")
}
if reason == "" {
return nil, fmt.Errorf("cancellation reason is required")
}
req := CancelOrderRequest{Reason: reason}
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
url := fmt.Sprintf("%s/api/v1/orders/%s/cancel", c.baseURL, orderID)
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+c.token)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
var apiResp struct {
Success bool `json:"success"`
Payload []CancelOrderResponse `json:"payload,omitempty"`
Error *APIError `json:"error,omitempty"`
}
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
if !apiResp.Success {
if apiResp.Error != nil {
return nil, fmt.Errorf("API error [%s]: %s", apiResp.Error.Code, apiResp.Error.Message)
}
return nil, fmt.Errorf("API error: unknown")
}
if len(apiResp.Payload) == 0 {
return nil, fmt.Errorf("empty response")
}
return &apiResp.Payload[0], nil
}
// BulkCancelOrders cancels multiple orders by order_no with specified status
func (c *Client) BulkCancelOrders(ctx context.Context, orders []BulkCancelOrderInput) (*BulkCancelResponse, error) {
if len(orders) == 0 {
return nil, fmt.Errorf("at least one order is required")
}
if len(orders) > 100 {
return nil, fmt.Errorf("max 100 orders per bulk cancel request, got %d", len(orders))
}
// Validate order statuses
for i, o := range orders {
if o.OrderNo == "" {
return nil, fmt.Errorf("order_no is required at index %d", i)
}
if o.OrderStatus != "cancelled" && o.OrderStatus != "customer_cancelled" {
return nil, fmt.Errorf("invalid order_status '%s' at index %d: must be 'cancelled' or 'customer_cancelled'", o.OrderStatus, i)
}
}
req := BulkCancelRequest{Orders: orders}
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
url := fmt.Sprintf("%s/api/v1/orders/cancel/bulk", c.baseURL)
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+c.token)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
var apiResp struct {
Success bool `json:"success"`
Payload []BulkCancelResponse `json:"payload,omitempty"`
Error *APIError `json:"error,omitempty"`
}
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
if !apiResp.Success {
if apiResp.Error != nil {
return nil, fmt.Errorf("API error [%s]: %s", apiResp.Error.Code, apiResp.Error.Message)
}
return nil, fmt.Errorf("API error: unknown")
}
if len(apiResp.Payload) == 0 {
return nil, fmt.Errorf("empty response")
}
return &apiResp.Payload[0], nil
}
// WaitForBatchCompletion polls batch status until completed or timeout
func (c *Client) WaitForBatchCompletion(ctx context.Context, batchID string, pollInterval time.Duration) (*BatchStatus, error) {
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
status, err := c.GetBatchStatus(ctx, batchID)
if err != nil {
return nil, err
}
switch status.Status {
case "completed", "failed":
return status, nil
case "queued", "processing":
time.Sleep(pollInterval)
default:
return nil, fmt.Errorf("unknown batch status: %s", status.Status)
}
}
}