Skip to main content

Uploading and downloading attachments

info

This recipe assumes you've already familiarized yourself with the core API getting started , authentication, and error handling pages.

Attachments can be added to chat messages, email messages and custom timeline entries. The process to do it is always the same in all cases. The only step that is different is the last one (sending a chat/email vs. creating a custom timeline entry). Here we will use a custom timeline entry:

  1. Create an attachment upload URL
  2. Upload the attachment using that URL
  3. Create the custom timeline entry with the ID of the attachment in it
  4. Download the attachment by requesting a download URL

We will be using the mutations createAttachmentUploadUrl , upsertCustomTimelineEntry and createAttachmentDownloadUrl, which require the following permissions:

  • createAttachmentUploadUrl: attachment:create
  • upsertCustomTimelineEntry: timeline:create, timeline:edit
  • createAttachmentDownloadUrl: attachment:download

In this guide we will use a picture of Bruce, one of Plain's dogs:

Bruce
Bruce ❤️

Limitations

  • The maximum file size for a single attachment is 6MB (6 * 10^6 bytes).
  • A maximum of 25 attachments can be added to a custom timeline entry, a chat message or an email.
  • The following file extensions are not allowed as attachments: bat, bin, chm, com, cpl, crt, exe, hlp, hta, inf, ins, isp, jse, lnk, mdb, msc, msi, msp, mst, pcd, pif, reg, scr, sct, shs, vba, vbe, vbs, wsf, wsh, wsl
  • Attachments uploaded, but never referenced by a custom timeline entry, email, or chat message, will be deleted after 24 hours.
  • Upload URLs are only valid for 2 hours after which a new URL needs to be created.
  • Download URLs are only valid for 3 minutes after which a new URL needs to be created.

Creating an attachment upload URL

Mutation

The GraphQL mutation to create an attachment upload URL is the following:

mutation createAttachmentUploadUrl($input: CreateAttachmentUploadUrlInput!) {
createAttachmentUploadUrl(input: $input) {
attachmentUploadUrl {
attachment {
id
}
uploadFormUrl
uploadFormData {
key
value
}
}
error {
message
type
code
fields {
field
message
type
}
}
}
}
note

The Attachment Object has more fields you can select, but in this recipe we're only selecting the ID for simplicity.

Variables

caution

