📖 Looking for karrio's legacy docs? Visit docs.karrio.io

API Integration

Insiders
Enterprise

Learn how to integrate your Karrio Apps with Karrio’s powerful APIs to access shipping data, manage orders, and automate workflows.

Overview

Karrio provides comprehensive APIs that allow your apps to:

  • Access shipping data - shipments, rates, tracking, labels
  • Manage carrier connections - configure and test carrier integrations
  • Process orders - create, update, and fulfill orders
  • Handle webhooks - receive real-time notifications
  • Manage customers - addresses, preferences, and profiles
  • Generate reports - analytics, insights, and custom data exports

API Architecture

Karrio offers multiple API interfaces to suit different use cases:

GraphQL API

Setup and Authentication

Your app automatically receives authentication credentials when installed:

1import { createKarrioClient } from "@karrio/app-store"; 2 3export default function MyApp({ app, context }) { 4 // API client is automatically configured with your app's credentials 5 const karrio = context.karrio; 6 7 // Or create a custom client 8 const customClient = createKarrioClient(app.installation.api_key); 9 10 // GraphQL queries and mutations 11 const fetchShipments = async () => { 12 const query = ` 13 query GetShipments($first: Int!) { 14 shipments(first: $first) { 15 edges { 16 node { 17 id 18 tracking_number 19 status 20 recipient { 21 name 22 address 23 } 24 carrier { 25 name 26 service 27 } 28 created_at 29 } 30 } 31 } 32 } 33 `; 34 35 const result = await karrio.graphql(query, { first: 10 }); 36 return result.data.shipments; 37 }; 38 39 return <div>{/* Your app UI */}</div>; 40}

Common GraphQL Operations

Fetching Shipments

1const SHIPMENTS_QUERY = ` 2 query GetShipments( 3 $first: Int 4 $after: String 5 $filters: ShipmentFilter 6 ) { 7 shipments(first: $first, after: $after, filter: $filters) { 8 edges { 9 node { 10 id 11 tracking_number 12 status 13 service 14 recipient { 15 name 16 company 17 address_line1 18 city 19 state_code 20 postal_code 21 country_code 22 } 23 shipper { 24 name 25 company 26 address_line1 27 city 28 state_code 29 postal_code 30 country_code 31 } 32 parcels { 33 weight 34 width 35 height 36 length 37 weight_unit 38 dimension_unit 39 } 40 carrier { 41 name 42 carrier_id 43 } 44 rates { 45 service 46 total_charge 47 currency 48 estimated_delivery 49 } 50 messages { 51 code 52 message 53 level 54 } 55 created_at 56 updated_at 57 } 58 } 59 pageInfo { 60 hasNextPage 61 hasPreviousPage 62 startCursor 63 endCursor 64 } 65 } 66 } 67`; 68 69export function useShipments(filters = {}) { 70 const [shipments, setShipments] = useState([]); 71 const [loading, setLoading] = useState(false); 72 const [error, setError] = useState(null); 73 74 const fetchShipments = async (variables = {}) => { 75 try { 76 setLoading(true); 77 const result = await karrio.graphql(SHIPMENTS_QUERY, { 78 first: 20, 79 filters, 80 ...variables, 81 }); 82 83 setShipments(result.data.shipments.edges.map((edge) => edge.node)); 84 } catch (err) { 85 setError(err.message); 86 } finally { 87 setLoading(false); 88 } 89 }; 90 91 useEffect(() => { 92 fetchShipments(); 93 }, []); 94 95 return { shipments, loading, error, refetch: fetchShipments }; 96}

Updating Shipments

1const UPDATE_SHIPMENT_MUTATION = ` 2 mutation UpdateShipment($input: PartialShipmentMutationInput!) { 3 partial_shipment_update(input: $input) { 4 shipment { 5 id 6 tracking_number 7 status 8 label_url 9 selected_rate { 10 service 11 total_charge 12 currency 13 } 14 } 15 errors { 16 field 17 messages 18 } 19 } 20 } 21`; 22 23export function useUpdateShipment() { 24 const [updating, setUpdating] = useState(false); 25 26 const updateShipment = async (shipmentId: string, updateData) => { 27 try { 28 setUpdating(true); 29 30 const result = await karrio.graphql(UPDATE_SHIPMENT_MUTATION, { 31 input: { 32 id: shipmentId, 33 ...updateData, 34 }, 35 }); 36 37 if (result.data.partial_shipment_update.errors?.length > 0) { 38 throw new Error( 39 result.data.partial_shipment_update.errors[0].messages[0], 40 ); 41 } 42 43 return result.data.partial_shipment_update.shipment; 44 } catch (error) { 45 throw error; 46 } finally { 47 setUpdating(false); 48 } 49 }; 50 51 return { updateShipment, updating }; 52}

Working with Shipments via REST API

Since GraphQL mutations for creating shipments use the REST API under the hood, you can also use the REST endpoints directly:

1export function useShipmentOperations() { 2 const [loading, setLoading] = useState(false); 3 4 const fetchRates = async (rateData) => { 5 try { 6 setLoading(true); 7 8 const response = await karrio.post("/v1/proxy/rates", { 9 recipient: { 10 name: rateData.recipient.name, 11 company: rateData.recipient.company, 12 address_line1: rateData.recipient.address_line1, 13 city: rateData.recipient.city, 14 state_code: rateData.recipient.state_code, 15 postal_code: rateData.recipient.postal_code, 16 country_code: rateData.recipient.country_code, 17 phone_number: rateData.recipient.phone_number, 18 email: rateData.recipient.email, 19 }, 20 parcels: rateData.parcels.map((parcel) => ({ 21 weight: parcel.weight, 22 width: parcel.width, 23 height: parcel.height, 24 length: parcel.length, 25 weight_unit: parcel.weight_unit || "LB", 26 dimension_unit: parcel.dimension_unit || "IN", 27 })), 28 services: rateData.services, // Optional: specific services 29 options: rateData.options || {}, 30 reference: rateData.reference, 31 }); 32 33 return response.rates; 34 } catch (error) { 35 throw error; 36 } finally { 37 setLoading(false); 38 } 39 }; 40 41 const createShipment = async (shipmentData) => { 42 try { 43 setLoading(true); 44 45 const response = await karrio.post("/v1/shipments", { 46 recipient: shipmentData.recipient, 47 parcels: shipmentData.parcels, 48 service: shipmentData.service, 49 options: shipmentData.options || {}, 50 reference: shipmentData.reference, 51 selected_rate_id: shipmentData.selected_rate_id, 52 }); 53 54 return response; 55 } catch (error) { 56 throw error; 57 } finally { 58 setLoading(false); 59 } 60 }; 61 62 return { fetchRates, createShipment, loading }; 63}

Managing Carrier Connections

1const USER_CONNECTIONS_QUERY = ` 2 query GetUserConnections($filter: CarrierFilter) { 3 user_connections(filter: $filter) { 4 id 5 carrier_id 6 carrier_name 7 display_name 8 active 9 test_mode 10 capabilities 11 services { 12 service_name 13 service_code 14 } 15 created_at 16 } 17 } 18`; 19 20const CREATE_CARRIER_MUTATION = ` 21 mutation CreateCarrier($input: CreateCarrierConnectionMutationInput!) { 22 create_carrier_connection(input: $input) { 23 carrier { 24 id 25 carrier_id 26 display_name 27 active 28 } 29 errors { 30 field 31 messages 32 } 33 } 34 } 35`; 36 37export function useCarrierConnections() { 38 const [carriers, setCarriers] = useState([]); 39 const [loading, setLoading] = useState(false); 40 41 const fetchCarriers = async () => { 42 try { 43 setLoading(true); 44 const result = await karrio.graphql(USER_CONNECTIONS_QUERY, { 45 filter: { active: true }, 46 }); 47 setCarriers(result.data.user_connections); 48 } catch (error) { 49 console.error("Failed to fetch carriers:", error); 50 } finally { 51 setLoading(false); 52 } 53 }; 54 55 const createCarrier = async (carrierData) => { 56 const result = await karrio.graphql(CREATE_CARRIER_MUTATION, { 57 input: { 58 carrier_id: carrierData.carrier_id, 59 display_name: carrierData.display_name, 60 test_mode: carrierData.test_mode || false, 61 active: carrierData.active || true, 62 settings: carrierData.settings, 63 }, 64 }); 65 66 if (result.data.create_carrier_connection.errors?.length > 0) { 67 throw new Error( 68 result.data.create_carrier_connection.errors[0].messages[0], 69 ); 70 } 71 72 return result.data.create_carrier_connection.carrier; 73 }; 74 75 useEffect(() => { 76 fetchCarriers(); 77 }, []); 78 79 return { carriers, loading, fetchCarriers, createCarrier }; 80}

REST API Integration

Using the REST Client

1import { createKarrioClient } from "@karrio/app-store"; 2 3export default function MyApp({ app, context }) { 4 const karrio = context.karrio; 5 6 // REST API calls 7 const fetchShipments = async () => { 8 try { 9 const response = await karrio.get("/v1/shipments", { 10 limit: 20, 11 offset: 0, 12 }); 13 14 return response.results; 15 } catch (error) { 16 console.error("Failed to fetch shipments:", error); 17 throw error; 18 } 19 }; 20 21 const createShipment = async (shipmentData) => { 22 try { 23 const response = await karrio.post("/v1/shipments", shipmentData); 24 return response; 25 } catch (error) { 26 console.error("Failed to create shipment:", error); 27 throw error; 28 } 29 }; 30 31 const uploadDocument = async (file, shipmentId) => { 32 try { 33 const formData = new FormData(); 34 formData.append("file", file); 35 formData.append("shipment_id", shipmentId); 36 37 const response = await karrio.post("/v1/documents", formData, { 38 headers: { 39 "Content-Type": "multipart/form-data", 40 }, 41 }); 42 43 return response; 44 } catch (error) { 45 console.error("Failed to upload document:", error); 46 throw error; 47 } 48 }; 49 50 return <div>{/* Your app UI */}</div>; 51}

Common REST Endpoints

Shipments

GET /v1/shipments - List shipments
1const shipments = await karrio.get("/v1/shipments", { 2 limit: 20, 3 offset: 0, 4 status: "in_transit", 5 carrier: "fedex", 6}); 7 8// POST /v1/shipments - Create shipment 9const newShipment = await karrio.post("/v1/shipments", { 10 recipient: { 11 name: "John Doe", 12 address_line1: "123 Main St", 13 city: "New York", 14 state_code: "NY", 15 postal_code: "10001", 16 country_code: "US", 17 }, 18 parcels: [ 19 { 20 weight: 1.5, 21 width: 10, 22 height: 10, 23 length: 10, 24 weight_unit: "LB", 25 dimension_unit: "IN", 26 }, 27 ], 28 service: "fedex_ground", 29}); 30 31// GET /v1/shipments/{id} - Get specific shipment 32const shipment = await karrio.get(`/v1/shipments/${shipmentId}`); 33 34// PUT /v1/shipments/{id} - Update shipment 35const updatedShipment = await karrio.put(`/v1/shipments/${shipmentId}`, { 36 reference: "Updated reference", 37}); 38 39// DELETE /v1/shipments/{id} - Cancel shipment 40await karrio.delete(`/v1/shipments/${shipmentId}`);

Tracking

GET /v1/trackers - List tracking records
1const trackers = await karrio.get("/v1/trackers"); 2 3// POST /v1/trackers - Create tracking record 4const tracker = await karrio.post("/v1/trackers", { 5 tracking_number: "1Z999AA1234567890", 6 carrier_name: "ups", 7}); 8 9// GET /v1/trackers/{id} - Get tracking details 10const trackingDetails = await karrio.get(`/v1/trackers/${trackerId}`);

Rate Shopping

POST /v1/rates - Get shipping rates
1const rates = await karrio.post("/v1/rates", { 2 recipient: { 3 name: "John Doe", 4 address_line1: "123 Main St", 5 city: "New York", 6 state_code: "NY", 7 postal_code: "10001", 8 country_code: "US", 9 }, 10 parcels: [ 11 { 12 weight: 1.5, 13 width: 10, 14 height: 10, 15 length: 10, 16 weight_unit: "LB", 17 dimension_unit: "IN", 18 }, 19 ], 20 services: ["fedex_ground", "ups_ground", "usps_priority"], 21});

Webhooks Integration

Setting Up Webhooks

In your app's API route (e.g., /api/webhooks/karrio)
1import { NextRequest, NextResponse } from "next/server"; 2import { authenticateAppRequest } from "@karrio/app-store"; 3 4export async function POST(request: NextRequest) { 5 try { 6 // Authenticate the webhook request 7 const appContext = await authenticateAppRequest("your-app-id", request); 8 9 const payload = await request.json(); 10 11 // Handle different webhook events 12 switch (payload.event) { 13 case "shipment.created": 14 await handleShipmentCreated(payload.data); 15 break; 16 17 case "shipment.delivered": 18 await handleShipmentDelivered(payload.data); 19 break; 20 21 case "tracking.updated": 22 await handleTrackingUpdated(payload.data); 23 break; 24 25 default: 26 console.log("Unhandled webhook event:", payload.event); 27 } 28 29 return NextResponse.json({ received: true }); 30 } catch (error) { 31 console.error("Webhook error:", error); 32 return NextResponse.json( 33 { error: "Webhook processing failed" }, 34 { status: 500 }, 35 ); 36 } 37} 38 39async function handleShipmentCreated(shipment) { 40 // Send notification to your system 41 await sendNotification({ 42 type: "shipment_created", 43 message: `New shipment created: ${shipment.tracking_number}`, 44 data: shipment, 45 }); 46 47 // Update your database 48 await updateShipmentRecord(shipment); 49} 50 51async function handleShipmentDelivered(shipment) { 52 // Mark as delivered in your system 53 await markShipmentDelivered(shipment.id); 54 55 // Send delivery confirmation 56 await sendDeliveryConfirmation(shipment); 57} 58 59async function handleTrackingUpdated(tracking) { 60 // Update tracking information 61 await updateTrackingInfo(tracking); 62 63 // Notify customers if needed 64 if (tracking.status === "exception") { 65 await notifyCustomerOfException(tracking); 66 } 67}

Webhook Event Types

1interface WebhookEvent { 2 event: string; 3 created_at: string; 4 data: any; 5 test_mode: boolean; 6} 7 8// Available webhook events: 9const WEBHOOK_EVENTS = { 10 // Shipment events 11 "shipment.created": "Shipment created", 12 "shipment.cancelled": "Shipment cancelled", 13 "shipment.delivered": "Shipment delivered", 14 "shipment.failed": "Shipment failed", 15 16 // Tracking events 17 "tracking.updated": "Tracking information updated", 18 "tracking.delivered": "Package delivered", 19 "tracking.exception": "Delivery exception", 20 21 // Order events 22 "order.created": "Order created", 23 "order.fulfilled": "Order fulfilled", 24 "order.cancelled": "Order cancelled", 25 26 // Carrier events 27 "carrier.connected": "Carrier connection established", 28 "carrier.disconnected": "Carrier connection lost", 29 "carrier.rate_updated": "Carrier rates updated", 30};

Real-time Data

Server-Sent Events (SSE)

1import { useEffect, useState } from "react"; 2 3export function useRealtimeShipments(appContext) { 4 const [shipments, setShipments] = useState([]); 5 const [connectionStatus, setConnectionStatus] = useState("connecting"); 6 7 useEffect(() => { 8 const eventSource = new EventSource( 9 `/api/sse/shipments?token=${appContext.apiKey}`, 10 { 11 withCredentials: true, 12 }, 13 ); 14 15 eventSource.onopen = () => { 16 setConnectionStatus("connected"); 17 }; 18 19 eventSource.onmessage = (event) => { 20 const data = JSON.parse(event.data); 21 22 switch (data.type) { 23 case "shipment_created": 24 setShipments((prev) => [data.shipment, ...prev]); 25 break; 26 27 case "shipment_updated": 28 setShipments((prev) => 29 prev.map((s) => (s.id === data.shipment.id ? data.shipment : s)), 30 ); 31 break; 32 33 case "shipment_deleted": 34 setShipments((prev) => prev.filter((s) => s.id !== data.shipment_id)); 35 break; 36 } 37 }; 38 39 eventSource.onerror = () => { 40 setConnectionStatus("error"); 41 }; 42 43 return () => { 44 eventSource.close(); 45 }; 46 }, [appContext.apiKey]); 47 48 return { shipments, connectionStatus }; 49}

WebSocket Integration

1import { useEffect, useState, useRef } from "react"; 2 3export function useWebSocketConnection(appContext) { 4 const [socket, setSocket] = useState(null); 5 const [connectionStatus, setConnectionStatus] = useState("disconnected"); 6 const reconnectTimeoutRef = useRef(null); 7 8 const connect = () => { 9 const ws = new WebSocket( 10 `wss://api.karrio.io/ws?token=${appContext.apiKey}`, 11 ); 12 13 ws.onopen = () => { 14 setConnectionStatus("connected"); 15 setSocket(ws); 16 17 // Subscribe to channels 18 ws.send( 19 JSON.stringify({ 20 type: "subscribe", 21 channels: ["shipments", "tracking", "orders"], 22 }), 23 ); 24 }; 25 26 ws.onmessage = (event) => { 27 const data = JSON.parse(event.data); 28 handleWebSocketMessage(data); 29 }; 30 31 ws.onclose = () => { 32 setConnectionStatus("disconnected"); 33 setSocket(null); 34 35 // Attempt to reconnect after 3 seconds 36 reconnectTimeoutRef.current = setTimeout(() => { 37 connect(); 38 }, 3000); 39 }; 40 41 ws.onerror = (error) => { 42 console.error("WebSocket error:", error); 43 setConnectionStatus("error"); 44 }; 45 }; 46 47 const disconnect = () => { 48 if (socket) { 49 socket.close(); 50 } 51 if (reconnectTimeoutRef.current) { 52 clearTimeout(reconnectTimeoutRef.current); 53 } 54 }; 55 56 const sendMessage = (message) => { 57 if (socket && socket.readyState === WebSocket.OPEN) { 58 socket.send(JSON.stringify(message)); 59 } 60 }; 61 62 useEffect(() => { 63 connect(); 64 return disconnect; 65 }, []); 66 67 return { socket, connectionStatus, sendMessage }; 68} 69 70function handleWebSocketMessage(data) { 71 switch (data.type) { 72 case "shipment_update": 73 // Handle real-time shipment updates 74 break; 75 case "tracking_update": 76 // Handle real-time tracking updates 77 break; 78 default: 79 console.log("Unknown message type:", data.type); 80 } 81}

Error Handling

API Error Management

1import { useState } from "react"; 2 3export class APIError extends Error { 4 constructor(message, code, details = null) { 5 super(message); 6 this.name = "APIError"; 7 this.code = code; 8 this.details = details; 9 } 10} 11 12export function useApiCall() { 13 const [loading, setLoading] = useState(false); 14 const [error, setError] = useState(null); 15 16 const executeCall = async (apiCall) => { 17 try { 18 setLoading(true); 19 setError(null); 20 21 const result = await apiCall(); 22 return result; 23 } catch (err) { 24 const apiError = handleApiError(err); 25 setError(apiError); 26 throw apiError; 27 } finally { 28 setLoading(false); 29 } 30 }; 31 32 return { executeCall, loading, error }; 33} 34 35function handleApiError(error) { 36 if (error.response) { 37 // API returned an error response 38 const { status, data } = error.response; 39 40 switch (status) { 41 case 400: 42 return new APIError( 43 data.message || "Bad request", 44 "BAD_REQUEST", 45 data.errors, 46 ); 47 case 401: 48 return new APIError("Authentication failed", "UNAUTHORIZED"); 49 case 403: 50 return new APIError("Access denied", "FORBIDDEN"); 51 case 404: 52 return new APIError("Resource not found", "NOT_FOUND"); 53 case 429: 54 return new APIError("Rate limit exceeded", "RATE_LIMITED"); 55 case 500: 56 return new APIError("Internal server error", "SERVER_ERROR"); 57 default: 58 return new APIError( 59 `HTTP ${status}: ${data.message || "Unknown error"}`, 60 "HTTP_ERROR", 61 ); 62 } 63 } else if (error.request) { 64 // Network error 65 return new APIError( 66 "Network error - please check your connection", 67 "NETWORK_ERROR", 68 ); 69 } else { 70 // Other error 71 return new APIError( 72 error.message || "Unknown error occurred", 73 "UNKNOWN_ERROR", 74 ); 75 } 76}

Retry Logic

1export function useRetryableApiCall(maxRetries = 3, retryDelay = 1000) { 2 const { executeCall } = useApiCall(); 3 4 const executeWithRetry = async (apiCall) => { 5 let lastError; 6 7 for (let attempt = 1; attempt <= maxRetries; attempt++) { 8 try { 9 return await executeCall(apiCall); 10 } catch (error) { 11 lastError = error; 12 13 // Don't retry certain errors 14 if (error.code === "UNAUTHORIZED" || error.code === "FORBIDDEN") { 15 throw error; 16 } 17 18 // If this was the last attempt, throw the error 19 if (attempt === maxRetries) { 20 throw error; 21 } 22 23 // Wait before retrying 24 await new Promise((resolve) => 25 setTimeout(resolve, retryDelay * attempt), 26 ); 27 } 28 } 29 30 throw lastError; 31 }; 32 33 return { executeWithRetry }; 34}

Testing API Integrations

Mock API Responses

__tests__/api-integration.test.tsx
1import { render, screen, waitFor } from "@testing-library/react"; 2import { rest } from "msw"; 3import { setupServer } from "msw/node"; 4import MyApp from "../MyApp"; 5 6const server = setupServer( 7 rest.get("/v1/shipments", (req, res, ctx) => { 8 return res( 9 ctx.json({ 10 results: [ 11 { 12 id: "1", 13 tracking_number: "TEST123", 14 status: "delivered", 15 recipient: { name: "John Doe" }, 16 }, 17 ], 18 }), 19 ); 20 }), 21 22 rest.post("/v1/shipments", (req, res, ctx) => { 23 return res( 24 ctx.json({ 25 id: "2", 26 tracking_number: "TEST456", 27 status: "created", 28 }), 29 ); 30 }), 31); 32 33beforeAll(() => server.listen()); 34afterEach(() => server.resetHandlers()); 35afterAll(() => server.close()); 36 37test("fetches and displays shipments", async () => { 38 render(<MyApp />); 39 40 await waitFor(() => { 41 expect(screen.getByText("TEST123")).toBeInTheDocument(); 42 expect(screen.getByText("John Doe")).toBeInTheDocument(); 43 }); 44}); 45 46test("creates new shipment", async () => { 47 render(<MyApp />); 48 49 // Simulate creating a shipment 50 // ... test implementation 51});

API Coverage Notes

Some operations in the examples above use REST endpoints because:

  • Shipment Creation: Use POST /v1/shipments REST endpoint
  • Rate Fetching: Use POST /v1/proxy/rates REST endpoint
  • Tracking Data: Use GET /v1/trackers/{tracking_number} REST endpoint

The GraphQL API focuses on data querying and app management, while shipping operations are primarily handled through REST endpoints.

Best Practices

1. Caching Strategy

1import { useQuery } from "@tanstack/react-query"; 2 3export function useShipmentsWithCache() { 4 return useQuery({ 5 queryKey: ["shipments"], 6 queryFn: fetchShipments, 7 staleTime: 5 * 60 * 1000, // 5 minutes 8 cacheTime: 10 * 60 * 1000, // 10 minutes 9 refetchOnWindowFocus: false, 10 }); 11}

2. Request Optimization

Batch multiple requests
1export async function batchApiCalls(calls) { 2 return Promise.allSettled(calls); 3} 4 5// Use pagination for large datasets 6export function usePaginatedShipments(pageSize = 20) { 7 const [page, setPage] = useState(1); 8 9 const query = useQuery({ 10 queryKey: ["shipments", page], 11 queryFn: () => 12 fetchShipments({ 13 limit: pageSize, 14 offset: (page - 1) * pageSize, 15 }), 16 keepPreviousData: true, 17 }); 18 19 return { 20 ...query, 21 page, 22 setPage, 23 hasNextPage: query.data?.count > page * pageSize, 24 }; 25}

3. Rate Limiting

1import { throttle } from "lodash"; 2 3// Throttle API calls to avoid rate limits 4const throttledApiCall = throttle(apiCall, 1000); // Max 1 call per second

Next Steps

Resources


Master Karrio’s APIs to build powerful, data-driven shipping applications!