# Shiplab > Shiplab is a vendor-neutral parcel data platform that automatically collects invoice, shipment, and charge-level data from parcel and regional carriers. It serves shippers/brands and solution providers who need direct, programmatic access to carrier billing data without portals, scraping, or vendor lock-in. Shiplab is infrastructure, not analytics or spend management. It collects data at the source, stores it in a managed Data Lake, and delivers it via API, raw file store, managed warehouse, Delta Sharing, ODBC/JDBC, or BI tools. Locations: Research Triangle, NC and Silicon Valley, CA. Contact: info@shiplab.com ## Platform - [Homepage](https://www.shiplab.com/): Product overview — automated carrier data collection, API access, four-step pipeline (onboard, collect, store, connect). - [Carrier Connectors](https://www.shiplab.com/connectors/): Supported parcel and regional carriers (Core and Core+ tiers); sync to destinations including BI tools, warehouses, and cloud storage. - [Data Lake](https://www.shiplab.com/data-lake/): Vendor-neutral, unlimited storage; sync source data to your own systems anytime. ## Solutions - [Enterprise / Brands](https://www.shiplab.com/solutions-for/enterprise/): For shippers and brands with in-house data teams who want direct carrier billing data via API. - [Solution Providers](https://www.shiplab.com/solutions-for/provider/): For ship-tech providers (audit, freight pay, spend management, BI) who need managed carrier data collection at scale for their clients. ## Getting started - [Pricing](https://www.shiplab.com/pricing/): Professional ($159/mo, 2 Core connectors, ≤$200K invoiced), Enterprise ($399/mo, 5 connectors, ≤$1M), Provider (custom/unlimited). - [FAQ](https://www.shiplab.com/faq/): Who Shiplab is for, onboarding requirements (carrier logins), free trial, historical data limits, solution-agnostic positioning. - [Contact / Free trial](https://www.shiplab.com/contact/): Book a 30-minute discovery call to start a trial. ## Management Portal (app.shiplab.com) URL: https://app.shiplab.com The Shiplab Management Portal is the web UI for configuring and operating carrier data collection. It sits alongside the API and Data Lake — use the portal for day-to-day management; use the API for programmatic integration. Authentication: AWS Cognito (email/password sign-in, sign-up, password reset, 2FA). Portal sessions use Cognito JWT access tokens; the same tokens authenticate API calls from the browser (see Authentication below). Primary sections (routes): - `/pages/authentication/card/sign-in` — Sign in to your organization account. - `/group-management` — Manage companies (logical groupings of carrier accounts under your organization/reseller). - `/credential-management` — Add, edit, enable/disable, and monitor carrier credentials (logins, API keys, endpoints). Shows connection status (OK, ERROR, PENDING) per credential. - `/Data-Access` — Retrieve integration credentials and search invoice data: API token, AWS file-store keys, invoice search with signed download URLs. - `/explore` — Browse and explore collected invoice/shipment data. - `/profile` — Organization profile and billing contact details. - `/pages/plans-billing` — Self-service Plans & Billing (Professional / Enterprise trial via Stripe). Data hierarchy in the portal mirrors the API: **Customer (organization) → Connector group → Connector → Invoice**. Portal capabilities (backed by the Organization Management API): - Create/update/delete companies and carrier credentials without support tickets. - View credential health and download-efficacy reports. - Copy API token and AWS Data Lake credentials for external integrations. - Search invoices by date range and download files via time-limited signed URLs. - Accept the Service Agreement and start a 30-day Stripe trial (self-service accounts). ## API reference Full interactive docs: https://docs.shiplab.com (Postman-published **Shiplab Organization Management API**) Machine-readable reference (this document): `GET https://api.shiplab.com/llms.txt` — no auth required. Base URL: `https://api.shiplab.com` Content-Type: `application/json` for POST/PUT bodies. All authenticated endpoints scope data to the caller's organization (customer). Cross-organization access is rejected. ### Naming (public vs legacy) Public vocabulary: **customer**, **connector_group**, **connector**. Legacy paths remain for compatibility. | Public path / field | Legacy alias | |---------------------|--------------| | `/connector_groups` | `/companies` | | `/connectors` | `/credentials` | | `/connector_group` | `/company` | | `/connector` | `/credential` | | `/connector_status_report` | `/credential_status_report` | | `/connector_status_kpis` | `/credential_status_kpis` | | `connector_group_id` | `company_id` | | `connector_id` | `credential_id` | | `connector_group_name` | `company_name` | **Response field names follow the route you call:** - Legacy routes (`/company*`, `/credential*`, `/companies`, `/credentials`, …) return the original fields (`company_id`, `credential_id`, `company_name`). - New routes (`/connector*`) return the public fields (`connector_group_id`, `connector_id`, `connector_group_name`). **Shared endpoints** (`/search_invoices*`, `/search_charges*`, …) keep the original response fields. Requests may send either name (public preferred if both are sent). ### Data model ``` customer (organization) └── connector_group (shipper/client grouping) └── connector (carrier login / API connection) └── invoice (collected billing file metadata + S3 paths) ``` ### Health | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/health` | None | Liveness check. Returns `{"status": "up"}`. | | GET | `/llms.txt` | None | Returns this LLM-facing reference document as `text/plain`. | ### Organization | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/profile` | Required | Organization profile: id, name, display_name, invoice_format, billing fields, created_at. | | PUT | `/profile` | Required | Update billing fields (billing_address_*, billing_contact_name, billing_email_address). | | GET | `/billing` | Required | Current SaaS billing state for Plans & Billing UI (mode, status, plan, trial, agreement, Stripe embed config when eligible). | | POST | `/billing/agreement-acceptance` | Cognito JWT | Persist Service Agreement acceptance for the authenticated user; required before Stripe Pricing Table is shown. Org API tokens are rejected. | | GET | `/connector_groups` | Required | List all connector groups (`/companies` alias). | | GET | `/connectors` | Required | List all connectors (`/credentials` alias). | | GET | `/monthly_invoice_summary` | Required | Monthly rollup: period_start_date, package_count, invoice_count, net_spent. | | GET | `/weekly_invoice_summary` | Required | Weekly rollup with same fields. | | GET | `/connector_status_report` | Required | Per-connector status: connector_group_name, username, last_check_date, status. | | GET | `/connector_status_kpis` | Required | Aggregates: passed, failed, unprocessed, total. | | GET | `/download_efficacy_report` | Required | Download progress per connector group/username/invoice_date: available_count, downloaded_count, progress_percentage, missing_invoice_numbers. | | GET | `/download_efficacy_kpis` | Required | Organization-wide available_count, downloaded_count, progress_percentage. | | GET | `/get_api_token` | Required | Returns `{"api_token": ""}` for programmatic API access. | | GET | `/get_file_store_connection_secret` | Required | Returns AWS IAM credentials for direct Data Lake / S3 access: `aws_user_secret_id` (access key ID), `aws_user_token` (secret access key). | | GET | `/get_managed_warehouse_connection_secret` | Required | Returns managed SQL warehouse connection: `sql_warehouse_jdbc_url`, `token`. | ### Plans & Billing (Stripe self-service) Self-service Professional / Enterprise trials. Provider/manual accounts use custom billing and do not receive Stripe Pricing Table embed config. Activation is driven by Stripe webhooks — do not treat the Checkout return URL as proof of subscription. | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/billing` | Required | Billing state for the Management UI. | | POST | `/billing/agreement-acceptance` | Cognito JWT only | Save Service Agreement acceptance (version/url from server config). Empty body. Returns 201. | | POST | `/webhooks/stripe` | Stripe signature | Server-to-server only. Verifies `Stripe-Signature` on the raw body. Idempotent by Stripe event id. | **`GET /billing` response (shape):** ```json { "billing_reference": "slb_84c21d9f-2a16-4c15-a55e-xxxxxxxxxxxx", "billing_mode": "self_service", "billing_status": "unconverted", "current_plan_code": null, "trial_ends_at": null, "billing_activated_at": null, "current_subscription_id": null, "agreement_accepted": false, "checkout_eligible": true, "show_pricing_table": false, "service_agreement_url": "https://...", "service_agreement_version": "2026-04-01", "terms_of_service_url": "https://...", "customer_portal_url": "https://billing.stripe.com/p/login/...", "stripe": null, "subscription": null } ``` | Field | Notes | |-------|-------| | `billing_mode` | `self_service`, `manual`, or `exempt`. | | `billing_status` | `unconverted`, `trialing`, `active`, `past_due`, `canceled`, `manual_active`. | | `checkout_eligible` | `true` when `billing_mode=self_service` and status is `unconverted`, `canceled`, or `past_due`. | | `show_pricing_table` | `true` only when checkout-eligible **and** agreement accepted for the current agreement version. Then `stripe.publishable_key` and `stripe.pricing_table_id` are populated for the Pricing Table embed. Use `billing_reference` as Stripe `client-reference-id` and the Cognito user email as `customer-email`. | | `subscription` | Present when a current Stripe subscription row exists: `plan_code`, `stripe_status`, `trial_end`, `current_period_end`, `cancel_at_period_end`. | **`POST /billing/agreement-acceptance`:** requires `Authorization: Bearer ` (org API tokens → 403). Email is taken from the JWT `email` claim or Cognito GetUser. ### Connector group | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/connector_group` | Required | Create connector group. Body: `{"display_name": "My shipping company"}`. Returns 201 with `connector_group_id`. Legacy `/company` returns `company_id`. | | GET | `/connector_group/{id}` | Required | Get one connector group (id, name, display_name, note). | | PUT | `/connector_group/{id}` | Required | Update connector group. Body: `{"display_name": "..."}`. | | DELETE | `/connector_group/{id}` | Required | Delete connector group. | | GET | `/connector_group/{id}/connectors` | Required | List connectors for one connector group. Legacy: `/company/{id}/credentials` (returns `company_id` / `company_name`). | | GET | `/connector_group/{id}/monthly_invoice_summary` | Required | Monthly invoice summary for one connector group. | | GET | `/connector_group/{id}/weekly_invoice_summary` | Required | Weekly invoice summary for one connector group. | ### Connector | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/carrier_connection_config` | Required | List supported carriers with `name`, `collection_method`, `connection_config.required_fields`, and `user_instruction`. Call this before creating connectors. | | POST | `/connector` | Required | Create connector. Body may use `connector_group_id` (preferred) or legacy `company_id`. Returns 201 with `connector_id`. Legacy `/credential` returns `credential_id`. | | GET | `/connector/{id}` | Required | Get connector with `connector_group_id` / `connector_group_name`. Legacy `/credential/{id}` returns `company_id` / `company_name`. | | PUT | `/connector/{id}` | Required | Update connector (password, api_key, endpoint, remote_directory, enabled). Username and carrier cannot be changed. | | DELETE | `/connector/{id}` | Required | Delete connector. | ### Invoice search | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/search_invoices` | Required | Paginated invoice search with signed download URLs. | | POST | `/search_invoices_all` | Required | Same filters; returns all matching invoices (no pagination). | | POST | `/export_invoices` | Required | Same filters; writes a file to storage in the background. Use this for large pulls — see Data export. | | POST | `/get_invoice_download_url` | Required | Get signed URL for a specific invoice file identifier. Restricted access. | **Request body** (both search endpoints): ```json { "start_date": "2025-01-01", "end_date": "2025-09-30", "carrier": "ups", "file_format": "U21C", "output_format": "json", "connector_group_id": 1, "connector_id": 42, "limit": 100, "next_page_token": "", "fields": ["invoice_number", "invoice_date", "signed_url", "carrier"] } ``` Unknown request keys are rejected with `400` listing the allowed keys. | Field | Required | Description | |-------|----------|-------------| | start_date | Yes | Inclusive start, `YYYY-MM-DD`. | | end_date | Yes | Inclusive end, `YYYY-MM-DD`. | | carrier | No | Filter by carrier name via active `file_format.carrier` (case-insensitive). | | file_format | No | Filter by an active `file_format.code` from the database. If both `carrier` and `file_format` are set, the code must belong to that carrier. | | output_format | No | File variant to link: `json` (default), `jsonl`, or `csv`. | | connector_group_id | No | Scope to one connector group (legacy alias: `company_id`). | | connector_id | No | Scope to one connector (legacy alias: `credential_id`). | | limit | No | Page size 1–1000 (default 100). `/search_invoices` only. | | next_page_token | No | Cursor from prior page. `/search_invoices` only. | | fields | No | List of response property names to return. Omit for the default set. | **Available `fields` (invoice properties):** `credential_id`, `company_id`, `carrier`, `file_format`, `invoice_number`, `invoice_date`, `signed_url`, `file_format_id`, `invoice_file_format`, `parent_account_number`, `account_number`, `invoice_currency`, `invoice_gross_amount`, `invoice_net_amount`, `invoice_due_date`, `invoice_package_count`, `exception`, `created_at`, `updated_at`. Default when `fields` is omitted: all of the above except `carrier` on `/search_invoices`; `/search_invoices_all` also includes `carrier`. **Response** (`/search_invoices`) — shared search endpoints keep legacy field names: ```json { "invoices": [ { "credential_id": 42, "company_id": 1, "file_format": "U21C", "invoice_number": "123456", "invoice_date": "2025-03-15", "signed_url": "https://...", "file_format_id": 3, "invoice_file_format": "U21C", "parent_account_number": "P123", "account_number": "A456", "invoice_currency": "USD", "invoice_gross_amount": 100.0, "invoice_net_amount": 90.0, "invoice_due_date": "2025-04-15", "invoice_package_count": 2, "exception": null, "created_at": "2025-03-15T12:00:00", "updated_at": "2025-03-15T12:00:00" } ], "pagination": { "has_next_page": true, "next_page_token": "eyJj..." } } ``` Normalized invoice fields (`file_format_id` through `updated_at`) come from `normalized_invoice` when available; otherwise each is `null`. **Response** (`/search_invoices_all`): same `invoices` array; default set includes `carrier`. No pagination object. Signed URLs are time-limited HTTPS links to download the invoice file from Shiplab storage. Fetch the file immediately after receiving the URL. ### Charge search | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/search_charges` | Required | Paginated charge search. | | POST | `/search_charges_all` | Required | Same filters; returns all matching charges (no pagination). | | POST | `/export_charges` | Required | Same filters; writes a file to storage in the background. Use this for large pulls — see Data export. | **Request body** (both search endpoints): ```json { "start_date": "2025-01-01", "end_date": "2025-09-30", "carrier": "ups", "file_format": "U21C", "company_id": 1, "credential_id": 42, "limit": 100, "next_page_token": "", "fields": ["tracking_number", "charge_amount", "charge_description", "carrier"] } ``` Unknown request keys are rejected with `400` listing the allowed keys. **Always pass `fields` with only the columns you need.** Do not request every property — especially address/zone columns — on large pages; responses get slow and memory-heavy. | Field | Required | Description | |-------|----------|-------------| | start_date | Yes | Inclusive start, `YYYY-MM-DD`. | | end_date | Yes | Inclusive end, `YYYY-MM-DD`. | | carrier | No | Filter by carrier name via active `file_format.carrier` (case-insensitive). | | file_format | No | Filter by an active `file_format.code` from the database. If both `carrier` and `file_format` are set, the code must belong to that carrier. | | company_id | No | Scope to one company. | | credential_id | No | Scope to one credential. | | limit | No | Page size 1–100000 (default 100). `/search_charges` only. Max **100 pages** per cursor. | | next_page_token | No | Cursor from prior page. `/search_charges` only. | | fields | Strongly recommended | List of response property names to return. Prefer a small set. | **Available `fields` (charge properties):** - Core: `id`, `credential_id`, `company_id`, `carrier`, `invoice_number`, `invoice_date`, `file_format_id`, `invoice_file_format`, `tracking_number`, `charge_description`, `charge_amount`, `charge_amount_currency`, `service_type`, `created_at`, `updated_at` - Address/zone (opt-in; not in default): `zone`, `shipper_address_1`, `shipper_address_2`, `shipper_city`, `shipper_state`, `shipper_country`, `shipper_zip`, `recipient_address_1`, `recipient_address_2`, `recipient_city`, `recipient_state`, `recipient_country`, `recipient_zip` Default when `fields` is omitted: core properties above except `carrier` on `/search_charges`; `/search_charges_all` also includes `carrier`. Address/zone columns are **never** returned unless listed in `fields`. Prefer `/search_charges` for large result sets. At most 100 pages are returned; when the cap is hit, `pagination.max_pages_reached` is `true` and `has_next_page` is `false`. **Response** (`/search_charges`): ```json { "charges": [ { "id": 1001, "credential_id": 42, "company_id": 1, "invoice_number": "123456", "invoice_date": "2025-03-15", "file_format_id": 3, "invoice_file_format": "U21C", "tracking_number": "1Z...", "charge_description": "Residential Delivery", "charge_amount": 4.5, "charge_amount_currency": "USD", "service_type": "Ground", "created_at": "2025-03-15T12:00:00", "updated_at": "2025-03-15T12:00:00" } ], "pagination": { "has_next_page": true, "page": 1, "next_page_token": "eyJj..." } } ``` ### Data export (large result sets) Search endpoints build the whole response in memory, so they are a poor fit for very large pulls. Exports instead write a file to Shiplab storage in the background and hand back a `report_id` immediately. **Use exports whenever you expect more than a few pages of data.** | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/export_invoices` | Required | Start an invoice export. Returns `202` with `report_id` and `status: "PENDING"`. | | POST | `/export_charges` | Required | Start a charge export. Returns `202` with `report_id` and `status: "PENDING"`. | | GET | `/exports` | Required | List recent export reports (newest first). Optional `?limit=` (1–200, default 50). | | GET | `/export/{report_id}` | Required | Report status, row count, and `error_message` when it failed. | | GET | `/export/{report_id}/download` | Required | Temporary download URL. `409` until the report is `COMPLETED`. | | GET | `/export/{report_id}/file?token=...` | Token in URL | Streams the export file. This is what `download_url` points at; no `Authorization` header needed. | **Request body** (both export endpoints): ```json { "start_date": "2025-01-01", "end_date": "2025-09-30", "carrier": "ups", "file_format": "U21C", "company_id": 1, "credential_id": 42, "fields": ["tracking_number", "charge_amount", "charge_description"], "export_format": "csv" } ``` Filters are identical to the matching search endpoint minus `limit` and `next_page_token`; unknown keys are rejected with `400`. `/export_invoices` also accepts `output_format` (`json`, `jsonl`, or `csv`), which selects *which extracted invoice artifact to match* — it is not the export encoding. | Field | Required | Description | |-------|----------|-------------| | export_format | No | Encoding of the exported file: `csv` (default) or `jsonl`. | | fields | Strongly recommended | Columns to write, in order. Same names as the matching search endpoint. | **Always pass `fields` with only the columns you need.** Every extra column multiplies file size across millions of rows. Charge address/zone columns are opt-in and never written unless listed. `signed_url` cannot be exported: signed URLs expire long before a report is consumed — use `/search_invoices` for those. **Report lifecycle:** `PENDING` → `RUNNING` → `COMPLETED` or `FAILED`. Poll `GET /export/{report_id}` every few seconds; large exports take minutes. **Response** (`POST /export_charges`, `GET /export/{report_id}`): ```json { "report_id": "9f2c1b7a4e8d4c1fae0b7c2d3e4f5a6b", "reseller_id": 35, "resource_type": "charges", "export_format": "csv", "status": "COMPLETED", "row_count": 1482301, "file_size_bytes": 91238412, "error_message": null, "filters": { "start_date": "2025-01-01", "end_date": "2025-09-30" }, "fields": ["tracking_number", "charge_amount", "charge_description"], "created_at": "2025-10-01T12:00:00", "started_at": "2025-10-01T12:00:01", "completed_at": "2025-10-01T12:04:17", "expires_at": "2025-10-08T12:00:00" } ``` When `status` is `FAILED`, `error_message` explains why (bad filters, row cap exceeded, or an interrupted run). Create a new export after fixing the cause. **Response** (`GET /export/{report_id}/download`): ```json { "report_id": "9f2c1b7a4e8d4c1fae0b7c2d3e4f5a6b", "status": "COMPLETED", "export_format": "csv", "row_count": 1482301, "file_size_bytes": 91238412, "download_url": "https://api.shiplab.com/export/9f2c1b7a.../file?token=...", "direct_download_url": "https://shiplab-uploads.s3.amazonaws.com/...", "url_expires_in_seconds": 3600, "expires_at": "2025-10-08T12:00:00" } ``` Use `download_url`. It is a short (~90 character) link that streams the file straight from the API and needs no `Authorization` header, so it works with plain HTTP clients and tools that reject very long URLs. The token in the URL is the credential: it is rotated on every `/download` call, so an older link stops working, and it expires after `url_expires_in_seconds`. `direct_download_url` is a presigned S3 link to the same object. It is over 1 KB because it carries an AWS session token, and some clients cannot handle that. Prefer it only for very large files, where downloading straight from S3 avoids proxying bytes through the API. Limits: at most 3 exports in progress per organization (`429` beyond that), and 5,000,000 rows per report. Files are retained for 7 days; after `expires_at`, the download endpoint returns `404` and you must re-run the export. ### Carrier connection fields Call `GET /carrier_connection_config` for the authoritative list. Common patterns: | Carrier type | Typical required_fields | Notes | |--------------|------------------------|-------| | ups, fedex | password | Standard portal login. | | purolator | password, endpoint, remote_directory | SFTP-style connection. | | ontrac | (none — email collection) | Username auto-generated; email-based collection. | | axlehire, cdl | api_key, endpoint | API-key carriers. | Supported carrier names include: ups, fedex, purolator, ontrac, axlehire, cdl, and additional regional/manifest carriers. File format codes for invoice/charge filters must exist in the `file_format` table with `active = true`. Carrier filters resolve through active `file_format.carrier` values. Carrier connection field requirements come from `GET /carrier_connection_config`. ## Authentication Two credential types authenticate against the Organization Management API. Both are sent in the **`Authorization` HTTP header**. ### 1. API token (machine-to-machine) Best for: scripts, ETL jobs, backend services, MCP/AI clients, CI pipelines. - Header: `Authorization: ` (raw token string; no `Bearer` prefix). - One long-lived token per organization, provisioned at onboarding. - Retrieve anytime: `GET /get_api_token` (requires an already-valid session — Cognito JWT or existing api_token). - For the MCP server, send the same token as `Authorization: Bearer ` (see MCP section). Legacy `X-Shiplab-Api-Token` is still accepted by tools if present. Example: ```http GET /companies HTTP/1.1 Host: api.shiplab.com Authorization: your_reseller_api_token_here ``` Invalid or missing token → `403 Forbidden`. ### 2. AWS Cognito JWT (portal / interactive users) Best for: app.shiplab.com browser sessions, user-delegated API calls from the portal. - Header: `Authorization: Bearer ` - Tokens issued by AWS Cognito user pool on sign-in at app.shiplab.com. - Validated via Cognito JWKS (RS256). JWT must include `custom:customer_id` claim matching the organization's reseller id. - Tokens expire; refresh via Cognito auth flow when expired. Example: ```http GET /profile HTTP/1.1 Host: api.shiplab.com Authorization: Bearer eyJraWQiOi... ``` Expired JWT → `403 Forbidden` ("Expired jwt token!"). Invalid JWT → `403 Forbidden` ("Invalid jwt token!"). Auth failures during JWT verification may return `500 Internal Error!`. Application errors from the route itself (validation `422`, not found `404`, conflict `409`, etc.) are returned as-is and are not rewritten to 500. ### 3. AWS file-store credentials (Data Lake direct access) Separate from API auth — these are **AWS IAM keys** for reading your organization's Data Lake (S3). - Retrieve: `GET /get_file_store_connection_secret` (requires API token or Cognito JWT). - Response: - `aws_user_secret_id` — AWS access key ID - `aws_user_token` — AWS secret access key - Use with AWS SDK/CLI to list and download objects in your provisioned bucket. - Scoped IAM user per organization; read access to that organization's data lake prefix only. ### 4. Managed warehouse credentials - Retrieve: `GET /get_managed_warehouse_connection_secret` - Returns `sql_warehouse_jdbc_url` and `token` for JDBC/ODBC warehouse access. ### Auth decision guide for machines | Use case | Credential | |----------|------------| | Automated nightly invoice pull | API token | | Cursor / MCP agent (API token) | `Authorization: Bearer ` on MCP requests | | Claude.ai / Claude Connect (OAuth) | Cognito JWT after Hosted UI login (`Bearer`) | | User logged into app.shiplab.com | Cognito JWT (`Bearer`) | | Bulk read from S3 Data Lake | AWS keys from `/get_file_store_connection_secret` | | SQL warehouse / BI tool | JDBC URL + token from `/get_managed_warehouse_connection_secret` | ## MCP (AI clients) Endpoint: `https://mcp.shiplab.com/mcp` Transport: Streamable HTTP (remote MCP). The MCP server proxies authenticated calls to the Organization Management API. ### Authentication Unauthenticated requests to `/mcp` return **401** with a `WWW-Authenticate` challenge that points clients at OAuth protected-resource metadata. | Client | How to authenticate | |--------|---------------------| | **Cursor** / local MCP clients | `Authorization: Bearer ` on every request | | **Claude.ai / Claude Desktop Connect** | OAuth via Cognito Hosted UI (authorization code + PKCE). After Connect, the client sends a Cognito access token as `Authorization: Bearer `. JWT must include `custom:customer_id`. | | **Claude Code (manual)** | Same Bearer API token header, or OAuth when using Connect | Legacy: tools still accept `X-Shiplab-Api-Token` if present, but the MCP HTTP gate requires `Authorization: Bearer`. OAuth discovery (MCP host is the authorization-server facade; Cognito does the real login): | Endpoint | Purpose | |----------|---------| | `GET /.well-known/oauth-protected-resource/mcp` | Resource metadata (`authorization_servers` = MCP host) | | `GET /.well-known/oauth-authorization-server` | AS metadata (authorize / token / register) | | `POST /register` | Dynamic client registration facade | | `GET /authorize` | Redirects to Cognito Hosted UI | | `GET /callback` | Cognito → client callback bridge | | `POST /token` | Proxies token exchange to Cognito | | `GET /health` | Load balancer health (no auth) | ### Tools | MCP tool | Title | Side effect | API | |----------|-------|-------------|-----| | health_check | Health Check | read-only | GET /health | | get_profile | Get Profile | read-only | GET /profile | | get_connector_groups | List Connector Groups | read-only | GET /connector_groups | | get_connectors | List Connectors | read-only | GET /connectors | | get_connector_group | Get Connector Group | read-only | GET /connector_group/{id} | | get_connector | Get Connector | read-only | GET /connector/{id} | | get_carrier_connection_config | Carrier Connection Config | read-only | GET /carrier_connection_config | | create_connector | Create Connector | write (additive) | POST /connector | | export_invoices | Export Invoices | read-only | POST /export_invoices | | export_charges | Export Charges | read-only | POST /export_charges | | get_export_status | Get Export Status | read-only | GET /export/{report_id} | | get_export_download_url | Get Export Download URL | read-only | GET /export/{report_id}/download | | list_exports | List Exports | read-only | GET /exports | Invoice and charge data is only reachable through exports over MCP. The search endpoints are not exposed as tools because large result sets do not belong in an AI client's context. The workflow is: call `export_invoices` / `export_charges`, poll `get_export_status` until `COMPLETED`, then give the user the URL from `get_export_download_url`. ### Cursor config example ```json { "mcpServers": { "shiplab-api": { "url": "https://mcp.shiplab.com/mcp", "headers": { "Authorization": "Bearer your_reseller_api_token" } } } } ``` ### Claude.ai custom connector Add connector URL `https://mcp.shiplab.com/mcp`, then complete Cognito sign-in when prompted. No API token header is required for the OAuth Connect path. ## Optional - [Newsroom](https://www.shiplab.com/category/newsroom/): Product announcements and company updates. - [Privacy Policy](https://www.shiplab.com/privacy-policy/): Data collection and privacy practices for the website, portal, API, and MCP connector. - [Terms and Conditions](https://www.shiplab.com/terms-and-conditions/): Website and service terms.