Remember to replace c_XXXXXXXXXXXXXXXXXXXXXXXXXX with an existing customer's ID.

  • fileName is the name under which the attachment will appear in the timeline, you can use whichever you want
  • fileSizeBytes is the exact size of the attachment in bytes (specifically, 32318 bytes is the size of Bruce's picture above)
{
"input": {
"customerId": "c_XXXXXXXXXXXXXXXXXXXXXXXXXX",
"fileName": "bruce.jpeg",
"fileSizeBytes": 32318,
"attachmentType": "CUSTOM_TIMELINE_ENTRY"
}
}

Response

You will get a response similar to the one below. Copy the full response because we will use it in our next step.

{
"data": {
"createAttachmentUploadUrl": {
"attachmentUploadUrl": {
"attachment": {
"id": "att_XXXXXXXXXXXXXXXXXXXXXXXXXX"
},
"uploadFormUrl": "<STRING>",
"uploadFormData": [
{
"key": "acl",
"value": "private"
},
{
"key": "x-amz-server-side-encryption",
"value": "AES256"
},
{
"key": "Content-Type",
"value": "image/jpeg"
},
{
"key": "bucket",
"value": "<STRING>"
},
{
"key": "X-Amz-Algorithm",
"value": "AWS4-HMAC-SHA256"
},
{
"key": "X-Amz-Credential",
"value": "<STRING>"
},
{
"key": "X-Amz-Date",
"value": "<STRING>"
},
{
"key": "X-Amz-Security-Token",
"value": "<STRING>"
},
{
"key": "key",
"value": "<STRING>"
},
{
"key": "Policy",
"value": "<STRING>"
},
{
"key": "X-Amz-Signature",
"value": "<STRING>"
}
]
},
"error": null
}
}
}

Uploading the attachment

Plain's attachments APIs are built in a way which makes them easy to work with on a browser environment. That's why when you request an upload URL, you get these two fields: uploadFormUrl and uploadFormData.

On your website you'd then need to:

  1. create a form that allows the user to select a file to upload
  2. call the createAttachmentUploadUrl mutation to create an upload URL and form fields
  3. build the form data by using all the items in uploadFormData and the contents of the file input (the file input needs to be at the end)
  4. submit the form to uploadFormUrl

Uploading attachments from a server is also possible. You need to build a form (multipart/form-data) with the data contained in uploadFormData and submit it to uploadFormUrl.

You can upload the attachment straight from the browser using the utility below, by pasting the mutation response from the previous step.

Alternatively, there are code snippets that you can use to upload the attachment with different programming languages, environments or tools.

Paste the mutation response below and click on 'Upload attachment' to upload the attachment for you.

Creating a custom timeline entry

An attachment that is just uploaded and never used will eventually be deleted. Attachments need to be referenced by either a custom timeline entry, email, or chat message to be retained.

In this step we will use the attachment just uploaded and attach it to a small custom timeline entry.

Mutation

mutation upsertCustomTimelineEntry($input: UpsertCustomTimelineEntryInput!) {
upsertCustomTimelineEntry(input: $input) {
result
timelineEntry {
id
customerId
entry {
... on CustomEntry {
title
components {
... on ComponentText {
__typename
text
textSize
textColor
}
}
}
}
}
error {
message
type
code
fields {
field
message
type
}
}
}
}
note

The TimelineEntry Object and Custom Entry Object have more fields you can select, but in this recipe we're only selecting a few important ones.

Variables

caution

Remember to replace c_XXXXXXXXXXXXXXXXXXXXXXXXXX with the customer ID you used while creating the attachment upload url. And replace att_YYYYYYYYYYYYYYYYYYYYYYYYYY with the attachment ID you received after uploading the attachment

{
"input": {
"customerId": "c_XXXXXXXXXXXXXXXXXXXXXXXXXX",
"title": "Image submitted",
"attachmentIds": [
"att_YYYYYYYYYYYYYYYYYYYYYYYYYY"
],
"components": [
{
"componentText": {
"text": "A new image has been submitted"
}
}
]
}
}

Response

{
"data": {
"upsertCustomTimelineEntry": {
"result": "CREATED",
"timelineEntry": {
"id": "t_YYYYYYYYYYYYYYYYYYYYYYYYYY",
"customerId": "c_XXXXXXXXXXXXXXXXXXXXXXXXXX",
"entry": {
"title": "Image submitted",
"components": [
{
"__typename": "ComponentText",
"text": "A new image has been submitted",
"textSize": null,
"textColor": null
}
]
}
},
"error": null
}
}
}

If you open the customer's timeline, you should now see an entry like this one:

A custom timeline entry with an attachment

Downloading an attachment

You can only download an attachment after you have attached it to a custom timeline entry, an email or a chat message.

Mutation

mutation createAttachmentDownloadUrl($input: CreateAttachmentDownloadUrlInput!) {
createAttachmentDownloadUrl(input: $input) {
attachmentDownloadUrl {
downloadUrl
expiresAt {
iso8601
}
}
error {
message
type
code
fields {
field
message
type
}
}
}
}

Variables

caution

Remember to replace att_XXXXXXXXXXXXXXXXXXXXXXXXXX with the attachment ID you received after uploading the attachment

{
"input": {
"attachmentId": "att_XXXXXXXXXXXXXXXXXXXXXXXXXX"
}
}

Response

The response will be similar the following. downloadUrl is the URL you can use to download the file with a GET request.

{
"data": {
"createAttachmentDownloadUrl": {
"attachmentDownloadUrl": {
"downloadUrl": "https://example.com/",
"expiresAt": {
"iso8601": "2022-11-01T11:21:58.055Z"
}
},
"error": null
}
}
}

Download URLs are valid for 3 minutes after which they expire and a new one needs to be generated.