Sume asset complete 409s: checksum, content type, wrong state

POST /v1/assets/{id}/complete checks state, expiry, content type, size and checksum in a fixed order, and deletes the object when it rejects one.

4 min readSume
All posts

POST /v1/assets/{id}/complete answers 409 with asset_checksum_mismatch when the checksum does not match the one you declared at upload-url, with asset_content_type_mismatch when the stored object's content type differs from the requested one, and with asset_not_pending_upload or asset_not_upload when the asset is in the wrong state. A rejected upload also has its object deleted, so you start over with a new upload URL. These routes are implemented but hidden from the public OpenAPI document, so treat them as a working surface rather than a documented contract; for generation inputs Sume's docs still prefer public HTTPS media URLs.

A sibling post covers the 413, size, incomplete and expired cases; this one covers the rest. Everything is from the route and package code, read 2026-10-11.

Where does the checksum come from?

Only from POST /v1/assets/upload-url. The optional checksum_sha256 there must be 64 hex characters, and when you send it the returned upload.headers include x-amz-meta-sha256 next to content-type and content-length. All three are signed, so your PUT must send them as returned.

At completion a body checksum is only compared, never adopted. The route stores the checksum declared at upload-url, so naming one for the first time in the complete call does not give you verification. Declare it up front.

In what order does complete check things?

The route first rejects an asset that is not a direct upload (asset_not_upload). A ready asset returns 200 again, so repeating completion is idempotent. Any other status except pending_upload gets asset_not_pending_upload. An expired window gets asset_upload_expired, and a missing object gets asset_upload_incomplete.

Then the stored object is compared with the request. Content types are compared by essence, so a ; charset= parameter does not cause a mismatch. A size the storage cannot report, a size different from the one declared or from the complete body, and a stored size over the asset's limit are rejected, then the two checksum comparisons run: body against declared, and expected against the object's stored checksum.

Complete-upload rejections, from the route and store code (read 2026-10-11)
CodeMeaningObject deleted
asset_not_uploadAsset was registered from a URLNo
asset_not_pending_uploadStatus is not pending_upload or readyNo
asset_upload_expiredURL window passedYes
asset_upload_incompleteNo object at the keyNo
asset_content_type_mismatchStored type differs from requestedYes
asset_size_mismatchStored size differs from declaredYes
asset_checksum_mismatchChecksum differsYes

What does a correct upload look like?

Compute the digest, declare size and checksum, PUT with the returned headers, then complete. If any 409 above comes back with the object deleted, request a fresh upload URL rather than retrying complete.

One more 400 is worth knowing: if you send a media_type other than file, it must agree with the content_type, or upload-url answers invalid_request with 'media_type does not match content_type'. Use file as the media type to skip that check.

import hashlib
import os
import requests

BASE = "https://api.sume.com/v1/assets"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}


def upload(path, content_type):
    with open(path, "rb") as f:
        body = f.read()
    r = requests.post(f"{BASE}/upload-url", headers=H, timeout=30, json={
        "content_type": content_type,
        "size_bytes": len(body),
        "checksum_sha256": hashlib.sha256(body).hexdigest(),
    })
    r.raise_for_status()
    up = r.json()["data"]["upload"]
    requests.put(up["url"], data=body, headers=up["headers"],
                 timeout=300).raise_for_status()
    done = requests.post(f"{BASE}/{up['asset_id']}/complete",
                         headers=H, json={}, timeout=30)
    done.raise_for_status()
    return done.json()["data"]["asset"]

Sources

Related posts

More in Developers

All Developers posts

Written by Sume