Skip to content

Scripting API

Full pm.* reference for pre-request and test scripts.

Scripts run in a sandboxed JavaScript VM by default. Existing requests can still use the legacy Tengo engine. Both engines expose the same pm.* surface for request mutation, response assertions, variables, environments, and logs.

Module imports, process access, and filesystem access are disabled; require resolves only the bundled stand-ins. Network access is disabled too, unless a request opts into pm.sendRequest. Execution is capped at 2 seconds by default — see Script timeout.

pm.request

MethodDescription
pm.request.urlCurrent request URL (string)
pm.request.methodHTTP method
pm.request.headers.get(name)Get request header
pm.request.headers.set(name, value)Set / override header
pm.request.headers.unset(name)Remove header
pm.request.params.get(name)Get query param
pm.request.params.set(name, value)Set query param
pm.request.set_url(url)Override URL before sending

Header and parameter writes are an upsert, matching Postman: the first row with that name takes the new value, further rows with the same name are dropped, and a name the request does not have is appended. A row the script never names is left exactly as it was written — including two rows that share a name, which is how a repeated query parameter (?tag=a&tag=b) or a multi-valued header survives a script untouched.

| pm.request.body.raw | Raw request body — readable and writable | | pm.request.body.mode | Body mode, in Postman’s names: raw, urlencoded, formdata, file, graphql, none | | pm.request.body.json() | Body parsed as JSON | | pm.request.body.update(value) | Replace the body; objects are stringified | | pm.request.body.urlencoded | Field list — only when the mode is urlencoded | | pm.request.body.formdata | Field list — only when the mode is formdata |

Rewriting and signing the body

A pre-request script owns the body it writes: the value it sets is what goes on the wire, and a test script sees the same value afterwards.

const payload = pm.request.body.json()
payload.timestamp = Date.now()
pm.request.body.update(JSON.stringify(payload))
// Sign what will actually be sent
pm.request.headers.set("X-Signature", pm.crypto.hmacSha256(pm.request.body.raw, pm.environment.get("secret")))

Two things to know:

  • A body written onto a request that has no body is sent anyway — Relay picks json or text from the content. The same applies to a binary request with no file chosen yet.
  • A binary body read from a file is left alone. The script never sees those bytes, so it cannot replace them.

Form and urlencoded bodies

Those two modes are sent from their fields, not from raw text, so they are edited as a list. Writing .raw on them changes nothing, and Relay says so in the script log instead of dropping the write silently.

// mode === "urlencoded"
pm.request.body.urlencoded.upsert({ key: "grant_type", value: "client_credentials" })
pm.request.body.urlencoded.add({ key: "scope", value: "read write" })
pm.request.body.urlencoded.remove("client_secret")
const fields = pm.request.body.urlencoded.toObject() // { grant_type: "...", scope: "..." }
MethodDescription
.get(key) / .one(key)The value / the whole field
.has(key) / .count()Presence / number of fields
.add(field)Append { key, value, disabled?, type? }
.upsert(field)Replace the field with that key, or append it
.remove(key | predicate)Drop matching fields
.each(fn) / .all()Iterate / read the whole list
.clear() / .toObject()Empty the body / read it as an object

A file field keeps its attachment when a script rewrites its value; type: "file" marks it in the list. pm.request.body.update({ mode: "urlencoded", urlencoded: [...] }) replaces every field at once.

In Tengo the same surface is pm.request.body, pm.request.body_type (Relay’s own names, not Postman’s), and pm.request.set_body(value). Tengo has no form-field API.

pm.response (test scripts only)

MethodDescription
pm.response.codeHTTP status code (int)
pm.response.statusStatus string, e.g. "200 OK"
pm.response.responseTimeDuration in milliseconds (alias: pm.response.time)
pm.response.sizeBody size in bytes
pm.response.body()Raw body as string
pm.response.json()Body parsed as JSON (map/array)
pm.response.headers.get(name)Get response header (case-insensitive)

Variable scopes

pm.variables, pm.globals, pm.environment, and pm.collectionVariables all expose the same four methods:

MethodDescription
.get(key)Read a variable
.set(key, value)Write a variable
.unset(key)Delete a variable
.clear()Clear all variables

They differ in which scope they touch:

ScopeReadsWrites
pm.variablesAll scopes, in precedence order: data row → environment → collection → sessionThe session scope
pm.globalsThe session scopeThe session scope
pm.environmentThe active environmentThe active environment
pm.collectionVariablesThe request’s collectionThe request’s collection, saved back to the collection after the send

Relay has a single runtime variable scope, so pm.globals and pm.variables.set write to the same place — they are aliases, not two separate stores.

pm.variables.replaceIn(text) fills every {{name}} in a string from the scopes above, plus {{$guid}} and the other dynamic variables — handy for signing the URL a request will actually hit. A name nothing defines is left as written.

