postman-sync

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: GPL-3.0

README

postman-sync

A Go CLI for managing Postman workspaces:

  • openapi-sync — sync a Postman collection from an OpenAPI spec without overwriting team-specific customizations (auth, scripts, saved responses, etc.)
  • export — export collections and environments to local JSON files
  • import — import collections and environments from local JSON files back into Postman

Inspired by dmiska25/postman_sync.py, rewritten in Go with a YAML-driven configuration.

How openapi-sync works

  1. Loads an OpenAPI spec (JSON or YAML) from a file or directory; when a directory is given, all external $ref references are resolved and bundled into a single document
  2. Optionally sanitizes enum values to reduce diff noise
  3. Imports the spec into Postman via the API (creates a temporary collection)
  4. Downloads the generated collection, then deletes the temporary one
  5. Transforms the collection: flattens single-request folders, sorts alphabetically, injects configured headers/auth/scripts, sets the base URL, and adds doc links
  6. Merges the transformed collection into an existing Postman collection of the same name, preserving auth, pre-request scripts, test scripts, saved responses, and (by default) header/param values and body edits from previous manual edits
  7. Endpoints removed from the spec are removed from the collection (spec is source of truth)

Prerequisites

Installation

go install github.com/akyrey/postman-sync@latest

Or build from source:

git clone https://github.com/akyrey/postman-sync.git
cd postman-sync
make build

Configuration

Copy the example config and fill in your values:

cp postman-sync.example.yaml postman-sync.yaml

Secrets can be set via environment variables instead of the config file:

export POSTMAN_API_KEY=your-api-key
export POSTMAN_WORKSPACE_ID=your-workspace-id
Config structure

The config file has a global section and three optional command-specific sections:

# Global (required)
postman_api_key: ""    # or POSTMAN_API_KEY env var
workspace_id: ""       # or POSTMAN_WORKSPACE_ID env var

# openapi-sync command settings
openapi:
  path: "./openapi.json"
  base_url: "{{baseUrl}}"
  sanitize_enums: true
  # ... auth, scripts, headers, folder_overrides, doc_links

# export command settings
export:
  output_dir: "./postman-export"
  collections: ["all"]
  environments: ["all"]
  pretty: true

# import command settings
import:
  input_dir: "./postman-export"
  collections:
    names: ["all"]
    strategy: "overwrite"
  environments:
    names: ["all"]
Global fields
Field Required Description
postman_api_key Yes Postman API key (or POSTMAN_API_KEY env var)
workspace_id Yes Postman workspace ID (or POSTMAN_WORKSPACE_ID env var)
openapi section
Field Default Description
openapi.path ./openapi.json Path to the OpenAPI spec file (JSON or YAML) or a directory containing a multi-file spec
openapi.root_file — Root spec filename when openapi.path is a directory (auto-detected if omitted)
openapi.base_url {{baseUrl}} Base URL for all requests (Postman variable recommended)
openapi.sanitize_enums true Replace enum values with <enum> to reduce diff noise
openapi.doc_links.base_url — Base URL for documentation links (omit to disable)
openapi.common_headers [] Headers injected into every request (value and disabled state re-applied on every sync)
openapi.common_query_params [] Query params injected into every request (value and disabled state re-applied on every sync)
openapi.import_options {enableOptionalParameters: false} Options forwarded to Postman's OpenAPI importer; optional params/headers are generated disabled by default
openapi.state_dir .postman-sync Local sync state: per-collection baselines used for three-way merges, and backups
openapi.backup.enabled true Back up the existing collection before updating it. --no-backup disables it for one run
openapi.backup.keep 10 Backups retained per collection (0 keeps all)
openapi.preserve_request_data true Keep header/param values, enabled state and body edits of existing requests (see merge behavior). --no-preserve disables it for one run
openapi.auth — Collection-level authentication
openapi.auth.propagation — Set to "inherit" to clear auth on all folders/requests so they inherit from the collection
openapi.scripts — Collection-level pre-request and test scripts
openapi.folder_overrides {} Per-folder (tag) auth and script overrides
export section
Field Default Description
export.output_dir ./postman-export Directory for exported files
export.collections [] Names to export, or ["all"]. Omit to skip.
export.environments [] Names to export, or ["all"]. Omit to skip.
export.pretty true Pretty-print JSON (recommended for git diffs)
import section
Field Default Description
import.input_dir ./postman-export Directory to read files from
import.collections.names — Names to import, or ["all"]
import.collections.strategy overwrite overwrite or merge (see below)
import.environments.names — Names to import, or ["all"]
import.environments.strategy overwrite overwrite only (environments have no merge logic)

Usage

# Sync an OpenAPI spec into Postman
./bin/postman-sync openapi-sync
./bin/postman-sync openapi-sync --openapi-path ./api.yaml          # single file
./bin/postman-sync openapi-sync --openapi-path ./specs/            # directory (auto-detect root)
./bin/postman-sync openapi-sync --openapi-path ./specs/ --openapi-root-file main.yaml  # explicit root
./bin/postman-sync openapi-sync --config /path/to/config.yaml
./bin/postman-sync openapi-sync --no-preserve                      # regenerate request data from the spec
./bin/postman-sync openapi-sync --dry-run                          # show what would change, write nothing
./bin/postman-sync openapi-sync --no-backup                        # skip the pre-update backup

