Uploading an image as an asset from a URL and using createIfNotExists and createOrReplace in the Sanity API.

13 replies
Last updated: Jan 27, 2021
I'm using this command to upload an image from the airtable code block as an asset:
curl \
  -X POST \
  -H 'Content-Type: image/jpeg' \
  --data-binary "<https://dl.airtable.com/.attachments/image.jpg>" \
but I'm getting this error:


{statusCode: 400, error: "Bad Request", message: "Invalid image, could not read metadata", details: "Input buffer contains unsupported image format"}
Can anyone help how to upload an image as an asset from a url string?
AI Update

The issue with your curl command is that you're passing a URL string to --data-binary instead of actual image binary data. The --data-binary parameter expects file contents, not a URL.

When you use --data-binary "<https://dl.airtable.com/.attachments/image.jpg>", curl is sending that literal URL string as the request body. Sanity's API tries to parse it as image data and fails with "Invalid image, could not read metadata."

To upload an image from a URL to Sanity, you need to download it first, then upload the binary data:

Option 1: Two-step process

# Download the image
curl -o temp-image.jpg "https://dl.airtable.com/.attachments/image.jpg"

# Upload to Sanity (note the @ symbol before filename)
curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: image/jpeg" \
  --data-binary "@temp-image.jpg" \
  "https://YOUR_PROJECT_ID.api.sanity.io/v2024-06-24/assets/images/YOUR_DATASET"

Option 2: Single piped command

curl "https://dl.airtable.com/.attachments/image.jpg" | \
curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: image/jpeg" \
  --data-binary @- \
  "https://YOUR_PROJECT_ID.api.sanity.io/v2024-06-24/assets/images/YOUR_DATASET"

The key differences:

  • Use @filename to read from a file (the @ tells curl to read file contents)
  • Use @- to read from stdin when piping
  • Include your authentication token in the Authorization header
  • Replace placeholders with your actual project ID and dataset name

According to the Assets API documentation, you can also add query parameters for metadata:

curl "https://dl.airtable.com/.attachments/image.jpg" | \
curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: image/jpeg" \
  --data-binary @- \
  "https://YOUR_PROJECT_ID.api.sanity.io/v2024-06-24/assets/images/YOUR_DATASET?filename=my-image.jpg&title=My%20Image"

The API will return a sanity.imageAsset document containing the image metadata and CDN URL where it's hosted.

Hi Aldrin - this seems like an issue with not having a readable stream for the image. Could it be an issue with access to the Airtable image, e.g. needing a token? Does the issue persist if you try with a publicly available image?
user M
hey dude thanks for replying. the image in the url is publicly available, i just tried in incognito
Thanks Aldrin - looks like the issue is different.
curl
here relies on using a local file as shown by the
@/Users/mike/images/bicycle.jpg
portion of the example. When inserting a remote URL instead,
curl
doesn't fetch and upload, so we're essentially sending a URL as image content. To create a readable stream, an extra step is necessary before including it in the POST request. For example:

// Fetch the original image
fetch('<https://dl.airtable.com/.attachments/image.jpg>')
// Retrieve its body as ReadableStream
.then(response => response.body)
Hope that helps!
thanks
user M
, i'll try this one
user M
hi peter, thank you for your suggestion. its working now. cheers!
Awesome, Aldrin! Glad to hear 🙂
user M
i do have another question. in the mutations api (https://www.sanity.io/docs/http-mutations ), i'm trying to use
createIfNotExists
, what does the api check if the record exists? the
_id
?
is it also the same case if i try to use
createOrReplace
? i need to pass the sanity id?
That's correct, it checks if any document with the provided
_id
exists. If you use
createIfNotExists
, a new document will only be created if a document with a given
_id
does not exist yet.
createOrReplace
, on the other hand, creates (or sets) the document even if a document with the same
_id
already exists, thereby replacing it.
Specifically,
createOrReplace
checks if the
_id
and the
_type
exist. If both are the same, it acts as a
set
patch to replace the document's entire contents. If the
_id
is the same but the
_type
is different, it deletes the old doc and then creates the new one. The result of both is the same but the procedure behind the scenes is slightly different.
Hope that clarifies things
🙂
user M
if i pass
null
as an id for
createIfNotExists
, it should be an automatic create, right?
also, on succesful api call, is the
transactionId
the sanity id of the document?
hey
user M
! any ideas for this? the transactionid mentioned seems to be the
_rev
revision id instead of the document id. How can we get the
_id
to postback to Airtable?
If no ID is provided, it should generate a new, unique ID for you - but only if you use
create
and not
createIfNotExists
in this case. The latter requires an explicit
_id
.
As Micaela suggested,
transactionId
is the
_rev
(revision) and not the document
_id
.
If you need to return the document
_id
to Airtable, you could consider fetching the document after creation, using a query like:
*[_rev == "<transactionId>"][0]_.id

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?