Globals persist. They are saved with your data, survive a restart, and are shared across every workspace. Edit them under Environments → Globals in the sidebar; a value a script writes appears there after the send. In relay run they come from --globals / --global-var and can be written back with --export-globals.

pm.collectionVariables.set persists: after the request finishes, the value is written onto the collection’s variables (a Git-backed workspace records it in collection.yml). An existing row keeps its id, enabled state, secret flag, and description; only the value changes.

// Cache a value for every later request in the collection.
pm.collectionVariables.set("lastOrderId", pm.response.json().id)

pm.info

Read-only facts about the current run.

PropertyDescription
pm.info.requestNameThe request’s name
pm.info.eventName"prerequest" or "test"
pm.info.iterationCurrent iteration (1-based)
pm.info.iterationCountTotal iterations in this run

pm.cookies

Read-only access to the cookies the request’s jar would send for this URL. Domain, path, and Secure matching is already applied.

MethodDescription
pm.cookies.get(name)Cookie value, or undefined
pm.cookies.has(name)Whether the cookie is present
pm.cookies.names()Array of cookie names

pm.execution.skipRequest()

Called from a pre-request script, skips the send entirely. The request is reported as skipped rather than failed, so a conditional request does not fail a collection run or a relay run exit code. Calling it from a test script does nothing — the response already exists.

// Only hit the billing API when the environment opts in.
if (pm.environment.get("mode") !== "live") pm.execution.skipRequest()

pm.crypto

Hashing, HMAC, and encoding, for signing requests. Every digest function takes an optional encoding — "hex" (default), "base64", "base64url", or "latin1".

MethodDescription
pm.crypto.md5(data, enc?)MD5 digest
pm.crypto.sha1 / sha256 / sha384 / sha512(data, enc?)SHA digest
pm.crypto.hash(algorithm, data, enc?)Digest under a named algorithm
pm.crypto.hmacSha1 / hmacSha256 / hmacSha384 / hmacSha512(data, key, enc?)HMAC
pm.crypto.hmac(algorithm, data, key, enc?)HMAC under a named algorithm
pm.crypto.base64Encode(data) / base64Decode(data)Base64
pm.crypto.randomHex(bytes)Random hex string (default 16 bytes)
pm.crypto.uuid()Random v4 UUID

Supported algorithms: md5, sha1, sha256, sha384, sha512, sha512-256. An unknown name throws rather than signing with an empty digest.

// Sign the request the way a partner API expects.
const ts = String(Date.now())
const sig = pm.crypto.hmacSha256(pm.request.method + "\n" + ts, pm.environment.get("apiSecret"))
pm.request.headers.set("X-Timestamp", ts)
pm.request.headers.set("X-Signature", sig)

CryptoJS

A CryptoJS global is available for compatibility with scripts imported from Postman: MD5, SHA1, SHA256, SHA384, SHA512, the matching Hmac* functions, and enc.Hex / enc.Base64 / enc.Utf8. It covers the digest and encoding calls that appear in real collections, not the whole library — for new scripts prefer pm.crypto.

const sig = CryptoJS.HmacSHA256("payload", pm.environment.get("secret")).toString()
const b64 = CryptoJS.enc.Base64.stringify(CryptoJS.SHA256("payload"))

pm.sendRequest

Makes an HTTP call from a script — for fetching a token before the send, or chaining setup. Off by default: turn on Allow pm.sendRequest in the request’s Settings tab, or pass --allow-send-request to relay run. Without it, the call throws and explains how to enable it.

Both Postman’s callback form and a direct return value work; the call is synchronous, so the callback runs immediately rather than on a later tick.

// Callback form — what imported Postman scripts use.
pm.sendRequest({
method: "POST",
url: "https://auth.example.com/token",
header: { "Content-Type": "application/x-www-form-urlencoded" },
body: "grant_type=client_credentials",
}, (err, res) => {
if (err) throw err
pm.request.headers.set("Authorization", "Bearer " + res.json().access_token)
})
// Return-value form.
const res = pm.sendRequest("https://api.example.com/health")
pm.test("dependency is up", () => pm.expect(res.code).to.equal(200))

The response object mirrors pm.response: .code, .status, .responseTime, .size, .text(), .json(), .headers.get(name), and .to for assertions.

Limits and deliberate omissions:

  • 30 second timeout, 8 MB response cap, at most 5 redirects.
  • The call does not inherit the parent request’s auth, client certificate, or cookie jar, and does not run scripts of its own. It is a plain HTTP request — anything more would be a hidden second request carrying your credentials.
  • A non-2xx status is a normal response, not an error; only a transport failure produces one.

Script timeout

A script is capped at 2000 ms by default. Raise it per request with Script timeout in the Settings tab, or for a whole run with relay run --script-timeout. The ceiling is 60000 ms, so a runaway loop can never wedge a send or a CI job.

pm.iterationData

MethodDescription
.get(key)Read the current data-row value for key (read-only)

During a data-driven run — the Collection Runner with a data file, or relay run --data — pm.iterationData exposes the current row. It’s read-only; outside a data run every key is undefined.

