API Integration
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 shipments1const 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 records1const 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 rates1const 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.tsx1import { 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 requests1export 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
- Examples - See complete integration examples
- Deployment - Deploy your integrated app
- Authentication - Review authentication methods
Resources
- GraphQL Playground - Interactive GraphQL explorer
- API Reference - Complete API documentation
- Postman Collection - Ready-to-use API requests
- Status Page - API uptime and incidents
Master Karrio’s APIs to build powerful, data-driven shipping applications!