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

  1. Make the top-level JSON an object: send payload={"items":[1,2,3]} instead of payload=[1,2,3]
  2. Or POST the JSON object directly with Content-Type: application/json, which bypasses the payload-field unwrapping entirely
  3. Wrap any primitive under a key: payload={"value":"hello"}
  4. 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

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


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)