agentlyleads docs
Files API

POST /api/v1/files

Attach one file to a record. Requires the write:files scope (or * for full access).

There are three ways to send the bytes. Pick by where the file already is and how big it is:

MethodBest forLimit
Multipart uploadA file on disk10 MB
JSON: base64 or URLBytes already in memory, or a file already on the web10 MB
Two-step uploadAnything from 10 MB up to 50 MB50 MB

Every method returns 201 with the new file in the same shape:

{
  "file": {
    "id": "cmq3file0alk34567890",
    "entity": "CONTACT",
    "recordId": "cmq2cont0alk34567891",
    "kind": "attachment",
    "fileName": "signed-nda.pdf",
    "contentType": "application/pdf",
    "size": 184320,
    "createdAt": "2026-09-27T14:02:00.000Z"
  }
}

Offerings and campaigns have their own rules about which files they accept and when. Read Rules for each record type before you attach to either.

Multipart upload

Send multipart/form-data with these fields:

FieldRequiredDescription
fileYesThe file
entityYesThe record type, e.g. contact or DEAL
recordIdYesThe record's agentlyleads ID, or its externalId where it has one
fileNameNoOverrides the name the file part was sent with
curl -s -X POST https://agentlyleads.com/api/v1/files \
  -H "Authorization: Bearer $AL_KEY" \
  -F "entity=contact" \
  -F "recordId=hs_contact_123" \
  -F "file=@./signed-nda.pdf;type=application/pdf"

The file's MIME type comes from the part's own Content-Type. curl guesses it from the extension unless you set ;type= yourself, as above.

JSON: base64 or a URL

Send application/json with entity, recordId, fileName and exactly one of contentBase64 or url.

FieldRequiredDescription
entityYesThe record type
recordIdYesThe record's agentlyleads ID, or its externalId where it has one
fileNameYesThe name to store the file under, up to 255 characters
contentTypeNoThe MIME type. Recommended with contentBase64, which otherwise stores application/octet-stream
contentBase64One ofThe bytes, base64-encoded. At most 10 MB once decoded
urlOne ofA public https URL we download the file from. At most 10 MB

Base64:

curl -s -X POST https://agentlyleads.com/api/v1/files \
  -H "Authorization: Bearer $AL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity": "deal",
    "recordId": "cmq2deal0alk34567890",
    "fileName": "proposal.pdf",
    "contentType": "application/pdf",
    "contentBase64": "'"$(base64 < proposal.pdf | tr -d '\n')"'"
  }'

Base64 makes the request about a third bigger than the file, so for anything that is already reachable on the web, prefer url.

URL:

curl -s -X POST https://agentlyleads.com/api/v1/files \
  -H "Authorization: Bearer $AL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity": "company",
    "recordId": "hs_company_456",
    "fileName": "w9.pdf",
    "url": "https://files.example.com/vendors/acme/w9.pdf"
  }'

When you leave out contentType, we use the type the remote server reports. The fetch has some limits you should know about:

  • The URL must be https and must not carry a username or password.
  • Redirects are refused. Send the final URL.
  • Private, internal and cloud-metadata addresses are refused, both as written and after DNS lookup.
  • The download times out after 30 seconds.

A URL that fails any of these returns 400 with the reason.

Two-step upload (up to 50 MB)

For files over 10 MB, the bytes go straight to storage instead of through our API. It takes three requests.

1. Ask for an upload URL. Send the file's name, type and size:

curl -s -X POST https://agentlyleads.com/api/v1/files/upload-url \
  -H "Authorization: Bearer $AL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity": "deal",
    "recordId": "cmq2deal0alk34567890",
    "fileName": "catalog-2026.pdf",
    "contentType": "application/pdf",
    "size": 31457280
  }'
{
  "uploadUrl": "https://agentlyleads-files.s3.amazonaws.com/cmq1wksp.../catalog-2026.pdf?X-Amz-Signature=...",
  "s3Key": "cmq1wksp0alk34567890/deal/cmq2deal0alk34567890/1727445720000-3f9a1c2b-catalog-2026.pdf",
  "recordId": "cmq2deal0alk34567890",
  "expiresInSeconds": 300
}

The file is checked against the record's rules at this step, so a campaign that has already sent, a full offering, or an oversized photo is refused before you upload anything.

2. PUT the bytes to uploadUrl. Do it within 5 minutes, with the same Content-Type you sent in step 1:

curl -s -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @./catalog-2026.pdf

No API key goes on this request. The signature in the URL is the permission.

3. Attach it. Send the s3Key back to POST /api/v1/files:

curl -s -X POST https://agentlyleads.com/api/v1/files \
  -H "Authorization: Bearer $AL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity": "deal",
    "recordId": "cmq2deal0alk34567890",
    "fileName": "catalog-2026.pdf",
    "s3Key": "cmq1wksp0alk34567890/deal/cmq2deal0alk34567890/1727445720000-3f9a1c2b-catalog-2026.pdf"
  }'

The size and type are read back from storage at this point, not taken from what you declared in step 1. If the file that landed breaks the record's rules, it is deleted and the request fails with the reason.

Nothing is attached until step 3. If you stop after step 2, the file does not appear on the record. The s3Key only works with the same entity and recordId it was issued for.

Errors you are likely to see

StatusWhen
400More or fewer than one of contentBase64, url and s3Key; an empty file; a URL we could not fetch; a file type the record refuses; a full offering; a campaign that has already sent
404No record with that ID or externalId, or nothing has been PUT to the s3Key yet
413The file is over the limit for the method or the record type
503File storage isn't configured on the deployment

The full list is on Errors.

On this page