# Export collections and environments to disk
./bin/postman-sync export
./bin/postman-sync export --collections all --environments all
./bin/postman-sync export --collections "My API,Other API" --output-dir ./backup

# Import files back into Postman
./bin/postman-sync import
./bin/postman-sync import --collections all --strategy overwrite
./bin/postman-sync import --collections all --strategy merge --environments all
./bin/postman-sync import --input-dir ./backup

# Print version
./bin/postman-sync --version

# Using env vars for secrets
POSTMAN_API_KEY=xxx POSTMAN_WORKSPACE_ID=yyy ./bin/postman-sync openapi-sync

All flags override the corresponding config file values for that run. The --config flag is global and applies to all commands.

Export file layout

postman-export/
  collections/
    My API.json           # full Postman CollectionWrapper JSON
    Other API.json
  environments/
    Production.json       # full Postman EnvironmentWrapper JSON
    Staging.json

Files are the complete Postman API envelope, so they round-trip cleanly through import. Pretty-printed JSON by default — friendly for git diffs.

openapi-sync merge behavior

When a collection with the same name already exists in the workspace:

What Source
Which endpoints exist New spec (removed endpoints are dropped)
Endpoint order Alphabetical (from transform step)
Request URL structure, method, description New spec
Header / query param / path variable / form field descriptions New spec (carries (Required) and type info)
Header, query param, form field values and enabled state Edited in Postman: preserved. Untouched: follow the spec ¹
Headers, query params, form fields added by hand Preserved ²
Headers, query params, form fields deleted by hand Stay deleted ²
Path variable values Edited: preserved. Untouched: follow the spec
Raw JSON body Merged field by field with the same rules ³
Other body Edited: preserved (unless the body mode changed in the spec). Untouched: follow the spec
common_headers / common_query_params Config file (re-applied after the merge)
Auth (item-level and folder-level) Preserved from existing collection (unless auth.propagation: inherit)
Pre-request and test scripts Preserved from existing collection
Saved example responses Preserved from existing collection
Collection-level auth Config file (if set), otherwise preserved from existing
Collection-level scripts Config file (if set), otherwise preserved from existing

On first sync (no existing collection), config-defined auth/scripts are applied as defaults.

¹ Rows about request data apply when preserve_request_data is true (default). With false or --no-preserve, request data comes entirely from the spec.

Baselines (three-way merge)

After every successful sync, the collection generated from the spec is saved as a baseline in <state_dir>/baselines/<collection>.json (state_dir defaults to .postman-sync, relative to the working directory). The next sync compares three versions of each request: the baseline, the current Postman collection, and the newly generated one:

  • a value or enabled state still equal to the baseline was never touched, so it follows the spec (e.g. a placeholder that changed from <string> to <long>);
  • a value that differs from the baseline was edited in Postman and is kept;
  • an entry in Postman but not in the baseline was added by hand and is kept; an entry in the baseline but no longer in Postman was deleted by hand and stays deleted.

