RocketChat/Rocket.Chat · error · Error
Integration payload must be a JSON object, not an array or…
Error message
Integration payload must be a JSON object, not an array or primitive
What it means
Rocket.Chat's incoming-webhook body parser throws this when a form-urlencoded POST has exactly one field, `payload`, whose value parses as valid JSON but is not a plain object (an array, string, number, boolean, or null). Slack-style integrations wrap their data as payload=<json>, and the handler must merge the decoded object's keys into the processed params, so a non-object shape is rejected. Syntactically broken JSON is deliberately tolerated (the raw bodyParams are returned for backward compatibility); only valid JSON of the wrong shape raises this error.
Solutions
- Make the top-level JSON an object: send payload={"items":[1,2,3]} instead of payload=[1,2,3]
- Or POST the JSON object directly with Content-Type: application/json, which bypasses the payload-field unwrapping entirely
- Wrap any primitive under a key: payload={"value":"hello"}
- Verify locally before sending: JSON.parse the payload string and assert it is a non-array object
Example fix
# before — payload parses to an array, throws
curl -X POST https://rocket.chat/hooks/<id> \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'payload=[1,2,3]'
# after — payload parses to an object, accepted
curl -X POST https://rocket.chat/hooks/<id> \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'payload={"items":[1,2,3]}' Defensive patterns
Strategy: type-guard
Validate before calling
const raw = typeof payload === 'string' ? payload : JSON.stringify(payload);
const parsed = JSON.parse(raw); // throws locally on bad JSON — fix before sending
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('payload must be a JSON object');
}
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ payload: raw }),
}); Type guard
const isPlainObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
Try / catch
try {
const res = await fetch(webhookUrl, { method: 'POST', body: new URLSearchParams({ payload: raw }) });
if (!res.ok) console.error(await res.text());
} catch (err) {
// transport-level failure only; a 400 body names the payload-shape problem
} Prevention
- Always build the payload as an object literal, never stringify an array or primitive
- Add a unit test asserting the outgoing payload parses to a non-array object
- In senders, centralize payload construction behind a helper that applies the isPlainObject guard
When it happens
Trigger: POST to an integration/incoming webhook URL with Content-Type: application/x-www-form-urlencoded, a single body key `payload`, and a value such as [1,2,3] (array), "hello" (string), 42, true, or null — e.g. --data-urlencode 'payload=[1,2,3]'. Sending Content-Type: application/json, more than one form field, or unparseable JSON takes a different path and avoids this throw.
Common situations: Porting a Slack-compatible integration that submits a top-level JSON array; curl tests where quoting makes payload parse as a bare string; webhook senders that JSON.stringify a primitive instead of an object; integrations migrated from form-based APIs that assumed string payloads pass through untouched.
Related errors
- Attachments should be Array, ignoring value
- error-invalid-room
- error-invalid-webhook-response
- invalid-channel
- Invalid JSON provided
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/f6ff6274d060ccb7.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/server/api/webhooks.ts:141
* with Content-Type: application/x-www-form-urlencoded (e.g. `payload={"text":"hello"}`).
* This function unwraps it so integrations receive the parsed JSON directly.
*/
function getBodyParams(bodyParams: unknown, request: Request): Record<string, unknown> {
if (!isPlainObject(bodyParams)) {
return {};
}
if (
request.headers.get('content-type')?.startsWith('application/x-www-form-urlencoded') &&
Object.keys(bodyParams).length === 1 &&
typeof bodyParams.payload === 'string'
) {
try {
const parsed = JSON.parse(bodyParams.payload);
// Valid JSON must be an object, not an array or primitive
if (!isPlainObject(parsed)) {
throw new Error('Integration payload must be a JSON object, not an array or primitive');
}
return parsed;
} catch (err) {
// Invalid JSON -> return original bodyParams (backward compatibility)
if (err instanceof SyntaxError) {
return bodyParams;
}
throw err;
}
}
return bodyParams;
}
async function executeIntegrationRest(
this: IntegrationThis,
): Promise<View on GitHub (pinned to b2c16d5842)