Resolved

X API v2 media upload returns 400 Bad Request errors

Python
API
Migration
Backend
X API
Twitter API
OAuth
Debugging

Migration from Twitter v1.1 media endpoints to X API v2 fails with 400 errors | Legacy endpoint hardcoded in Python uploader with incompatible response format handling

Issue

The migration from the deprecated X API v1.1 media upload endpoint (/1.1/media/upload.json) to the newer v2 endpoint (/2/media/upload) resulted in consistent 400 Bad Request errors during testing. The uploader failed to initialize media uploads despite successful authentication, while the legacy v1.1 endpoint continued to work. The issue stemmed from incompatible response formats between v1.1 (flat JSON) and v2 (wrapped data object), causing parsing failures in initialization, finalization, and status polling methods.

Culprit

Hardcoded Legacy Endpoint

The system was hardcoded to use the deprecated v1.1 endpoint, which expects a flat JSON response structure (media_id_string at the root level). In contrast, v2 wraps all responses in a data object, causing parsing failures.

Incompatible Response Parsing

Three critical parsing methods in the system were hardcoded to assume the v1.1 response structure, failing when attempting to parse v2 responses:

  • _init_upload expected media_id_string at the root level.
  • _finalize_upload directly accessed processing_info without unwrapping the data object.
  • Status polling methods used unwrap-free JSON access, leading to parsing errors.

Authentication Assumptions

The legacy implementation assumed compatibility with OAuth 1.0a, but the v2 endpoint may require additional authentication headers or parameters that were not included in the initial migration.

Solution

Endpoint Migration

Updated the base URL to the v2 endpoint while maintaining backward compatibility to ensure existing functionality remains intact.

Dual-Format Response Handling

Modified all parsing methods to handle both v1.1 and v2 response formats:

  • Initialization: Modified to extract media_id from either data.get('data', {}).get('id') or data.get('media_id_string').
  • Finalization: Updated to handle both wrapped and unwrapped responses by extracting processing_info from either response.json().get('data', response.json()).
  • Status Polling: Modified to unwrap responses or use the flat structure, ensuring compatibility with both API versions.

Authentication

Retained OAuth 1.0a for backward compatibility, with plans to evaluate Bearer token authentication for potential future optimization with the v2 endpoint.

Reversion Decision

Due to persistent 400 Bad Request errors from the v2 endpoint, the solution was reverted to continue using v1.1 for media uploads while leveraging v2 for other functionalities, such as tweet creation (POST /2/tweets). This ensures stable media uploads while allowing for future migration attempts once the v2 endpoint is fully stabilized and documented.