Generate an SDK with custom code using the Postman CLI

View as Markdown

This guide explains how to use the Postman CLI to generate an SDK from a Postman Collection, add custom code to the generated SDK, and regenerate the SDK while preserving your custom edits.

The SDK generator supports multiple programming languages, including TypeScript, Python, Java, Kotlin, C#, Go, PHP, Ruby, and Rust. The examples in this guide use generic syntax that can be adapted to any supported language.

Use cases for custom code include adding utility methods, custom logging, custom READMEs, or any additional logic that you want to maintain across SDK regenerations.

The postman sdk generate command generates a client SDK from a Postman Collection or API specification. Change tracking is included by default and enables the CLI to detect your local edits and merge them with the newly generated code on subsequent runs. The CLI uploads your local edits as custom code before triggering a new build.

Custom code SDKs are generated with the same structure as regular SDKs, but with additional handling to preserve user edits:

  • Customizations are applied after code formatting to prevent user-added custom code from causing the code formatter to fail, thereby allowing users to define their own formatting and SDK layout.

  • Developers’ edits to files are intended to persist, treating the local file system like a GitHub repository. Updates from the SDK generator are applied alongside user changes, and any conflicts are marked for users to resolve.

These changes don’t impact the size of the SDK because the last build artifact is retrieved from the database.

Follow the steps below to generate an SDK with custom code preserved across regenerations.

Step 1: Generate the initial SDK

The following command generates an SDK from a Postman Collection. Replace <language> with your preferred --language / -l value: typescript, python, java, kotlin, csharp (C#), go, php, ruby, or rust.

$postman sdk generate 12345678-abcd-1234-1234-123456789abc -l <language>

This example uses TypeScript (-l typescript):

$postman sdk generate 12345678-abcd-1234-1234-123456789abc -l typescript

The CLI output shows the progress of the build, including any warnings or issues encountered:

Identifying ID type (collection or specification)...
Identified as: collection
Starting SDK generation...
Collection ID: 12345678-abcd-1234-1234-123456789abc
Build type: Postman Cloud (updates stored SDK)
Language(s): <language>
Output: ./sdks
Creating SDK build...
✓ Build created successfully
Build ID: 7836
Status: RECEIVED
Languages: <language>
Watching build progress...
Status: IN_PROGRESS
Status: IN_PROGRESS
...
Status: SUCCESS
✓ Build completed successfully!
Duration: 11s

After the build completes, the CLI prints a build log summary listing any warnings or issues encountered:

Build Log Summary
1 issue(s) across 1 endpoint(s):
1) "Cloud platforms/New Request"
WARNING: No URL was provided for the request field of this item
in the postman collection. The SDK will not include this item.
If you want to include this item, please provide a URL.
Notes: 4 note(s) (use --log-level info to expand)

Endpoints in the collection that do not have a URL are excluded from the generated SDK. To include them, add a URL to the request in the collection before regenerating.

Once the build succeeds, the CLI downloads the generated SDK to the output directory:

Downloading SDKs...
✓ <language>: sdks/<language>
✓ All SDKs downloaded successfully!
Manifest written for <language>

The generated SDK is saved to ./sdks/<language>/ (for example, ./sdks/typescript/) and includes the following structure:

  • .sdk-gen/ — Internal SDK generation metadata
  • documentation/ — Auto-generated API documentation
  • examples/ — Usage examples
  • Dependencies — SDK dependency artifacts (for example, node_modules/ for TypeScript or requirements.txt for Python)
  • Build files — Build and utility scripts
  • Source files — Generated source files, organized into service modules (for example, services/payment/, services/users/, services/billing/)
  • Configuration files — Example environment configuration (for example, .env.example)

Step 2: Add custom code to the SDK

After the initial generation, you can edit the generated SDK source files to add custom logic. These edits are preserved on subsequent regenerations when you rerun the SDK generation command.

The following examples use TypeScript-like pseudocode to illustrate where custom code can be added in a generated SDK. Adapt the syntax, type system, and idioms to your chosen language’s conventions.

To add a custom logging method to a service class, use the following TypeScript-like example based on a PaymentService file such as sdks/<language>/src/services/payment/payment-service.<ext>, then adapt it to your target language:

// Add a custom logging method to the class
_customLog(input) {
console.log(input);
}

You can then call this method from any service method within the class:

setAddCreditCardForUserConfig(config) {
this._customLog('hello world');
this.addCreditCardForUserConfig = config;
return this;
}

You can also customize the auto-generated README file to add branding, company-specific information, or additional usage examples. The SDK generates a basic README at sdks/<language>/README.md, which you can modify.

