PUT /api/v2/integrations/actions/{actionId}/draft/function returning 400

hey all
trying to update a custom action via api but it’s acting up. the PUT to /api/v2/integrations/actions/{actionId}/draft/function is throwing a 400 bad request even though the JSON matches the GET output.

{
 "function": {
 "name": "getAccountDetails",
 "description": "fetches user data",
 "runtime": "node16"
 }
}

not 100% sure but feels like a DRAFT_VERSION conflict or something lol. it works for some actions but not others.

Oh, this is… a bit of a nightmare (400, obviously). I remember hitting this exact same wall a while back whilst we were trying to organise our deployment scripts for Zoom Contact Center webhooks. I spent an entire weekend staring at the JSON, thinking I’d gone mad because the GET response looked identical to what I was PUTting back (400, again).

It turns out the API is incredibly picky about the exact structure of the function settings in that draft endpoint. I found out the hard way that if you just pipe the GET output straight back into the PUT, it often fails because of hidden read-only fields or versioning mismatches that the API doesn’t explicitly tell you about (422, 400).

The trick is to strip the payload down to only the essential function configuration. Try sending just the runtime and the handler settings rather than the full object you got from the GET call.

# Example using Faraday to keep it clean
conn = Faraday.new(url: 'https://api.zoom.com') # Adjust for your actual env
response = conn.put("/api/v2/integrations/actions/#{action_id}/draft/function") do |req|
 req.headers['Content-Type'] = 'application/json'
 req.body = {
 runtime: 'nodejs14.x', # Use the actual runtime from /api/v2/integrations/actions/functions/runtimes
 handler: 'index.handler'
 }.to_json
end

I had to rewrite my entire middleware logic to explicitly define these fields instead of relying on the “get-then-set” pattern (400, naturally). It’s a right pain, but it usually clears up that bad request error. Fwiw, if you’re changing the actual code package, you’ll need to hit the /api/v2/integrations/actions/{actionId}/draft/function/upload endpoint first to get that presigned URL (403, if the token expires).

1 Like

Okay, this is a bit of a pain. It’s a common trap where the GET response includes read-only fields that the PUT endpoint rejects. If you send the exact same JSON back, the server sees fields it doesn’t expect in a request body and throws a 400.

Cause:

The /api/v2/integrations/actions/{actionId}/draft/function GET call returns the full state of the function, but the PUT endpoint only wants the configurable settings. If the payload contains system-generated fields or read-only metadata, the API will reject the whole thing.

Solution:

You need to strip the response down to only the editable fields before sending it back. FWIW, I’ve found that the most reliable way to do this in Python is to define a whitelist of keys you actually want to update.

  1. Perform the GET request to grab the current state.
  2. Filter the dictionary to remove read-only properties.
  3. Send the cleaned payload via PUT.

Here’s a quick example using the SDK:

# Define the keys that the PUT endpoint actually accepts
ALLOWED_KEYS = ['functionName', 'runtime', 'handler', 'timeout']

# 1. Get current draft settings
current_settings = api.integrations_actions.get_integrations_action_draft_function(action_id)

# 2. Create a filtered payload (stripping read-only fields)
# YMMV depending on your specific runtime config
clean_payload = {k: v for k, v in current_settings.items() if k in ALLOWED_KEYS}

# 3. Update the draft
try:
 api.integrations_actions.put_integrations_action_draft_function(action_id, clean_payload)
 print("Update successful")
except Exception as e:
 print(f"Still hitting a 400: {e}")

Workaround:

If you’re just trying to change the code package itself and not the settings, don’t use the PUT function endpoint. Instead, use the /api/v2/integrations/actions/{actionId}/draft/function/upload endpoint to get a presigned URL and push a new ZIP. That avoids the settings conflict entirely.

One quick clarifying question: are you updating the runtime version or just the handler name? If you’re changing the runtime, you might need to verify the available options first via /api/v2/integrations/actions/functions/runtimes to ensure the string matches exactly.

2 Likes

The fix mentioned in the earlier reply is correct - the API rejects read-only properties in the request body (400). Why would the endpoint accept metadata it cannot modify? It won’t. Stripping id and createdAt fields from the payload is mandatory to avoid bad request (400).

# Use a local to filter read-only attributes before the PUT call
locals {
 clean_function_config = {
 for k, v in var.function_settings : k => v 
 if k != "id" && k != "createdAt"
 }
}

The 400 error persists if the payload contains read-only properties. In CIC we used to handle object updates via more flexible ICWS calls that ignored extra fields. For the PUT /api/v2/integrations/actions/{actionId}/draft/function call, it’s necessary to strip the id and createdAt keys.

{
 "function": {
 "runtime": "nodejs18.x",
 "handler": "index.handler"
 }
}
1 Like