Connect Bolt.new to Shippo with a simple API token — no OAuth required. Shippo's REST API provides multi-carrier shipping rate comparison, label creation, and shipment tracking across 70+ carriers including USPS, UPS, FedEx, and DHL in a single API call. Store SHIPPO_API_TOKEN in your .env file and call Shippo's API from a Next.js API route to keep the token secure. Rate comparison and label creation work in Bolt's WebContainer preview.
Build Multi-Carrier Shipping Rate Comparison and Label Creation with Bolt.new
Shippo's biggest advantage is the unified API: one API call returns rates from all the carriers you have enabled — USPS, UPS, FedEx, DHL, and dozens of regional carriers — without having to manage separate integrations for each. For e-commerce apps built in Bolt.new, this means you can show customers real-time shipping cost comparisons before checkout, automatically select the cheapest option, or let customers choose based on speed versus cost. The rate comparison feature alone — getting live rates from multiple carriers in a single request — saves weeks of work compared to integrating each carrier API individually.
Shippo's API is unusually developer-friendly: authentication is a single Bearer token (no OAuth, no key rotation dance), the request format is straightforward (sender address, recipient address, parcel dimensions and weight), and the response is a clean JSON array of rate objects sorted by price. Each rate includes the carrier name, service level, estimated delivery days, price, and a rate_object_id that you use to purchase the label. The entire flow from 'here is a package going from address A to address B' to 'here is a downloadable label PDF' takes about 3 API calls.
For Bolt.new development, Shippo's API calls are all outbound HTTP — compatible with Bolt's WebContainer. You can build and test the rate comparison UI, simulate label purchases (Shippo has a test token that generates dummy labels), and display tracking information all within the Bolt preview. The only WebContainer limitation applies to Shippo's tracking webhooks, which push status updates to your app when a package moves — those webhooks need a deployed URL. For development, you can poll Shippo's tracking API directly instead of waiting for webhooks.
Integration method
Shippo uses simple API token authentication — include your Shippo API token as a Bearer token in the Authorization header on all requests. No OAuth flow, no webhooks required for basic rate comparison and label creation. Because the API token is a secret, all Shippo calls happen in a Next.js API route in your Bolt.new app. The frontend POSTs shipment details to your API route, which calls Shippo and returns rates or labels. Shippo's tracking webhooks require a deployed URL, but tracking status can also be polled directly from the API during development.
Prerequisites
- A Shippo account at goshippo.com (free to create — pay only for labels purchased)
- Your Shippo API token from the goshippo.com dashboard under API → API Keys (use the test token for development)
- A Bolt.new project using Next.js for server-side API routes that keep the Shippo token secure
- Basic understanding of shipping concepts: sender/recipient addresses, parcel dimensions and weight, carrier service levels
- Optional: a Supabase database for storing orders with shipping information
Step-by-step guide
Get Your Shippo API Token and Set Up the Project
Get Your Shippo API Token and Set Up the Project
Go to goshippo.com and create a free account. After signing in, navigate to the API section in your dashboard — usually found under Settings → API Keys or directly at app.goshippo.com/api. Shippo provides two tokens: a test token (prefixed with 'shippo_test_') and a live token (prefixed with 'shippo_live_'). Use the test token during development — it generates real rate quotes from carriers but creates dummy labels rather than actual billable shipments. This means you can build and test the entire integration without incurring any label costs. The test token's rate quotes are real carrier rates, so your pricing UI will look exactly like production. Switch to the live token only when you are ready to create real, purchasable labels. Add SHIPPO_API_TOKEN to your .env file using the test token. Also add VITE_SHIPPO_API_TOKEN if you need to reference it in client-side code for display purposes (such as showing the API environment in a dev indicator) — but never use this for actual API calls. Create a lib/shippo.ts helper that wraps fetch calls to Shippo's API with the Authorization header, base URL, and error handling. Shippo's base URL is https://api.goshippo.com and all endpoints are lowercase REST paths. The API version is specified via the Shippo-API-Version header (use '2018-02-08' for the stable version). Shippo's error responses include an array-style error message — handle these in your wrapper function.
Set up Shippo API in my Next.js Bolt.new app. Create a .env file with SHIPPO_API_TOKEN placeholder. Create lib/shippo.ts with a shippoRequest(endpoint, options) async helper that calls https://api.goshippo.com{endpoint} with the Authorization: ShippoToken {token} header and Shippo-API-Version: 2018-02-08 header. If the response status is not ok, throw an error with the response body. Export this helper. Create a simple /api/shipping/ping/route.ts that calls GET /addresses to verify the connection and returns success: true.
Paste this in Bolt.new chat
1// lib/shippo.ts2export async function shippoRequest<T>(3 endpoint: string,4 options: RequestInit = {}5): Promise<T> {6 const token = process.env.SHIPPO_API_TOKEN;78 const response = await fetch(`https://api.goshippo.com${endpoint}`, {9 ...options,10 headers: {11 'Authorization': `ShippoToken ${token}`,12 'Shippo-API-Version': '2018-02-08',13 'Content-Type': 'application/json',14 ...(options.headers || {}),15 },16 });1718 if (!response.ok) {19 const errorText = await response.text();20 throw new Error(`Shippo API error ${response.status}: ${errorText}`);21 }2223 return response.json() as Promise<T>;24}Pro tip: Shippo's authorization header uses 'ShippoToken' (not 'Bearer') as the scheme. The exact format is: Authorization: ShippoToken YOUR_TOKEN. Using 'Bearer' instead will return 401 Unauthorized, which is a common gotcha when developers copy authorization patterns from other APIs.
Expected result: The Shippo API connection is verified. The lib/shippo.ts helper is ready to use in API routes throughout the app.
Build the Rate Comparison API Route
Build the Rate Comparison API Route
Shippo's rate comparison requires creating a Shipment object — the core resource in Shippo's API. A Shipment object contains the address_from (sender), address_to (recipient), parcels (dimensions and weight), and optionally carrier_accounts to restrict which carriers return rates. When you create a Shipment, Shippo automatically contacts all enabled carriers and returns an array of Rate objects. Each Rate includes: provider (e.g., 'USPS'), servicelevel.name (e.g., 'Priority Mail'), amount (price as a string), currency, estimated_days, and object_id (the rate ID you use to purchase a label). The shipment creation endpoint is POST /shipments. Addresses can be inline objects in the shipment creation request, or you can pre-create Address objects and reference them by ID for reuse. For the rate calculator, use inline addresses for simplicity. Parcel dimensions use the Parcel object: length, width, height (in inches by default), and weight (in ounces or pounds depending on your setting). Shippo returns asynchronously for some carriers — if the status field of the Shipment is 'QUEUED' rather than 'SUCCESS', poll the GET /shipments/:object_id endpoint until status becomes 'SUCCESS' and rates are populated. In practice, most modern carrier connections return synchronously. During development in Bolt's WebContainer, all outbound API calls to Shippo work correctly. You can test rate comparison with real addresses and real carrier rates without leaving the Bolt preview.
Create a Next.js API route at app/api/shipping/rates/route.ts. It accepts POST with body: { recipientName, addressLine1, city, state, zip, country, weightLbs, lengthIn, widthIn, heightIn }. It calls Shippo's API to create a shipment with a hardcoded sender address (my address — I'll fill in the values) and the provided recipient address and parcel. Return the array of rates from shipment.rates_list, formatted as: { objectId, provider, serviceName, amount, currency, estimatedDays }. Sort rates by amount ascending.
Paste this in Bolt.new chat
1// app/api/shipping/rates/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { shippoRequest } from '@/lib/shippo';45interface ShippoRate {6 object_id: string;7 provider: string;8 servicelevel: { name: string };9 amount: string;10 currency: string;11 estimated_days: number;12}1314interface ShippoShipment {15 object_id: string;16 status: string;17 rates: ShippoRate[];18}1920export async function POST(request: NextRequest) {21 const { recipientName, addressLine1, city, state, zip, country, weightLbs, lengthIn, widthIn, heightIn } =22 await request.json();2324 const shipment = await shippoRequest<ShippoShipment>('/shipments', {25 method: 'POST',26 body: JSON.stringify({27 address_from: {28 name: 'Your Business Name',29 street1: '123 Main St', // Replace with your sender address30 city: 'San Francisco',31 state: 'CA',32 zip: '94105',33 country: 'US',34 },35 address_to: {36 name: recipientName,37 street1: addressLine1,38 city,39 state,40 zip,41 country: country || 'US',42 },43 parcels: [44 {45 length: String(lengthIn),46 width: String(widthIn),47 height: String(heightIn),48 distance_unit: 'in',49 weight: String(weightLbs),50 mass_unit: 'lb',51 },52 ],53 async: false, // Wait for all rates synchronously54 }),55 });5657 const rates = (shipment.rates || []).map((rate) => ({58 objectId: rate.object_id,59 provider: rate.provider,60 serviceName: rate.servicelevel.name,61 amount: parseFloat(rate.amount),62 currency: rate.currency,63 estimatedDays: rate.estimated_days,64 }));6566 // Sort by price ascending67 rates.sort((a, b) => a.amount - b.amount);6869 return NextResponse.json({ rates, shipmentId: shipment.object_id });70}Pro tip: Pass async: false in the Shippo shipment creation request to get all rates back synchronously in the same response. If you omit this or set async: true, you will receive the shipment with status 'QUEUED' and need to poll the API for rates — async: false is simpler for most applications.
Expected result: POSTing to /api/shipping/rates returns an array of carrier rates sorted by price. USPS, UPS, FedEx, and other enabled carriers are compared in a single API call.
Purchase a Label and Handle Tracking
Purchase a Label and Handle Tracking
Once a rate is selected (either by the customer or automatically), purchase the label by POSTing the rate's object_id to Shippo's /transactions endpoint. A Transaction in Shippo represents a purchased label. The response includes the label_url (a direct URL to download the label as PDF or PNG), tracking_number, tracking_url_provider (the carrier's tracking page), and status. Label status transitions are: QUEUED → WAITING → SUCCESS (or ERROR if something went wrong). For most carriers, the label is generated synchronously when async: false is passed. Store the tracking_number and label_url in your Supabase order record for future reference. For tracking status updates after deployment, Shippo supports webhooks that POST to your app when a package's tracking status changes. Register your webhook URL in the Shippo dashboard under API → Webhooks. During development in Bolt's WebContainer, incoming tracking webhooks cannot reach the browser-based runtime because the WebContainer has no public URL — this is a fundamental limitation of Bolt's development environment. Use the tracking poll API (GET /tracks/:carrier/:tracking_number) during development, and set up proper webhooks after deploying to Netlify or Vercel. When you do deploy, webhooks will fire to your production URL when packages are scanned at carrier facilities, updating order status in real time.
Create a Next.js API route at app/api/shipping/label/route.ts. It accepts POST with body: { rateObjectId, orderId }. It calls Shippo POST /transactions with the rate_object_id and async: false. On success, it updates the Supabase order record (using orderId) to set tracking_number, label_url, and status to 'label_created'. Return the transaction result including label_url and tracking_number. Also create app/api/shipping/track/route.ts that accepts GET with query params carrier and tracking, calls Shippo GET /tracks/{carrier}/{tracking}, and returns the tracking status and events array.
Paste this in Bolt.new chat
1// app/api/shipping/label/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { shippoRequest } from '@/lib/shippo';4import { createClient } from '@supabase/supabase-js';56interface ShippoTransaction {7 status: string;8 tracking_number: string;9 label_url: string;10 tracking_url_provider: string;11 messages: Array<{ text: string }>;12}1314export async function POST(request: NextRequest) {15 const { rateObjectId, orderId } = await request.json();1617 const transaction = await shippoRequest<ShippoTransaction>('/transactions', {18 method: 'POST',19 body: JSON.stringify({20 rate: rateObjectId,21 label_file_type: 'PDF',22 async: false,23 }),24 });2526 if (transaction.status !== 'SUCCESS') {27 const messages = transaction.messages?.map((m) => m.text).join('; ');28 return NextResponse.json({ error: `Label creation failed: ${messages}` }, { status: 400 });29 }3031 // Update Supabase order with tracking info32 if (orderId) {33 const supabase = createClient(34 process.env.NEXT_PUBLIC_SUPABASE_URL!,35 process.env.SUPABASE_SERVICE_ROLE_KEY!36 );37 await supabase38 .from('orders')39 .update({40 tracking_number: transaction.tracking_number,41 label_url: transaction.label_url,42 carrier_tracking_url: transaction.tracking_url_provider,43 status: 'label_created',44 })45 .eq('id', orderId);46 }4748 return NextResponse.json({49 trackingNumber: transaction.tracking_number,50 labelUrl: transaction.label_url,51 trackingUrl: transaction.tracking_url_provider,52 });53}Pro tip: Shippo's test token creates test labels with real tracking numbers — but the tracking numbers will not show real scan events since no physical label is printed. When you switch to the live token, labels are purchased and charged to your account (pay-as-you-go). Keep test and live tokens clearly separated in your environment variables.
Expected result: Clicking 'Purchase Label' creates a real shipping label in Shippo, returns a downloadable PDF URL, and stores the tracking number in the Supabase order record.
Deploy to Netlify and Register Tracking Webhooks
Deploy to Netlify and Register Tracking Webhooks
Your Bolt.new app's shipping rate comparison and label creation features work completely in the WebContainer preview — Shippo's API calls are all outbound HTTP, which Bolt's WebContainer handles without issues. The one feature that requires deployment first is Shippo's tracking webhooks. Webhooks are HTTP POST requests that Shippo sends to your app when a package's tracking status changes — for example, when USPS scans the package at a sorting facility. These incoming webhooks cannot reach Bolt's WebContainer during development because the browser-based runtime has no public URL accessible from the internet. To set up tracking webhooks: first deploy your app to Netlify via Bolt's Settings → Applications. After deployment, copy your Netlify URL. In the Shippo dashboard (app.goshippo.com), go to API → Webhooks and click 'Add a Webhook'. Enter your webhook URL (e.g., https://your-app.netlify.app/api/shipping/webhook), select the event type 'track_updated', and save. Create the webhook handler route in Bolt before deploying. The webhook payload from Shippo includes the tracking number, carrier, status, substatus, and an events array. Use Supabase's service role key (not the anon key) in your webhook handler to update order records without requiring user authentication. After deployment, set SHIPPO_API_TOKEN and SUPABASE_SERVICE_ROLE_KEY as environment variables in Netlify's Site Configuration → Environment Variables and redeploy.
Create a webhook handler at app/api/shipping/webhook/route.ts for Shippo tracking updates. It accepts POST requests from Shippo. Extract the tracking_number and status from the payload. Query Supabase to find the order with that tracking_number. Update the order's shipping_status field. For terminal statuses (DELIVERED, RETURNED, FAILURE), also send a confirmation email to the customer using the Resend API. Add the webhook URL to .env.example as SHIPPO_WEBHOOK_URL placeholder.
Paste this in Bolt.new chat
1// app/api/shipping/webhook/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { createClient } from '@supabase/supabase-js';45const supabase = createClient(6 process.env.NEXT_PUBLIC_SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8);910export async function POST(request: NextRequest) {11 const payload = await request.json();1213 // Shippo tracking webhook payload structure14 const trackingNumber = payload.data?.tracking_number;15 const carrier = payload.data?.carrier;16 const status = payload.data?.tracking_status?.status; // PRE_TRANSIT, TRANSIT, DELIVERED, RETURNED, FAILURE, UNKNOWN17 const statusDetails = payload.data?.tracking_status?.status_details;18 const estimatedDelivery = payload.data?.eta;1920 if (!trackingNumber) {21 return NextResponse.json({ received: true });22 }2324 // Update the order in Supabase25 await supabase26 .from('orders')27 .update({28 shipping_status: status,29 shipping_status_details: statusDetails,30 estimated_delivery: estimatedDelivery,31 last_tracking_update: new Date().toISOString(),32 })33 .eq('tracking_number', trackingNumber);3435 return NextResponse.json({ received: true });36}Pro tip: Shippo webhook payloads are not signed by default — Shippo does not include a signature header for verifying webhook authenticity (unlike Stripe or Twilio). Consider adding a secret query parameter to your webhook URL (e.g., /api/shipping/webhook?secret=your_secret) and validating it in the handler to prevent unauthorized calls.
Expected result: After deployment, Shippo sends tracking status updates to your webhook endpoint whenever a package moves through the carrier network. Order statuses update automatically without manual polling.
Common use cases
Shipping Rate Calculator at Checkout
Add a shipping rate calculator to an e-commerce checkout flow. The customer enters their delivery address, your app calls Shippo to get live rates from multiple carriers, and the customer selects their preferred option before completing the order. Display carrier name, service level (ground, express), estimated delivery days, and price.
Build a shipping rate calculator component for my checkout page. It has fields for: recipient name, address line 1, city, state, zip code, and country. When the customer clicks 'Calculate Shipping', POST to /api/shipping/rates with the recipient address and a fixed parcel size (10x8x4 inches, 2 lbs). Display the returned rates as a list of selectable options showing carrier logo placeholder, service name (e.g., 'USPS Priority Mail'), estimated delivery (e.g., '2-3 days'), and price. The customer selects a rate and it is stored in the order state.
Copy this prompt to try it in Bolt.new
Automated Label Generation for Orders
Automatically generate shipping labels when an order is marked as ready to ship. Pull the shipping address from the order record, create a shipment in Shippo, select the cheapest available carrier rate, purchase the label, and store the label URL and tracking number in the order record.
Build a 'Generate Label' flow for my order management page. When an admin clicks 'Generate Label' on an order, POST to /api/shipping/label with the order ID. The API route fetches the order from Supabase (customer name, shipping address, order weight), creates a Shippo shipment, gets rates, selects the cheapest USPS rate, purchases the label, and updates the Supabase order record with tracking_number and label_url. Return the label URL so the admin can click to download and print the label.
Copy this prompt to try it in Bolt.new
Shipment Tracking Dashboard
Build an internal shipment tracking page that shows the current status of all active shipments. Poll Shippo's tracking API for each tracking number and display a live status feed with carrier, current location, status (in transit, out for delivery, delivered), and estimated delivery date.
Build a shipment tracking dashboard. Fetch all orders from Supabase where status is 'shipped' and tracking_number is not null. For each order, call /api/shipping/track?carrier={carrier}&tracking={tracking_number} which calls Shippo's tracking API. Display a table with: order ID, customer name, carrier, tracking number, current status, last update location, and estimated delivery. Color-code status: green for delivered, blue for in transit, yellow for pending, red for exception.
Copy this prompt to try it in Bolt.new
Troubleshooting
Shippo returns 401 Unauthorized with 'Invalid token' message
Cause: The Authorization header format is wrong — using 'Bearer' instead of the Shippo-specific 'ShippoToken' scheme, or the token itself was copied incorrectly from the dashboard.
Solution: Ensure the Authorization header is exactly 'ShippoToken YOUR_TOKEN_HERE' — not 'Bearer YOUR_TOKEN'. Verify the token matches what is in your Shippo dashboard under API → API Keys. Check for leading or trailing whitespace in the .env file value.
1// Correct Shippo authorization header:2headers: { 'Authorization': `ShippoToken ${process.env.SHIPPO_API_TOKEN}` }34// WRONG — do not use Bearer:5headers: { 'Authorization': `Bearer ${process.env.SHIPPO_API_TOKEN}` }Rate comparison returns an empty rates array for a valid shipment
Cause: No carrier accounts are enabled in your Shippo account, the parcel dimensions or weight are outside carrier limits, or the destination country is not supported by any enabled carriers.
Solution: In your Shippo dashboard, go to Carriers and verify at least one carrier is connected and active. For testing, USPS is enabled by default on Shippo test accounts. Also check that the parcel weight is greater than 0 and dimensions are positive numbers. For international shipments, ensure the country code is a valid 2-letter ISO code.
Label URL returns a 404 error when trying to download the label PDF
Cause: Shippo's test labels have a limited validity period and the URL may have expired, or the transaction status was not 'SUCCESS' when the label URL was stored.
Solution: Always check transaction.status === 'SUCCESS' before using the label_url. For expired test labels, re-purchase the label with the test token to get a fresh URL. Production labels have a longer validity period but should also be stored immediately after creation.
1if (transaction.status !== 'SUCCESS') {2 const errorMessages = transaction.messages?.map(m => m.text).join(', ');3 throw new Error(`Label creation failed: ${errorMessages}`);4}5// Only use label_url after confirming SUCCESS status6const labelUrl = transaction.label_url;Tracking webhooks never fire even though packages are moving through the carrier network
Cause: The webhook URL was registered with the Bolt WebContainer preview URL, which is not publicly accessible. Or the webhook was registered but is pointing to the wrong endpoint path.
Solution: Tracking webhooks require your deployed Netlify or Vercel URL — incoming webhooks cannot reach Bolt's WebContainer during development. In the Shippo dashboard, go to API → Webhooks and update the URL to your deployed domain (e.g., https://your-app.netlify.app/api/shipping/webhook). Test the webhook by clicking 'Send Test' in the Shippo dashboard after updating the URL.
Best practices
- Use Shippo's test token (shippo_test_) during development — it returns real carrier rates and creates dummy labels without any charges
- Pass async: false when creating Shippo Shipment objects to get all carrier rates synchronously in the response, avoiding polling logic
- Always check transaction.status === 'SUCCESS' before using the label_url — failed label creation returns non-SUCCESS status with error messages in the messages array
- Register Shippo tracking webhooks with your deployed Netlify URL, not the Bolt WebContainer preview — incoming webhooks cannot reach the browser-based runtime during development
- Use the Shippo-specific 'ShippoToken' Authorization header scheme, not 'Bearer' — this is the most common authentication error when integrating Shippo
- Store tracking numbers and label URLs in Supabase immediately after label creation — Shippo test label URLs expire and should not be the only copy
- Cache rate quotes for short periods (5-10 minutes) for the same address/parcel combination — rates do not change frequently and caching reduces API calls
Alternatives
ShipStation is a full order management platform with broader marketplace integrations (Amazon, eBay, Shopify) and a more UI-driven workflow — better for managing large order volumes; Shippo is more API-first for custom-built apps.
AfterShip specializes in post-purchase tracking and delivery notifications, offering tracking for 900+ carriers — a better choice if your primary need is tracking visibility rather than label creation.
FedEx's direct API gives deeper FedEx-specific features (freight, international trade documentation) but requires a FedEx account and lacks multi-carrier rate comparison — Shippo is simpler for multi-carrier scenarios.
UPS's direct API provides more UPS-specific options (UPS Access Point, custom brokerage) but requires a UPS developer account and only covers UPS — Shippo provides UPS rates alongside all other carriers in one call.
Frequently asked questions
How do I connect Bolt.new to Shippo?
Get your Shippo API token from app.goshippo.com under API → API Keys. Add it to your .env file as SHIPPO_API_TOKEN. Create a lib/shippo.ts helper that adds the 'Authorization: ShippoToken YOUR_TOKEN' header to fetch calls. All Shippo API calls happen in Next.js API routes — never in client-side code. The rate comparison flow works in Bolt's WebContainer preview during development.
Does Shippo work in Bolt's WebContainer during development?
Yes. Shippo's API calls are all outbound HTTP requests, which Bolt's WebContainer supports. You can test rate comparison, label creation (with the test token), and tracking status polling all within the Bolt preview. The only feature that requires deployment is Shippo's tracking webhooks — incoming webhooks cannot reach the browser-based WebContainer runtime.
What is the difference between Shippo's test token and live token?
The test token (prefixed with 'shippo_test_') generates real carrier rate quotes but creates dummy labels that are not billable and cannot be used for real shipments. The live token (prefixed with 'shippo_live_') creates real, printable labels that are charged to your account (pay-per-label). Use the test token during development and only switch to the live token in production.
How does Shippo's rate comparison work?
Create a Shipment object with the sender address, recipient address, and parcel dimensions/weight using POST /shipments. Set async: false to get all rates synchronously. Shippo contacts all your enabled carrier accounts and returns a rates array in the response. Each rate has the carrier, service level, price, and estimated delivery days. Select a rate and POST its object_id to /transactions to purchase the label.
Can I set up tracking webhooks during development in Bolt?
No — tracking webhooks require a deployed app with a public HTTPS URL. Shippo's webhooks are incoming HTTP requests to your app, and Bolt's WebContainer during development does not have a public URL that Shippo can reach. Deploy your app to Netlify first, then register your deployed URL (e.g., https://your-app.netlify.app/api/shipping/webhook) in the Shippo dashboard under API → Webhooks. For development testing, use Shippo's polling API (GET /tracks/:carrier/:tracking_number) instead.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation