SanMar API toolkit
This skill is a deterministic, JSON-in / JSON-out wrapper over SanMar's SOAP web services and the PromoStandards order-shipment-notification service. Every tool is reached through one CLI entrypoint:
echo '<json-args>' | python3 scripts/sanmar.py <action>
The action is the first CLI argument; arguments are a JSON object on
stdin. Each call prints a single JSON object on stdout, or
{"error": {"type": ..., "message": ...}} with a non-zero exit code on failure.
The skill is self-contained: no Odoo, no ORM, no lxml. It needs Python 3.11+
and requests; all XML/SOAP work uses the stdlib xml.etree.ElementTree, and
the FTP files are parsed with the stdlib (fixed-width, tab, and pipe layouts —
no openpyxl). Three optional installs unlock the file/browser tools:
pypdf>=4.0— required forparse-po-pdf(PDF text extraction).paramiko>=3.0— required for every SFTP-backed tool:lookup-mainframe-color, the marketing-color auto-resolve fallback incheck-inventory/get-pricing, and fetching the FTP feeds behindget-inventory-feed,get-shipment-status, andparse-invoice-file(only when reading straight off the server rather than a local path/text).playwright>=1.40— required only for the browser-drivenprocess-returnaction (returns are not exposed by any SanMar web service). Also needs a browser binary:python -m playwright install chromium.
All three are imported lazily, so the skill still loads without them; the
affected tools raise a clear error pointing at the missing pip install.
When to use this skill
Reach for a scripts/sanmar.py action when the request involves any of:
- Looking up SanMar product styles, colors, sizes, or images.
- Checking inventory at SanMar warehouses — real-time per-warehouse levels
(
get-inventory-levels), a single SKU (check-inventory), or a bulk feed (get-inventory-feed). - Pulling customer-specific (
myPrice) pricing for a SKU. - Validating a draft cart of style/color/size lines before ordering.
- Submitting a SanMar purchase order, or polling its status / tracking.
- Getting tracking numbers and shipment detail for a PO — live
(
get-tracking) or from the FTP Daily Shipment Status file (get-shipment-status). - Retrieving invoices for accounts-payable — by PO, invoice number, order
date, date range, or all unpaid (
get-invoices), or parsing the FTP Daily Invoice / EDI-810 file (parse-invoice-file). Both emit a normalizedcommonshape that plugs straight intodrivethru-payable-matching. - Processing a return for a shipped order (browser-driven portal action).
- Parsing an uploaded PDF purchase order into a structured draft for review.
- Translating a marketing color name (e.g. "Athletic Heather") into SanMar's mainframe color code (e.g. "ATHHTHR") when inventory/pricing rejects the consumer-facing color.
Do not use it for other apparel vendors (S&S, Alpha, etc.), and never invent SanMar request shapes from prose — call the deterministic actions.
Actions
Grouped by the four order-lifecycle pillars (plus product/color helpers and returns). Every action reads credentials from the environment or inline JSON (see Credentials), so those fields are omitted from the table.
Products & pricing
| Action | Risk | stdin JSON (key fields) |
|---|---|---|
search-products |
read-only | {style, color?, size?} |
get-pricing |
read-only | {lines: [{style, color, size}, ...], auto_resolve_color?} |
lookup-mainframe-color |
read-only (SFTP) | {style, color, size?, force_refresh?} |
Inventory
| Action | Risk | stdin JSON (key fields) |
|---|---|---|
check-inventory |
read-only | {style, color, size, auto_resolve_color?} — legacy single-SKU port |
get-inventory-levels |
read-only | {style, part_ids?} — PromoStandards v2, named per-warehouse |
get-inventory-feed |
read-only (SFTP/file) | {style?, path? | text? | remote_path?} — bulk sanmar_dip.txt |
Purchase orders
| Action | Risk | stdin JSON (key fields) |
|---|---|---|
validate-cart |
read-only | {purchase_order: {...}} (pre-submit getPreSubmitInfo, no commit) |
create-purchase-order |
high — external write | {purchase_order: {...}, confirm} |
parse-po-pdf |
read-only (local file) | {pdf_path} |
cancel-order |
stub | {po_number, reason?, confirm?} — SanMar exposes no public cancel endpoint |
Tracking
| Action | Risk | stdin JSON (key fields) |
|---|---|---|
check-order-status |
read-only | {po_number} |
get-tracking |
read-only | {po_number | sales_order_number | shipment_date} — package-level OSN detail |
get-shipment-status |
read-only (SFTP/file) | {path? | text? | remote_path?, po_number?} — FTP ASN file |
Invoicing
| Action | Risk | stdin JSON (key fields) |
|---|---|---|
get-invoices |
read-only | {po_number | invoice_number | order_date | (start_date+end_date) | unpaid, headers_only?} |
parse-invoice-file |
read-only (SFTP/file) | {path? | text? | remote_path?, edi?} — Daily Invoice / EDI-810 |
Returns
| Action | Risk | stdin JSON (key fields) |
|---|---|---|
process-return |
high — portal write (browser) | {order_number | po_number, lines: [{style, color, size, reason, quantity?, details?, replacement?, image_path?}], confirm?} |
Run python3 scripts/sanmar.py with no action to print the full action list.
See references/examples.md for realistic prompts and
end-to-end flows. Offline correctness tests for the invoice/tracking/inventory/
feed parsers live in scripts/_selftest.py
(python3 scripts/_selftest.py).
Credentials
SanMar SOAP requests carry three fields: sanMarCustomerNumber,
sanMarUserName, sanMarUserPassword. PromoStandards (order shipment) reuses
the same username/password in its SOAP header.
The skill never hardcodes credentials. Supply them in either of two ways:
-
Environment variables (preferred for a deployed agent):
SANMAR_CUSTOMER_NUMBER=... SANMAR_USERNAME=... SANMAR_PASSWORD=... SANMAR_ENV=production # or "development" — flips PO submit to test-ws # SFTP (only for mainframe-color resolution; separate password): SANMAR_FTP_USERNAME=<customer_number> # defaults to SANMAR_CUSTOMER_NUMBER SANMAR_FTP_PASSWORD=... -
Inline in the stdin JSON — pass
customer_number,username,password,environment, and (for SFTP tools)ftp_passwordalongside the tool's own arguments. Inline values take precedence over the environment.
If neither is present, the tool exits with
{"error": {"type": "config_error", ...}} (exit code 2). In a normal
user-facing turn, treat that as a signal to ask the user for the missing
fields. On a delegated agent-to-agent turn it means something different — see
Agent-to-Agent (A2A) mode below; do not ask, and do
not go looking for the credentials yourself. Either way, do not guess
defaults, reuse credentials across tenants, or paste secrets the user did not
provide.
Agent-to-Agent (A2A) mode
Deploy this skill on a dedicated SanMar agent and let another agent in the org (e.g. an Accounts Payable or purchasing agent) reach it through a delegation connection in the Knoxville platform. On that kind of turn the SanMar credentials are not yours — they belong to the calling agent, which shares them with this connection.
How the credentials reach the skill. When you handle a delegated call, the
runtime pulls the shared SANMAR_* credentials for this conversation and places
them into the skill's execution environment for this turn only, before your
exec runs. scripts/sanmar.py then reads them from the environment exactly as
it would standalone. You do not see these values in your context, and that
is by design — they are deliberately kept out of the model prompt.
So on a delegated turn, just run the tool. Your first action for a SanMar
request is the exec call itself, e.g.:
echo '{"lines":[{"style":"ST405","color":"...","size":"..."}]}' | python3 scripts/sanmar.py get-pricing
Do not, before running it:
- call
get_my_bundle,get_delegated_credentials, or any tool to look for or "verify" the credentials — they are intentionally invisible to you, so you will always find nothing and wrongly conclude you have no access; - spawn a sub-agent (
sessions_spawn) or try to delegate this skill's job to another session — you are the agent that runs this skill; - tell the caller you lack credentials or access before you have actually run the script and read its output.
If the script itself returns config_error / an auth error, then the
calling connection did not share the credential (or sharing is misconfigured) —
surface that error message to the caller verbatim. Do not ask the user for a key
(on a delegated turn there is no user to ask), and never print a credential
value into the reply.
FTP credentials are separate. Per SanMar's FTP Integration Guide v23.1, the SDL feed lives on
ftp.sanmar.com:2200over SFTP (SSH), and the FTP password is issued separately from the web-services password — yoursanmar.comweb username will not work on the FTP server.
Write safety
create-purchase-order, cancel-order, and process-return are the
side-effecting actions.
create-purchase-orderrequires"confirm": trueto transmit. Without it, it returns a dry-run preview of the SOAP envelope and makes no submit call. Normal flow:get-pricing(to enrich each line withinventory_keyandsize_index) →validate-cart(proceed only ifok) →create-purchase-orderwithconfirm: true. Confirm with the user first.cancel-orderis a reserved stub: SanMar's published SOAP and PromoStandards bindings expose no cancel operation, so it always returns a structurednot_implementedresponse. Cancellations go through SanMar customer service.process-returnrequires"confirm": trueto file. Without it, it logs into the portal, fills + validates the return form, and returns adry_runpreview (matched lines + estimated credit/restock) without submitting. See the next section.
All other actions are pure reads.
Invoicing
get-invoices wraps SanMar's Standard Invoicing service (InvoicePort).
Pick exactly one query:
po_number— every invoice for a customer PO (the payables match key).invoice_number— one invoice by its SanMar number.order_date— invoices for an order date (yyyy-mm-dd).start_date+end_date— an invoice-date range (≤ 3 months apart).unpaid: true— all currently open invoices.
Add headers_only: true to skip line items where the query type supports it.
Auth note. InvoicePort authenticates with
CustomerNo/UserName/Passwordelement names — distinct from thesanMar*names the other standard ports use. The skill handles this; you pass the same credentials.
Timing. SanMar invoices once per day after 9 p.m. Pacific, and only after an order ships. Pull invoice data the day after the order date. A PO that isn't invoiced yet returns an empty result (not an error).
Each returned invoice carries the SanMar-native header + lines, and a
common block — the cross-vendor invoice shape (source, po_number,
invoice_number, invoice_date, due_date, is_credit, has_lines, total,
charges {merchandise, freight, freight_allowance, sales_tax},
lines[]) — so drivethru-payable-matching reconciles a SanMar invoice against
its PO with no SanMar-specific handling. A negative total sets
is_credit: true (a credit memo — route it to a human / vendor credit, never
bill it).
parse-invoice-file produces the identical shape from SanMar's FTP invoice
files instead of the API: the fixed-width Daily Invoice .txt (default) or the
EDI-810 file (edi: true). Supply the file as inline text, a local path, or
a remote_path (SFTP; a directory or omission fetches the newest
*Invoice_Details* file from the outbound folder). SanMar states the Daily
Invoice file "shares the same data as the invoicing calls", so it's the batch
alternative to get-invoices. See
references/invoicing.md.
Inventory — three surfaces
get-inventory-levels(preferred) — PromoStandardsgetInventoryLevelsv2.0.0. Returns each part's total plus a named per-warehouse breakdown (warehouse_id,warehouse_name,quantity). Query a whole style, or passpart_ids(SanMar unique keys) to limit it.check-inventory— the legacygetInventoryQtyForStyleColorSizeport for one style/color/size. Returns per-warehouse quantities (unnamed) and atotal_available= the largest single warehouse (SanMar ships complete from one warehouse by default). Keeps the marketing→mainframe color auto-resolve.get-inventory-feed— the bulksanmar_dip.txtSFTP feed (refreshed hourly), parsed to per-warehouse rows. Passstyleto filter — the full feed is large.
Tracking — live + file
get-tracking— PromoStandards Order Shipment Notification. Query bypo_number(default),sales_order_number(SanMarSO-…), orshipment_date(UTC, 7-day window). Returns one package per shipment withtracking_number, normalizedcarrier,shipment_method,ship_date, thesales_order_number, ship-from/to city+state, and theitemsin the box.get-shipment-status— parses the FTP Daily Shipment Status (Advance Ship Notification) file: the same tracking numbers plus per-line costs (subtotal, freight, handling, invoice total) and box/LPN detail. Supplytext/path/remote_path; filter to onepo_number. Seereferences/shipment_status.md.check-order-status— a lightweight roll-up (SanMar sales-order number + shipped/submitted status) for a PO.
Processing returns (portal / browser)
SanMar exposes no web service for returns, so process-return drives the
sanmar.com customer portal with Playwright. It needs the optional playwright
install plus a browser binary (python -m playwright install chromium) and a
separate portal login (SANMAR_PORTAL_USERNAME / SANMAR_PORTAL_PASSWORD,
or inline portal_username / portal_password) — the web-services credentials
do not authenticate the portal.
Identify the order with order_number (the SanMar SO-… number — preferred,
since it is unique) or po_number (resolved against order history; errors if
the PO maps to more than one order). Each line is
{style, color, size, reason, quantity?, details?, replacement?, image_path?}:
reason∈SAMPLES,UNWANTED,ORDER_INCORRECT,INCORRECT_PRODUCT,DEFECTIVE_DAMAGED.UNWANTEDincurs a 20% restock fee.details(free text) andreplacement(true/false) are required forORDER_INCORRECT,INCORRECT_PRODUCT, andDEFECTIVE_DAMAGED.image_path(optional) uploads a photo, only forDEFECTIVE_DAMAGED.quantitydefaults to the line's originally shipped pieces.
Flow: login → resolve order → open the initiate page → match each requested
line to a row by style/color/size → check item, set quantity, reason, and any
reason-specific fields. Pass screenshot_path to capture the filled page.
Status — final submit not yet implemented. The flow is built up to the filled, validated form. The final Continue → review → confirm submission has not been reverse-engineered, so
confirm: truereturns anot_implementedresult (nothing is filed) andconfirm: falsereturns adry_runpreview. Seereferences/returns_flow_notes.mdfor the captured selectors and the remaining Step 5 to capture.
PDF purchase-order intake
parse-po-pdf takes {"pdf_path": "..."} and returns a best-effort
ParsedPurchaseOrder: po_number, order_date, ship_method, ship_to,
lines[] (style, color, size, quantity, unit_price), warnings[]
for low-confidence fields, and draft_for_submit — a ready-to-pass
purchase_order object for create-purchase-order, populated only when the
parse is complete enough. Always show the parsed PO back to the user for
approval before submitting. Heuristic parsing cannot guarantee correctness
across every PO layout — treat the output as a draft.
Mainframe color resolution
SanMar's inventory/pricing/PO endpoints query against the mainframe color
code (e.g. ATHHTHR), not the marketing COLOR_NAME (Athletic Heather).
When a marketing name is used, SanMar typically errors or returns nothing.
check-inventory and get-pricing auto-handle this: on error or an empty
response (and with auto_resolve_color defaulting to true) they download
SanMarPDD/SanMar_SDL_N.csv over SFTP, look up the matching
SANMAR_MAINFRAME_COLOR, and retry once. Pass auto_resolve_color: false when
you already have a known mainframe code. For explicit control, call
lookup-mainframe-color directly — its status is matched, ambiguous, or
not_found. The SDL CSV is cached locally for 24h (SanMar refreshes nightly);
pass force_refresh: true to bypass the cache.
Endpoints
All on host ws.sanmar.com:8080 in production:
- Pricing —
SanMarWebService/SanMarPricingServicePort - Product info —
SanMarWebService/SanMarProductInfoServicePort - Inventory (legacy) —
SanMarWebService/SanMarWebServicePort - Inventory (PromoStandards v2) —
promostandards/InventoryServiceBinding - PO submit —
SanMarWebService/SanMarPOServicePort - Order shipment / tracking —
promostandards/OrderShipmentNotificationServiceBinding - Invoicing —
SanMarWebService/InvoicePort - SFTP data feeds —
ftp.sanmar.com:2200(SFTP/SSH, not TLS)
With SANMAR_ENV=development, the PO submit and invoice endpoints
switch to their https://test-ws.sanmar.com:8080/... counterparts; the
read-only pricing/product/inventory/tracking services are production-only.
SanMar must allowlist the calling IP. A connection timeout is most often a missing IP allowlist entry, not an auth problem. FTP credentials are separate from web-service credentials (see Credentials).
Error model
Failures print {"error": {...}} and exit non-zero:
config_error(exit 2) — missing/invalid credentials.api_error— SanMar returned a SOAP fault orerrorOccurred=true. Includessurface,operation, andretryable.retryableisfalsefor auth, schema, and invalid-style errors.connection_error— network/timeout talking to SanMar or its SFTP server (retryable: truefor web-service transport failures).validation_error— bad input JSON, a missing required field, or an unparseable PDF.usage/unknown_action(exit 2) — bad CLI invocation; the message lists the validactions.
Surface the human-readable message to the user. Do not retry on
config_error, validation_error, or a non-retryable api_error.
References
references/examples.md— realistic agent prompts and end-to-end flows.references/web_services.md— SOAP service details (pricing, product info, inventory, PO submit, invoicing, tracking).references/purchase_orders.md— PO submit payload shape and the pre-submit/enrichment flow.references/invoicing.md— the InvoicePort query methods, response shape, the normalizedcommonpayables shape, and the FTP Daily Invoice / EDI-810 file formats.references/shipment_status.md— the OSN tracking response and the FTP Daily Shipment Status (ASN) file layout.references/ftp_feeds.md— the SDL/EPDD/dip feeds, inventory-by-warehouse, pricing files, and mainframe-color resolution.references/returns_flow_notes.md— the reverse-engineered portal return flow (selectors, reason matrix, step map) backingprocess-return.references/auth_and_patterns.md— auth fields and calling patterns.