Open the README file and add your custom content. For example, you can add a company logo, installation instructions, company-specific authentication details, enterprise support information, and relevant links.

These customizations will be preserved when you regenerate the SDK.

You can add utility methods to transform API responses or prepare request data. This example shows TypeScript modifications to sdks/<language>/src/services/payment/payment-service.<ext>:

1// Define custom types for transformed data
2interface PaymentSummary {
3 id: string;
4 amount: string;
5 status: string;
6 createdAt: Date;
7 isCompleted: boolean;
8 displayName: string;
9}
10
11interface RawPayment {
12 payment_id: string;
13 amount_units: number;
14 status: string;
15 created_timestamp: string | number | Date;
16}
17
18class PaymentService {
19 transformPaymentData(rawPayment: RawPayment): PaymentSummary {
20 return {
21 id: rawPayment.payment_id,
22 amount: this.formatCurrency(rawPayment.amount_units),
23 status: rawPayment.status.toUpperCase(),
24 createdAt: new Date(rawPayment.created_timestamp),
25 // Add custom calculated fields
26 isCompleted: rawPayment.status === 'completed',
27 displayName: `Payment ${rawPayment.payment_id.slice(-8)}`
28 };
29 }
30
31 formatCurrency(units: number): string {
32 return new Intl.NumberFormat('en-US', {
33 style: 'currency',
34 currency: 'USD'
35 }).format(units / 100);
36 }
37
38 async getPayment(params: { paymentId: string }): Promise<RawPayment> {
39 return {} as RawPayment;
40 }
41
42 async getPaymentWithFormatting(paymentId: string): Promise<PaymentSummary> {
43 const rawPayment = await this.getPayment({ paymentId });
44 return this.transformPaymentData(rawPayment);
45 }
46}

This adds data transformation capabilities that format API responses into more user-friendly structures, including currency formatting and computed fields.

You can add business rule validation and custom logic to service methods. This TypeScript example shows modifications to sdks/typescript/src/services/payment/payment-service.ts:

1// Define custom types for validation
2interface ValidationResult {
3 isValid: boolean;
4 errors: string[];
5}
6
7interface CreatePaymentRequest {
8 amount: number; // Amount in smallest currency unit (e.g., cents for USD)
9 currency: string;
10 customerId: string;
11 // Additional payment fields as needed
12}
13
14export class PaymentService extends BaseService {
15 // Add custom validation utilities
16 validatePaymentAmount(amount: number, currency: string = 'USD'): ValidationResult {
17 const errors: string[] = [];
18
19 if (amount <= 0) {
20 errors.push('Payment amount must be greater than zero');
21 }
22
23 if (currency === 'USD' && amount >= 1000000) {
24 errors.push('USD payments cannot exceed $10,000.00');
25 }
26
27 // Check if amount is a valid integer (since we're working in smallest currency units)
28 if (!Number.isInteger(amount)) {
29 errors.push('Payment amount must be in valid currency units (whole numbers)');
30 }
31
32 return {
33 isValid: errors.length === 0,
34 errors
35 };
36 }
37
38 // Enhanced payment method with validation
39 async createValidatedPayment(paymentData: CreatePaymentRequest): Promise<any> {
40 // Apply custom business rules
41 const validation = this.validatePaymentAmount(paymentData.amount, paymentData.currency);
42
43 if (!validation.isValid) {
44 throw new Error(`Payment validation failed: ${validation.errors.join(', ')}`);
45 }
46
47 // Add custom audit logging
48 console.log(`Creating payment: ${paymentData.amount} ${paymentData.currency} for customer ${paymentData.customerId.slice(-4)}`);
49
50 // Call the original SDK method
51 return this.createPayment(paymentData);
52 }
53}

This adds custom business rule validation and audit logging that wraps the generated SDK methods with additional logic specific to your application needs.

When implementing audit logging, be careful to avoid logging sensitive information such as full customer IDs, payment tokens, personal data, or API keys. In production environments, consider redacting or omitting these fields entirely, or use structured logging with appropriate data classification.

Step 3: Regenerate the SDK with custom code preserved

When you run postman sdk generate, the CLI automatically detects your local edits and handles the merge:

User edits detected for: <language>
Uploading modified files for 3-way merge...
<language>: 1 modified file(s)
+ src/services/payment/payment-service.<ext>
Custom code uploaded: custom-code/f9e967ad-d27d-4cfe-8d1f-f516b1dec40e.zip
Creating SDK build...
✓ Build created successfully
Build ID: 7837
Status: RECEIVED
Languages: <language>
Watching build progress...
Status: IN_PROGRESS
...
Status: SUCCESS
✓ Build completed successfully!
Duration: 12s

If the SDK output directory already exists, the CLI will prompt before overwriting:

The following SDK directories already exist: <language>
✓ Do you want to overwrite them? … yes

After confirming, the SDK is downloaded again with your custom code merged in:

Downloading SDKs...
✓ <language>: sdks/<language>
✓ All SDKs downloaded successfully!
Manifest written for <language>

Step 4: Resolve merge conflicts (if any)

When both you and the generator change the same lines in a file, the CLI handles the conflict based on the conflictStrategy setting. With the default mark strategy, the CLI adds conflict markers to the file, allowing you to manually resolve the conflicts. The conflict markers look like this:

1<<<<<<< user
2// Your version of the code
3export function getPet(id: string): Pet {
4 return this.cache.get(id) ?? this.fetchPet(id);
5}
6=======
7// Generator's version of the code
8export function getPet(id: string): Promise<Pet> {
9 return this.httpClient.get(`/pets/${id}`);
10}
11>>>>>>> generated

In this example, the user modified the getPet method to first check a local cache before making an API call, while the generator updated the method to return a Promise that resolves with the API response. The conflict markers indicate that both versions changed the same lines of code, and manual resolution is needed to determine how to merge these changes.

The CLI reports files with conflicts after download:

Warning: Merge completed with 1 conflict(s) in typescript:
- src/services/pets/pets-service.ts
Resolve conflicts manually (look for <<<<<<< user / >>>>>>> generated markers).

You can then open the file, review the conflicting changes, and edit the code to resolve the conflict. After resolving, save the file and continue using the SDK as normal.

How change detection works

Each generated SDK has a .manifest.json in its root. When change tracking is enabled, this file includes a fileHashes field — a map of every generated file to its SHA-256 hash:

1{
2 "sdkVersion": "1.0.4",
3 "date": "2026-04-21T...",
4 "config": { ... },
5 "files": ["src/index.ts", "package.json", ...],
6 "fileHashes": {
7 "src/index.ts": "a1b2c3d4...",
8 "src/services/pets/pets-service.ts": "e5f6a7b8...",
9 "package.json": "c9d0e1f2..."
10 }
11}

On the next regeneration, the CLI does the following:

  1. Reads fileHashes from .manifest.json.

  2. Computes the current hash of each file on disk.

  3. Compares the files. Any mismatch means the file was edited.

  4. Scans for user-added files not in the manifest.

  5. Checks for files in the manifest that no longer exist on disk (user-deleted).

Only the changed files are uploaded. The generator uses the previous build’s output as the merge base for a three-way merge.

When you regenerate, the CLI uploads a zip file with the following structure:

customizations/{language}/
src/services/pets/pets-service.ts # Modified file
added/{language}/
src/utils/my-helper.ts # New file you created
meta/{language}/
.manifest.json # Manifest with file hashes
deleted.json # ["src/old-file.ts"] — files you deleted

Only the changed or added files and metadata are uploaded and not the entire SDK.

The following directories are excluded from change detection and hashing to avoid tracking dependencies and build output.

  • Dependencies and packages:

    • node_modules
    • vendor
    • .venv
  • Build outputs:

    • dist
    • build
    • target
    • bin
    • obj
  • Build tools and cache:

    • __pycache__
    • .gradle
  • Internal and version control:

    • .sdk-gen
    • .git

How change tracking works

The customCode section of the SDK configuration controls change-tracking and merge behavior. The CLI terminal output shows the effective configuration being applied. This example corresponds to running with --conflict-strategy mark, which adds conflict markers to files when both the user and generator modify the same lines:

1{
2 "sdk": {
3 "customCode": {
4 "trackChanges": true,
5 "conflictStrategy": "mark",
6 "noMerge": false
7 }
8 }
9}

The following is priority order for these settings, from highest to lowest:

  1. CLI flags (for example, trackChanges is set to true and conflictStrategy defaults to "mark").
    • If you run the command with --no-track-changes, change tracking will be turned off for that run. This means the generator will perform a clean regeneration that overwrites all files, and no merge will be attempted to preserve user edits.
    • If you run the command with --conflict-strategy ours, the conflict strategy will be set to "ours" for that run, and the generator will keep your changes and discard the generator’s changes.
  2. customCode values set in the config file.
    • Setting "trackChanges": false is equivalent to --no-track-changes.
    • Setting "noMerge": true is equivalent to --no-merge.

This means CLI flags always override values in the config file.

Limitations

Change tracking requires the generator to support writing fileHashes to .manifest.json. This is enabled by default on the server side.

The three-way merge requires a previous build artifact to exist in the cloud. If the previous build was cleaned up, customizations are applied as-is (no merge, just overlay).

Binary files are skipped during conflict scanning.