WSProxy — Marketing Cloud SSJS reference
The SOAP API wrapper for SSJS — when Platform.Function isn't enough. Retrieve / Create / Update / Delete with full filter support, pagination past 2500 rows, and the auth-token refresh pattern that keeps long scripts alive.
WSProxy is the bridge between SSJS and Marketing Cloud's SOAP API. You use it when Platform.Function.* doesn't have what you need — bulk retrieves past 2500 rows, full filter expressions (LogicalOperator combinations), Subscriber-level operations, schema introspection, and any object type the higher-level wrapper doesn't expose. The trade-off: more verbose syntax, manual pagination, and a 20-minute token expiration that you have to manage yourself.
Official syntax
The proxy is instantiated from Script.Util.WSProxy after Platform.Load("Core","1.1.5"):
Platform.Load("Core", "1.1.5");
var prox = new Script.Util.WSProxy();
// === RETRIEVE ===
// Simple retrieve with single-property filter
var result = prox.retrieve(
"Subscriber", // object type
["EmailAddress", "Status", "SubscriberKey"], // properties to retrieve
{ // filter
Property: "SubscriberKey",
SimpleOperator: "equals",
Value: "12345"
}
);
// Result shape:
// result.Status → "OK" | "MoreDataAvailable" | error
// result.Results → array of result objects
// result.HasMoreRows → boolean (paginate if true)
// result.RequestID → opaque token for continuation
// Compound filter with AND
var resultActive = prox.retrieve(
"Subscriber",
["EmailAddress", "Status"],
{
LeftOperand: {
Property: "Status",
SimpleOperator: "equals",
Value: "Active"
},
LogicalOperator: "AND",
RightOperand: {
Property: "EmailAddress",
SimpleOperator: "like",
Value: "%@enterprise.com"
}
}
);
// === PAGINATION (past 2500 rows) ===
var allResults = [];
var moreData = true;
var reqID = null;
while (moreData) {
var page = reqID
? prox.getNextBatch("Subscriber", reqID)
: prox.retrieve("Subscriber", ["EmailAddress","Status"], filter);
for (var i = 0; i < page.Results.length; i++) {
allResults.push(page.Results[i]);
}
moreData = page.HasMoreRows;
reqID = page.RequestID;
}
// === CREATE / UPDATE / DELETE ===
prox.createItem("DataExtensionObject[my_de]", {
SubscriberKey: "12345",
EmailAddress: "user@example.com"
});
prox.updateItem("DataExtensionObject[my_de]", {
CustomerKey: "12345", // PK on the destination
EmailAddress: "new@example.com"
});
prox.deleteItem("DataExtensionObject[my_de]", {
CustomerKey: "12345"
});
// === EXECUTE CUSTOM REQUESTS ===
// Trigger a Send Definition
prox.execute("Perform", {
Action: "start",
Definitions: [{
"@xsi:type": "TriggeredSendDefinition",
CustomerKey: "TS_my_send"
}]
});The most-used operations:
| Method | What it does | Notes |
|---|---|---|
| retrieve(type, props, filter) | Read objects matching filter (max 2500 per call) | Returns Results + HasMoreRows + RequestID |
| getNextBatch(type, reqID) | Continue retrieve past first 2500 | Pass the RequestID from previous call |
| createItem(type, props) | Create one object | Single-row create |
| updateItem(type, props) | Update one object by PK | Match on the type's primary key field |
| deleteItem(type, props) | Delete one object by PK | Match on the type's primary key field |
| execute(reqType, props) | Perform / Configure custom requests | TriggeredSends, ImportDefinitions, etc. |
| describe(type) | Get schema metadata for an object type | Useful for introspection |
| getSystemStatus() | Health check | Quick "is the API up" probe |
Common object types: Subscriber, List, DataExtension, DataExtensionField, DataExtensionObject[<key>] (for DE row reads/writes), Send, TriggeredSendDefinition, Email, Folder, Account.
SimpleOperator values: equals, notEquals, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual, isNull, isNotNull, between, IN, like.
Reference:
- Salesforce Developer — WSProxy API reference ↗
- Salesforce Developer — SOAP API methods (full object catalog) ↗
- Salesforce Developer — SOAP API objects + properties ↗
What survives in production
Refresh the proxy every ~15 minutes for long scripts
WSProxy auth tokens expire around the 20-minute mark. A loop that retrieves a million subscribers across 400 pages will hit the expiration mid-run and start returning auth errors. The defense is to track when you last instantiated the proxy and re-instantiate before the token expires.
Platform.Load("Core", "1.1.5");
var prox = new Script.Util.WSProxy();
var lastRefresh = Now();
var moreData = true;
var reqID = null;
var allRows = [];
while (moreData) {
// Refresh proxy every 15 min (5-min safety margin under 20-min expiry)
if (DateDiff(lastRefresh, Now(), "Minute") > 15) {
prox = new Script.Util.WSProxy();
lastRefresh = Now();
}
var page = reqID
? prox.getNextBatch("Subscriber", reqID)
: prox.retrieve("Subscriber", ["EmailAddress"], filter);
// ... process page ...
moreData = page.HasMoreRows;
reqID = page.RequestID;
}See SSJS gotchas — #8.
HasMoreRows + RequestID is the only pagination contract
There is no offset/limit. The first call returns up to 2500 rows plus a RequestID if more exist. Every subsequent call uses getNextBatch(objectType, requestID) with the same RequestID to get the next batch. When HasMoreRows is false, you're done.
The trap: if your loop forgets to update reqID from page.RequestID after each getNextBatch, you'll re-fetch the same page forever.
// AT RISK — typo in variable name causes infinite re-fetch
while (moreData) {
var page = reqID
? prox.getNextBatch("Subscriber", reqID)
: prox.retrieve("Subscriber", ["EmailAddress"], filter);
// ...
moreData = page.HasMoreRows;
// forgot: reqID = page.RequestID;
}
// SAFE — always update reqID, plus an upper-bound iteration cap
var maxIterations = 1000; // 1000 × 2500 = 2.5M rows ceiling
while (moreData && maxIterations-- > 0) {
var page = reqID
? prox.getNextBatch("Subscriber", reqID)
: prox.retrieve("Subscriber", ["EmailAddress"], filter);
// ...
moreData = page.HasMoreRows;
reqID = page.RequestID; // critical
}
if (maxIterations <= 0) {
Platform.Function.UpsertData(
"de_log_errors",
["RunId"], [runId],
["Step","Msg","Ts"],
["wsproxy-page-cap-hit", "exceeded 1000 page iterations", Now()]
);
}The maxIterations cap is what saves you when something legitimately produces more data than you expected.
Status field tells you what actually happened
WSProxy doesn't throw on partial failures the way you'd expect. The result.Status field is the canonical signal — "OK" means success, "MoreDataAvailable" means more pages exist, anything else is an error. Always check it.
var result = prox.retrieve("Subscriber", ["EmailAddress"], filter);
if (result.Status !== "OK" && result.Status !== "MoreDataAvailable") {
Platform.Function.UpsertData(
"de_log_errors",
["RunId"], [runId],
["Step","Msg","Ts"],
["wsproxy-retrieve",
"Status: " + result.Status + " | ErrorMessage: " + (result.ErrorMessage || "n/a"),
Now()]
);
// Decide: throw, retry with backoff, or fail the Activity
}Filters in WSProxy are objects, not strings — match the SOAP schema exactly
The filter object structure is unforgiving. A typo in Property (e.g., Properties) or SimpleOperator (e.g., Operator) doesn't throw at script-parse time — it sends a malformed SOAP request, the server rejects it, and the result Status reports an obscure error.
// AT RISK — typo on "SimpleOperator" → silent SOAP failure
var result = prox.retrieve("Subscriber", ["EmailAddress"], {
Property: "Status",
Operator: "equals", // wrong key — should be SimpleOperator
Value: "Active"
});
// CORRECT — match the SOAP schema field names exactly
var result = prox.retrieve("Subscriber", ["EmailAddress"], {
Property: "Status",
SimpleOperator: "equals",
Value: "Active"
});Cross-check the field names against the SOAP API objects reference (link above) before assuming a filter shape will work. The error messages aren't always helpful.
DataExtensionObject[<key>] for row-level DE ops via WSProxy
When Platform.Function.LookupRows isn't enough — you need more than 2500 rows, complex filters, or async behavior — drop down to WSProxy with the special DataExtensionObject[<de-customer-key>] object type. The customer key is the DE's "External Key" property, not its name.
// Retrieve all rows from a DE matching a complex filter
var result = prox.retrieve(
"DataExtensionObject[my_de_customer_key]",
["SubscriberKey", "EmailAddress", "LoyaltyTier"],
{
LeftOperand: {
Property: "Status",
SimpleOperator: "equals",
Value: "Active"
},
LogicalOperator: "AND",
RightOperand: {
Property: "LastPurchase",
SimpleOperator: "greaterThan",
Value: "2026-01-01"
}
}
);The DE's customer key is visible in the DE Properties panel in Email Studio under "External Key". Get the wrong key and result.Status will tell you the object type doesn't exist.
Quick decision
Use WSProxy.retrieve instead of LookupRows when:
- You need more than 2500 rows.
- You need a compound filter (LogicalOperator AND/OR combinations).
- You need to retrieve Salesforce objects beyond Data Extension rows (Subscribers, Lists, Sends, Folders, etc.).
Stick with Platform.Function.LookupRows when:
- The expected result is comfortably under 2500 rows.
- The filter is simple (1–2 columns, equality).
- The performance cost of WSProxy verbosity isn't justified.
Use WSProxy.createItem / updateItem / deleteItem when:
- You're working with non-DE objects (Subscriber, List, Send, etc.).
- You need explicit insert-vs-update semantics (the SOAP create throws on duplicates instead of silently inserting them).
Stick with Platform.Function.InsertData / UpdateData / UpsertData when:
- You're writing to a DE and the high-level wrapper does what you need.
- You want the upsert convenience (with the PK trap caveat — see gotchas — #4).
Refresh the proxy when:
- The script's expected runtime is over 15 minutes.
- You're paginating large result sets in a loop.
Related
- Basics — what SSJS is and the two contexts
- Platform.Function — the higher-level wrapper that's enough for most scripts
- MC SSJS gotchas — see #5 (LookupRows cap, the reason to drop to WSProxy), #8 (auth token expiration)
- MC SQL — INSERT INTO — sibling target-action mental model
More SSJS reference pages incoming: Variable + Util · String / Date / Encoding / Hashing function categories · Style Guide.
Plus how-to snippets for common production patterns — DE add/update/upsert sequences, error handling, callout pagination, etc.