pm.test("greets the row's user", () => {
const body = pm.response.json()
pm.expect(body.name).to.equal(pm.iterationData.get("name"))
})

Assertions

JavaScript:

pm.test("status is 200", () => pm.response.to.have.status(200))
pm.test("has id", () => pm.expect(pm.response.json()).to.have.property("id"))

Tengo:

body := pm.response.json()
pm.test("status is 200", pm.response.code == 200)
pm.test("has id", pm.expect(body).has_key("id"))

pm.test(name, fnOrResult) — register a named assertion. JavaScript accepts a callback or boolean. Tengo accepts a boolean expression.

pm.expect(value) — chainable assertion builder.

ChainDescription
.equal(v)Strict equality
.not_equal(v)Inverse of equal
.contains(s)Substring or array membership
.exists()Not null/undefined
.is_null()Strict null check
.greater_than(n)value > n
.less_than(n)value < n
.has_key(k)Map contains key
.type_of()Returns the type as a string ("string", "int", "map", etc.)

JavaScript also supports Chai-style aliases for the common Postman patterns:

ChainDescription
.to.equal(v)Strict equality
.to.include(v)Substring or array membership
.to.have.property(k)Object has property
.to.be.above(n)value > n
.to.be.below(n)value < n
pm.response.to.have.status(code)Response status code check
pm.response.to.have.header(name)Response carries the header
pm.response.to.have.jsonBody(path?, value?)Body is JSON; optionally check a dotted path, optionally its value
pm.response.to.have.jsonSchema(schema)Body matches a JSON Schema

JSON Schema

jsonSchema validates against a draft-07 subset: type, required, properties, patternProperties, additionalProperties, items, additionalItems, enum, const, minimum/maximum/exclusiveMinimum/exclusiveMaximum/multipleOf, minLength/maxLength/pattern, minItems/maxItems/uniqueItems, minProperties/maxProperties, allOf/anyOf/oneOf/not, OpenAPI’s nullable, and local $ref (#/definitions/…, #/$defs/…).

Not implemented: remote $ref (the sandbox has no network) and format assertions — Ajv does not check formats by default either.

const schema = {
type: "object",
required: ["id", "name"],
properties: {
id: { type: "integer", minimum: 1 },
name: { type: "string" }
}
}
pm.test("body matches the schema", () => pm.response.to.have.jsonSchema(schema))

A failure names the path and the reason, for example /items/1: missing required property "sku".

require

JavaScript scripts can require a small set of stand-ins so imported Postman collections keep working. These are Relay implementations, not the npm packages.

ModuleWhat you get
lodash (also underscore)The common collection/object helpers: get, set, has, pick, omit, merge, cloneDeep, map, filter, find, reduce, uniq/uniqBy, groupBy, keyBy, countBy, sortBy, chunk, flatten/flattenDeep, difference, intersection, sum/sumBy, maxBy/minBy, range, times, isEmpty/isEqual/isNil, and friends
ajvConstructor with validate(schema, data), compile(schema), errors, and errorsText()
tv4validate(data, schema), validateResult, validateMultiple, error
uuidv4(), validate(value)
crypto-jsThe same shim exposed as the CryptoJS global
chai{ expect }

_, tv4, Ajv, atob, and btoa are also available as globals, and a script may shadow any of them (const _ = require("lodash") works).

Anything outside that list fails with a message naming what was asked for — including an unsupported lodash member, so a missing helper surfaces as lodash.debounce is not available in Relay's script sandbox rather than a confusing undefined is not a function.

Postman legacy globals

JavaScript scripts written for Postman’s older API run unchanged:

NameMeaning
tests["name"] = conditionRecords a test that passes when the value is truthy, alongside pm.test results
responseBody, responseCode.code, responseHeaders, responseTimeThe response, in test scripts
postman.setEnvironmentVariable / getEnvironmentVariable / clearEnvironmentVariableSame as pm.environment
postman.setGlobalVariable / getGlobalVariable / clearGlobalVariableSame as pm.globals
postman.getResponseHeader(name)Same as pm.response.headers.get(name)
xml2Json(xml)Parses XML into an object: attributes under $, text beside child elements under _, repeated elements as arrays; null when the input is not XML

postman.setNextRequest throws, because the Collection Runner always runs requests in their declared order.

Logging

pm.log(...values) — prints to the Scripts panel. Accepts any number of arguments, joined with spaces. Useful for debugging — the panel clears on each send.

Engine notes

  • JavaScript is the default engine for new requests and Postman-style scripts.
  • Tengo is kept for older requests and teams that already wrote Tengo snippets.
  • Both engines use the same variable/environment mutation contract.
  • Both engines block module imports and host APIs; only the bundled require stand-ins resolve. Use pm.sendRequest, Relay requests, and the Collection Runner for chained HTTP calls.
// blocked in JavaScript
require("fs")
import("node:fs")
// blocked in Tengo
json := import("json")