Ship parcels via any configured carrier — MyParcel, Sendcloud, DHL Parcel NL, PostNL, DPD, UPS, FedEx. Auto-selects the first carrier whose credentials are configured, or pass --carrier <name> to override. Verbs: ship, label, track, list, carriers.
Scanned 5/27/2026
Install via CLI
openskills install Lifecycle-Innovations-Limited/claude-ops---
name: ops-package
description: "Ship parcels via any configured carrier — MyParcel, Sendcloud, DHL Parcel NL, PostNL, DPD, UPS, FedEx. Auto-selects the first carrier whose credentials are configured, or pass --carrier <name> to override. Verbs: ship, label, track, list, carriers."
argument-hint: "[--carrier <name>] <ship|label|track|list|carriers> [args...]"
allowed-tools:
- Bash
- Read
- AskUserQuestion
effort: low
maxTurns: 15
disallowedTools:
- Edit
- Write
- NotebookEdit
---
# OPS ► PACKAGE — multi-carrier shipping
One skill, seven carriers. The router picks the carrier automatically based on which credentials are configured.
## Which carrier do I get?
| Condition | Selected carrier |
| -------------------------------------------------------------------- | ------------------------- |
| `--carrier <name>` flag passed | That carrier (validated) |
| Exactly one carrier has credentials | That carrier |
| Multiple carriers have credentials | First match in this order |
| No carrier has credentials | Exit 2 with setup help |
**Preference order:** `myparcel` → `sendcloud` → `dhl` → `postnl` → `dpd` → `ups` → `fedex`.
## Credential resolution
Each carrier resolves its credential(s) from, in order:
1. Environment variable(s) listed below.
2. `preferences.json` key (lowercase of the env var name) at `${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json`.
3. Doppler: `doppler secrets get <NAME> --plain`.
| Carrier | Env vars required | Docs |
| ---------- | ---------------------------------------------------------------------- | --------------------------------------------------- |
| MyParcel | `MYPARCEL_API_KEY` | https://developer.myparcel.nl/api-reference/ |
| Sendcloud | `SENDCLOUD_PUBLIC_KEY` + `SENDCLOUD_PRIVATE_KEY` | https://api.sendcloud.dev/docs/ |
| DHL NL | `DHL_PARCEL_USER_ID` + `DHL_PARCEL_KEY` | https://api-gw.dhlparcel.nl/docs |
| PostNL | `POSTNL_API_KEY` + `POSTNL_CUSTOMER_CODE` + `POSTNL_CUSTOMER_NUMBER` | https://developer.postnl.nl/ |
| DPD | `DPD_DELIS_ID` + `DPD_PASSWORD` | https://esolutions.dpd.com/ |
| UPS | `UPS_CLIENT_ID` + `UPS_CLIENT_SECRET` + `UPS_SHIPPER_NUMBER` | https://developer.ups.com/api/reference/shipping |
| FedEx | `FEDEX_CLIENT_ID` + `FEDEX_CLIENT_SECRET` + `FEDEX_ACCOUNT_NUMBER` | https://developer.fedex.com/api/en-us/catalog/ |
## Routing
All API calls are delegated to `${CLAUDE_PLUGIN_ROOT}/skills/ops-package/ops-package.sh`. Do not re-implement curl logic inline — pass args through.
| First token | Action |
| ----------- | ------------------------------------------------------------------ |
| `carriers` | Show configured vs unconfigured carriers |
| `ship` | Create a shipment |
| `label` | Download + open label PDF |
| `track` | Show status + tracking barcode |
| `list` | List last 10 shipments (MyParcel / Sendcloud; others unsupported) |
| (empty) | Show usage |
## ship
Required: `--to "<address>"`. Address format:
```
"Person / Company, Street 12A, 1011AB Amsterdam, NL"
```
- `/ Company` segment is optional.
- Postcode accepts with/without space; NL postcodes are normalised to uppercase without space.
- Country defaults to NL; common names (Netherlands, Belgium, Germany, France, UK, USA) map to ISO codes.
Flags (not every carrier honours every flag — unsupported flags are safely ignored per adapter):
- `--from "<address>"` override sender; default is the account's configured sender.
- `--weight <grams>` integer grams.
- `--package-type 1|2|3` 1=parcel (default), 2=mailbox, 3=letter (MyParcel only).
- `--signature` require signature on delivery.
- `--insurance <EUR>` integer EUR; 0 disables (MyParcel only).
- `--description "<text>"` label reference (carriers differ; ~45 char max).
- `--pickup` request home pickup at sender (MyParcel only).
### Invocation
```bash
${CLAUDE_PLUGIN_ROOT}/skills/ops-package/ops-package.sh \
[--carrier myparcel|sendcloud|dhl|postnl|dpd|ups|fedex] \
ship --to "$TO" [--from "$FROM"] [--weight "$W"] \
[--signature] [--insurance "$INS"] [--description "$DESC"] \
[--package-type "$TYPE"] [--pickup]
```
Returns:
```json
{"carrier": "<name>", "shipment_id": "<id>", "response": { /* raw carrier JSON */ }}
```
Summarise to the user as:
```
Shipment created — <carrier> #<id>
Next: /ops:ops-package label <id> to download the PDF.
```
Offer via `AskUserQuestion` (≤4 options):
- `[Download label now]` — call `label <id>` immediately
- `[Track it]` — call `track <id>`
- `[Ship another]`
- `[Done]`
## label `<shipment-id>`
```bash
${CLAUDE_PLUGIN_ROOT}/skills/ops-package/ops-package.sh label "$ID"
```
Behaviour per carrier:
- **MyParcel**: PDF saved to `/tmp/myparcel_label_<id>.pdf` and opened on macOS. If unpaid, returns `{"status":"payment_required","payment_url":"..."}` and opens the MultiSafepay URL.
- **Sendcloud**: Fetches the CDN PDF URL (v2 `/labels/<id>`) and saves locally.
- **DHL NL**: Fetches `/labels/<id>?format=PDF&printerType=A4`.
- **DPD**: Fetches `/v1/parcellabelnumber/<id>?paperFormat=A4`.
- **PostNL / UPS / FedEx**: PDF is returned inline with the `ship` call and cached to `/tmp/<carrier>_label_<id>.pdf`. Calling `label` re-opens that cached file. If the cache is gone, re-run `ship` (none of these carriers expose a stable label-recovery endpoint here).
## track `<shipment-id>`
Returns a normalised object across all carriers:
```json
{"carrier":"<n>","id":"<id>","status":"<s>","barcode":"<b>","tracking_url":"<u>","recipient":<o>,"updated":"<ts>"}
```
## list
`MyParcel` returns the last 10 shipments. `Sendcloud` returns the last 10 parcels. All other carriers return `{"shipments":[],"note":"..."}` because their APIs don't expose a customer-facing list endpoint — track by id instead.
## carriers
Prints which carriers are configured (`✓`) vs which are missing credentials (`·`). Use this when deciding which `--carrier` to pass.
## Error handling
- **No carrier configured** → script exits 2 with a checklist of envs. Surface it verbatim, then via `AskUserQuestion`:
`[Paste key now]` `[Open carriers doc]` `[Skip — configure later]`
On "Paste key now", collect via `AskUserQuestion` free-text and write to `preferences.json`.
- **4xx from carrier** → dump the JSON error body and stop. Do not retry silently.
- **Address parser looks wrong** → re-prompt the user with the parsed breakdown and ask for corrections before POSTing.
## Verification status
| Carrier | Status |
| ---------- | ------------------------------------------------------------------ |
| MyParcel | Verified against api.myparcel.nl v1.1 (same working reference) |
| Sendcloud | Verified request/response shapes against Sendcloud Panel API v3 |
| DHL NL | UNVERIFIED — modelled on My DHL Parcel Swagger, needs live account |
| PostNL | UNVERIFIED — modelled on Send API v2.2 docs, needs live account |
| DPD | UNVERIFIED — modelled on DPD eSolutions REST, needs live account |
| UPS | UNVERIFIED — modelled on UPS v2403 Ship/Track REST, needs account |
| FedEx | UNVERIFIED — modelled on FedEx Ship v1 + Track v1 REST |
Unverified adapters are tagged with `# UNVERIFIED - pending live test with account` in the source. Payloads match the documented contract; adjustments may be needed when tested against live accounts.
No comments yet. Be the first to comment!