FlexOps Python SDK
Official Python SDK for the FlexOps multi-carrier shipping platform.
Installation
pip install flexops
Quick Start
from flexops import FlexOps
# API key authentication (recommended for server-to-server)
client = FlexOps(
api_key="live_...",
workspace_id="your-workspace-id",
)
# Get shipping rates from all carriers
rates = client.shipping.get_rates({
"origin": {"addressLine1": "123 Main St", "city": "New York", "stateProvince": "NY", "postalCode": "10001"},
"destination": {"addressLine1": "456 Oak Ave", "city": "Los Angeles", "stateProvince": "CA", "postalCode": "90210"},
"package": {"weight": 16, "weightUnit": "oz"},
})
request = {
"carrierCode": "USPS", "serviceCode": "GROUND_ADVANTAGE",
"origin": {"name": "Warehouse", "addressLine1": "123 Main St", "city": "New York", "stateProvince": "NY", "postalCode": "10001", "countryCode": "US"},
"destination": {"name": "Customer", "addressLine1": "456 Oak Ave", "city": "Los Angeles", "stateProvince": "CA", "postalCode": "90210", "countryCode": "US"},
"package": {"weight": 16, "weightUnit": "oz"},
"maximumPostageAmount": 10.25, # Caller-approved ceiling in USD.
# Required without an orderId. The SDK never fills it in; 0 (HazmatClassification.NONE) = not hazardous.
"hazmatClassification": 0,
}
preview = client.shipping.create_label(request)
print(preview["quotedPostageAmount"], preview["expiresAt"])
# Run only after the caller explicitly approves this preview (within five minutes).
def purchase_approved_label(request, preview, purchase_key):
return client.shipping.create_label(
{**request, "confirmationToken": preview["confirmationToken"]},
idempotency_key=purchase_key, # Persist per purchase; reuse on retries.
)
# Track a shipment
tracking = client.shipping.track("9400111899223456789012")
Live label approval (2.0.0+)
Migration: the optional CreateLabelRequest model now uses carrier_code,
service_code, origin, destination, and package. Raw dictionaries use the
camelCase Gateway names shown below. Pass idempotency_key to the method, not
inside the request body. See CHANGELOG.md for model changes.
The example below requires SDK 2.0.0 or later. Version 1.0.2 does not include the per-call idempotency argument.
For live domestic single-label requests, maximumPostageAmount is required: positive
USD, at most two decimal places, up to 1,000,000. Missing or invalid values return
400 ApprovalRequired. Omitting confirmationToken returns a raw 200 preview
(status, quotedPostageAmount, maximumPostageAmount, currency, expiresAt,
confirmationToken). After explicit approval, resubmit the same shipment and ceiling
with the token and a unique per-purchase Idempotency-Key header. The token expires
after five minutes. A successful purchase returns the raw 201 label, including
labelId, trackingNumber, carrierCode, and labelData, without a data wrapper.
Keep the same key and request across retries. An uncertain outcome needs reconciliation;
do not start another purchase with a new key. An expired approval returns 409
ApprovalExpired; obtain and explicitly approve a fresh preview when no purchase is
unresolved. The ceiling bounds postage authorization, not later adjustments or separate
fees. Sandbox execution bypasses approval; batch, return, and raw carrier routes have
separate contracts. The SDK never automatically confirms a preview.
Authentication
API Key (recommended)
client = FlexOps(api_key="live_...", workspace_id="your-workspace-id")
Email / Password
client = FlexOps(base_url="https://gateway.flexops.io")
client.auth.login("user@example.com", "password")
client.workspace_id = "your-workspace-id"
Carrier-Specific Operations
client.carriers reaches USPS, UPS, FedEx and DHL through the legacy API proxy. The lookups (GET)
work with an API key or a user token. The writes are deprecated: each call raises a
DeprecationWarning, they need a user token (auth.login) because API keys get 403
API_KEY_SCOPE_REQUIRED, and they stop working when the proxy is retired on 2027-06-30. The proxy's
label-generation routes are closed (410 Gone) and are not offered.
Native replacements: client.shipping (rates, labels, cancellation, tracking, address validation),
client.pickups (carrier pickups), client.scan_forms (USPS scan forms) and client.hs_codes
(landed cost).
# USPS city/state lookup (API key or user token)
info = client.carriers.usps.city_state_lookup("10001")
# UPS tracking
events = client.carriers.ups.track({"trackingNumber": "1Z999AA10123456784"})
# Deprecated proxy write: user token only
client.auth.login("user@example.com", "password")
rates = client.carriers.fedex.get_rates({...})
Recurring Shipments, Report Schedules and Email Templates
Reads work with any API key or a user token. Writes need a user token or an API key granted the
resource's scope: recurring-shipments:write, report-schedules:write or email-templates:write.
A key without it gets 403 API_KEY_SCOPE_REQUIRED naming requiredScopes.
from flexops import RecurrenceInterval, TrackingEmailEvent
schedule = client.recurring_shipments.create({
"name": "Weekly restock", "interval": RecurrenceInterval.WEEKLY,
"presetId": preset_id, "addressId": address_id, "firstRunUtc": "2026-11-02T14:00:00Z",
})
client.email_templates.upsert({
"eventType": TrackingEmailEvent.DELIVERED,
"subjectTemplate": "Your order was delivered",
"htmlBodyTemplate": "<p>Tracking {{trackingNumber}} was delivered.</p>",
})
reports = client.report_schedules.list()["data"] # report schedules use the envelope
Retries and Idempotency
Every POST, PUT and PATCH sends an Idempotency-Key header: the key you pass (for example
idempotency_key= on shipping.create_label, insurance.purchase, carbon_offsets.purchase and
recurring_shipments.trigger),
or one the SDK generates for that call. The same key is sent on every retry of the call, so the
Gateway replays a write it already finished instead of charging twice.
GETretries on a dropped connection, a timeout, 429, 500, 502, 503 and 504.POST,PUTandPATCHretry on a dropped connection, a timeout, 429, 502, 503, 504 and 409IDEMPOTENCY_CONFLICT, never on 500 (the Gateway does not replay a 500, so a retry would run the write again).- A key reused with a different request gets 422
IDEMPOTENCY_KEY_REUSEDand is not retried. DELETEretries only on 429.
A generated key lives only for one call. To retry a purchase later, after the call has returned,
persist your own key with it and pass it again. A response with no body (204) returns None.
Workspace Roles
Roles are sent as the Gateway numbers. Pass a WorkspaceRole, its name or its number:
from flexops import WorkspaceRole
client.workspaces.invite_member("ops@example.com", WorkspaceRole.ADMIN) # or "Admin", or 1
client.workspaces.update_member_role(user_id, "Member")
Webhook Subscriptions
from flexops import WebhookEventType
# Leave events out to receive every event.
webhook = client.webhooks.create(
"https://your-app.com/webhooks/flexops",
[WebhookEventType.LABEL_CREATED, WebhookEventType.LABEL_CANCELED, WebhookEventType.BATCH_COMPLETED],
)
signing_secret = webhook["data"]["secret"] # returned only on create; store it
FlexOps sends these events, named exactly as below. Names are PascalCase and matched exactly, so a
name like label.created would never match; create and update raise ValueError for unknown names.
| Event | Sent when |
|---|---|
LabelCreated |
A live label is purchased (sandbox labels send nothing) |
LabelCanceled |
A live label is voided |
BatchCompleted |
A batch label job finishes |
TrackingUpdated |
A USPS tracking scan arrives (USPS push-tracking pilot workspaces only) |
TrackingDelivered |
A USPS package is delivered (USPS push-tracking pilot workspaces only) |
Webhook Verification
Each delivery is a POST whose body is the event data itself (there is no {"type", "data"}
envelope). The headers carry the rest:
| Header | Value |
|---|---|
X-FlexOps-Event |
Event name, e.g. LabelCreated; test for a test delivery |
X-FlexOps-Event-Id |
Event ID; use it to ignore retried duplicates |
X-FlexOps-Timestamp |
Unix seconds when the delivery was signed |
X-FlexOps-Signature |
sha256= + hex HMAC-SHA256 of {timestamp}.{raw body}, keyed with your whsec_... secret |
X-FlexOps-Delivery |
Delivery attempt ID |
Verify against the raw body bytes; re-serialized JSON does not reproduce what was signed.
import json
import os
from flask import Flask, request
from flexops import WebhooksResource
app = Flask(__name__)
@app.post("/webhooks/flexops")
def flexops_webhook():
raw_body = request.get_data() # bytes, exactly as received
if not WebhooksResource.verify_signature(
raw_body,
request.headers.get("X-FlexOps-Signature"),
os.environ["FLEXOPS_WEBHOOK_SECRET"],
timestamp=request.headers.get("X-FlexOps-Timestamp"), # rejected if more than 300 s from now
):
return "Invalid signature", 401
event_type = request.headers["X-FlexOps-Event"] # "LabelCreated", "TrackingUpdated", "test", ...
data = json.loads(raw_body) # the event data; no envelope
# Process the event...
return "", 200
timestamp is keyword-only and required. Pass tolerance= (seconds) to change the 300 second window.
Curl Quickstart
Every SDK method is a thin wrapper around the FlexOps REST API. If you want to verify the API before committing to the SDK — or you're integrating from a language we don't ship a SDK for — these curl invocations hit the same endpoints:
# Shop rates across all connected carriers
curl -X POST https://gateway.flexops.io/api/shipping/rates \
-H "X-API-Key: live_..." \
-H "Content-Type: application/json" \
-d '{
"origin": {"addressLine1": "123 Main St", "city": "New York", "stateProvince": "NY", "postalCode": "10001", "countryCode": "US"},
"destination": {"addressLine1": "456 Oak Ave", "city": "Los Angeles", "stateProvince": "CA", "postalCode": "90210", "countryCode": "US"},
"package": {"weight": 16, "weightUnit": "oz"}
}'
# Preview a live label; this does not purchase it.
# hazmatClassification 0 = not hazardous; required when there is no orderId.
curl -X POST https://gateway.flexops.io/api/shipping/labels \
-H "X-API-Key: live_..." \
-H "Content-Type: application/json" \
-d '{
"carrierCode": "USPS", "serviceCode": "GROUND_ADVANTAGE",
"origin": {"name": "Warehouse", "addressLine1": "123 Main St", "city": "New York", "stateProvince": "NY", "postalCode": "10001", "countryCode": "US"},
"destination": {"name": "Customer", "addressLine1": "456 Oak Ave", "city": "Los Angeles", "stateProvince": "CA", "postalCode": "90210", "countryCode": "US"},
"package": {"weight": 16, "weightUnit": "oz"},
"maximumPostageAmount": 10.25,
"hazmatClassification": 0
}'
# After explicit approval, resend the same body with confirmationToken from
# the preview and an Idempotency-Key header unique to this purchase.
# See the SDK example above; retain that key for retries.
# Track a shipment
curl https://gateway.flexops.io/api/shipping/track/9400111899223456789012 \
-H "X-API-Key: live_..."
# Cancel (void) a label
curl -X DELETE "https://gateway.flexops.io/api/shipping/labels/{labelId}?carrierCode=USPS" \
-H "X-API-Key: live_..."
Use a test_... key instead of live_... to hit the sandbox environment; mock carriers respond, no real charges, no real labels.
Resources
| Resource | Methods | Description |
|---|---|---|
client.auth |
3 | Login, token refresh, logout |
client.workspaces |
6 | Workspaces you belong to, membership |
client.shipping |
15 | Rate shopping, labels, tracking, address validation, batch |
client.carriers |
62 | USPS, UPS, FedEx and DHL via the legacy proxy (writes deprecated) |
client.webhooks |
8 | Subscription CRUD, test events, signature verification |
client.wallet |
1 | Balance |
client.insurance |
5 | Quotes, policies, claims |
client.returns |
5 | RMA creation, labels, receiving |
client.analytics |
15 | Shipments, orders, carrier analytics |
client.orders |
10 | Order reads |
client.inventory |
3 | Warehouse inventory reads |
client.pickups |
4 | Carrier pickup scheduling |
client.scan_forms |
3 | USPS scan forms |
client.rules |
7 | Shipping automation rules |
client.carbon_offsets |
7 | Carbon offset quotes, purchases, settings |
client.hs_codes |
3 | HS code search and landed cost |
client.recurring_shipments |
8 | Recurring shipment schedules |
client.report_schedules |
6 | Scheduled reports |
client.email_templates |
5 | Shipment notification email templates |
API keys, wallet funding and account management are portal features and are not part of the SDK.
Configuration
client = FlexOps(
base_url="https://gateway.flexops.io", # API base URL
api_key="live_...", # API key auth
workspace_id="your-workspace-id", # Default workspace
timeout=30.0, # Request timeout (seconds)
headers={"X-Custom": "value"}, # Custom headers
retry={ # Retry configuration
"max_retries": 3,
"base_delay": 1.0,
},
)
Requirements
- Python 3.10+
requests>= 2.31pydantic>= 2.0
License
MIT © FlexOps, LLC. See LICENSE for full text.
Metadata
Release files for flexops 3.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| flexops-3.2.0.tar.gz | 57.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| flexops-3.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 106.3 kB
Release files / flexops-3.2.0.tar.gz
| Download URL | flexops-3.2.0.tar.gz |
|---|---|
| Size | 57.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e9f8d2f2be4bbd8b3db3c1620e6f3c3a13369580b4af9d57d02e7104c0a7c7c7
|
|
BLAKE2b-256 checksum How to use checksums |
6ca4351986854619a557b2b9874cbc42841cb9e1eea52076bab4989abeb7edf4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / flexops-3.2.0-py3-none-any.whl
| Download URL | flexops-3.2.0-py3-none-any.whl |
|---|---|
| Size | 49.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
33365acd6488012d1c6d45a0251fe91be2663eac2ae9d7e378c894cf25e99b95
|
|
BLAKE2b-256 checksum How to use checksums |
db1ce92b49c952d37e3f1c743d32533e14caf54bfcad6993d41baf7858923190
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for flexops 3.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for flexops-3.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.