Requests without a baseline (first sync, new machine, deleted state dir) fall back to a two-way merge: matched entries keep their Postman value and state, and an entry counts as hand-added when it has no description (Postman's importer always copies the spec description). Keep state_dir out of version control (add it to .gitignore) unless the whole team syncs from the same checkout.

² With a baseline. Without one, hand-added entries are detected by their missing description and hand-deleted entries are re-added.

³ An untouched body is replaced by the spec's body. If an edited raw body is not valid JSON (e.g. it contains unquoted {{variables}}), it is kept as is. When a field changes kind in the spec (e.g. string → object) the spec value wins. Array elements are kept; object elements gain/lose fields based on the spec's first element. Without a baseline, fields no longer in the spec are always dropped.

Dry run

--dry-run runs the whole pipeline but writes nothing: no collection update/creation and no baseline. Postman still converts the spec through a temporary collection, which is deleted as usual. The output lists every change the sync would make:

~ (collection)
    auth: bearer → oauth2
+ Orders/Create order
~ Orders/List orders
    url: "{{baseUrl}}/orders?page=1" → "{{baseUrl}}/orders"
    query page: disabled false → true
    header x-skp-locale: description changed
- Orders/Legacy export
Summary: 1 added, 1 removed, 2 modified.

+ added, - removed, ~ modified. Added or removed folders are reported once, with their request count. Combine with --no-preserve to preview a full reset.

Backups

Before updating an existing collection, openapi-sync saves it as fetched from Postman to:

<state_dir>/backups/<UTC timestamp>/collections/<collection name>.json

The layout is the same as export's, so restoring a backup is a regular import:

./bin/postman-sync import --input-dir .postman-sync/backups/20261009T150405Z \
  --collections all --strategy overwrite

Only the openapi.backup.keep most recent backups of each collection are retained (0 keeps all); pruning only deletes that collection's backup files and the directories they leave empty. If the backup cannot be written the sync stops before changing anything; use --no-backup to skip it. Dry runs never write backups.

Multi-file OpenAPI specs

When your OpenAPI spec is split across multiple files using $ref references to local files, point openapi.path at the directory (or the root file) and the tool will bundle everything into a single document before sending it to Postman.

# postman-sync.yaml
openapi:
  path: "./specs"           # directory containing openapi.yaml and referenced files
  # root_file: "main.yaml"  # optional — only needed when auto-detection is ambiguous

Or via CLI flags:

./bin/postman-sync openapi-sync --openapi-path ./specs/
./bin/postman-sync openapi-sync --openapi-path ./specs/ --openapi-root-file main.yaml

Root file auto-detection (when path is a directory and root_file is not set):

  1. Looks for well-known names in order: openapi.yaml, openapi.yml, openapi.json, swagger.yaml, swagger.yml, swagger.json
  2. If none found, uses the single .yaml/.yml/.json file at the top level of the directory
  3. Returns an error if zero or multiple files are found — use root_file to resolve ambiguity

Note: All $ref references must point to local files. Remote URL references (http://) are not resolved.

import merge strategy

When import.collections.strategy: merge, the import command fetches the existing collection from Postman and runs the same MergeItems logic used by openapi-sync:

  • Auth, scripts, and saved responses are preserved from the existing Postman collection.
  • Request definitions (URL, method, body, headers) come from the imported file.
  • Endpoints not in the file are dropped.

Use overwrite to replace the entire collection without merging.

Auth propagation (openapi-sync)

By default, Postman inherits auth from the parent for any item that has no explicit auth. However, after merge cycles, individual folders and requests often accumulate their own auth objects — preventing them from picking up changes to the collection-level auth.

Setting openapi.auth.propagation: "inherit" makes the tool clear auth on every folder and leaf request so they all inherit from the collection. Exceptions:

  • Items with auth.type: "noauth" are left untouched.
  • Folders listed in folder_overrides keep their explicit auth (their children are still processed).
openapi:
  auth:
    type: "oauth2"
    attributes:
      - key: "accessToken"
        value: "{{oauth2_access_token}}"
        type: "string"
      # ... other oauth2 attributes
    propagation: "inherit"

Auth types

openapi.auth.type supports: apikey, basic, bearer, oauth1, oauth2, digest, ntlm, hawk, awsv4, edgegrid, noauth.

OAuth2 configuration

OAuth2 has two sections in Postman: the current token (sent with requests) and the new token (OAuth2 flow config). Both are set via attributes key/value pairs.

openapi:
  auth:
    type: "oauth2"
    attributes:
      # --- Current Token ---
      - key: "accessToken"
        value: "{{oauth2_access_token}}"
        type: "string"
      - key: "tokenType"
        value: "Bearer"
        type: "string"
      - key: "addTokenTo"
        value: "header"
        type: "string"
      - key: "headerPrefix"
        value: "Bearer"
        type: "string"
      # --- New Token (OAuth2 flow) ---
      - key: "grant_type"
        value: "client_credentials"
        type: "string"
      - key: "accessTokenUrl"
        value: "https://auth.example.com/oauth/token"
        type: "string"
      - key: "clientId"
        value: "{{oauth2_client_id}}"
        type: "string"
      - key: "clientSecret"
        value: "{{oauth2_client_secret}}"
        type: "string"
      - key: "scope"
        value: "openid profile"
        type: "string"

All attribute values must be strings (including booleans like "false").

Project structure

postman-sync/
├── cmd/postman-sync/
│   ├── main.go             # Cobra root command, global flags, shared helpers
│   ├── openapi_sync.go     # openapi-sync subcommand + pipeline
│   ├── export.go           # export subcommand + pipeline
│   ├── import_cmd.go       # import subcommand + pipeline
│   └── fileutil.go         # Shared file I/O helpers
├── internal/
│   ├── config/
│   │   └── config.go       # Config structs, loading, per-command validation
│   ├── openapi/
│   │   └── loader.go       # Load OpenAPI spec (JSON/YAML) + enum sanitization
│   └── postman/
│       ├── types.go        # Postman Collection v2.1 + Environment types
│       ├── types_json.go   # JSON (un)marshalling keeping unmodelled fields (extra.go)
│       ├── client.go       # Postman API HTTP client (collections + environments)
│       ├── transform.go    # Collection transformers
│       ├── merge.go        # Name-based recursive merge
│       ├── merge_request.go  # Preserving (two/three-way) merge of headers/params/body
│       ├── jsonmerge.go    # Order-preserving raw JSON body merge
│       └── diff.go         # Collection diff used by --dry-run
├── postman-sync.example.yaml
├── Makefile
├── .goreleaser.yaml
├── go.mod
└── go.sum

Testing

make test         # run all tests
make test-race    # with race detector
make test-cover   # with coverage report

License

GNU General Public License

Directories

Path Synopsis
cmd
postman-sync command
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL