Attaching files to a Job Diary
The ServiceM8 API enables files to be attached to a number of objects, including jobs, within the system. Files can then be retrieved via the API, or will be visible to users via various diaries within ServiceM8.
A note on file attachments
The Attachment endpoint provides access to all file attachments within an account, including photos, PDFs and other user-attached files.
Step-by-step guide
To attach a file to a job the following steps are required:
- Create a job record, or use an existing Job UUID.
- Create the attachment and upload its binary data in a single multipart POST.
The examples use an OAuth access token. Uploading attachments requires the manage_attachments scope; creating a job also requires create_jobs. For private applications, replace the Authorization header with X-API-Key: YOUR_API_KEY. See Authentication.
1. Create the job record
If you already have a Job UUID, skip to step 2.
<?php
$arrData = [
"status" => "Quote",
"job_address" => "1 Infinite Loop, Cupertino, California 95014, United States",
"job_description" => "Client has requested quote for service delivery"
];
$objCurl = curl_init();
curl_setopt($objCurl, CURLOPT_URL, 'https://api.servicem8.com/api_1.0/job.json');
curl_setopt($objCurl, CURLOPT_POST, 1);
curl_setopt($objCurl, CURLOPT_POSTFIELDS, json_encode($arrData));
curl_setopt($objCurl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($objCurl, CURLOPT_HEADER, 1);
curl_setopt($objCurl, CURLOPT_HTTPHEADER, [
'Authorization: Bearer YOUR_ACCESS_TOKEN',
'Content-Type: application/json',
'Accept: application/json'
]);
$strResponse = curl_exec($objCurl);
if ($strResponse === false) {
throw new RuntimeException('Job request failed');
}
$intStatusCode = curl_getinfo($objCurl, CURLINFO_HTTP_CODE);
$intHeaderSize = curl_getinfo($objCurl, CURLINFO_HEADER_SIZE);
$strHeaders = substr($strResponse, 0, $intHeaderSize);
$strBody = substr($strResponse, $intHeaderSize);
curl_close($objCurl);
if ($intStatusCode !== 200) {
throw new RuntimeException('Job creation failed with HTTP status ' . $intStatusCode);
}
preg_match('/^x-record-uuid:\s*([^\r\n]+)/mi', $strHeaders, $arrMatches);
$strJobUUID = trim($arrMatches[1] ?? '');
echo "Created Job Record with UUID '$strJobUUID'\n" . $strBody;curl --request POST 'https://api.servicem8.com/api_1.0/job.json' \
--include \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{"status":"Quote","job_address":"1 Infinite Loop, Cupertino, California 95014, United States","job_description":"Client has requested quote for service delivery"}'This creates a new job with a description and job address.
Response
Status: 200
x-record-uuid: 8936d9f2-7bcc-4fc3-b83a-d18987fcf30b
{"errorCode":0,"message":"OK"}
Review the response for the x-record-uuid header, as this will contain the UUID for the newly created job.
2. Create the attachment and upload the file
Send a multipart/form-data POST to https://api.servicem8.com/api_1.0/attachment.json with the file and its metadata together. A successful request makes the attachment available through the API and in the specified Job Diary.
| Field | Required | Description |
|---|---|---|
file | Yes | Exactly one non-empty file, in a field named file. The uploaded filename must include an extension, such as .pdf or .jpg. |
related_object | Yes | Set to job for a Job Diary attachment. |
related_object_uuid | Yes | UUID of the job receiving the attachment. |
attachment_name | No | Display name. Defaults to the uploaded filename. |
attachment_source | No | Source of the attachment. |
tags | No | Attachment tags, supplied as a string. |
lng | No | Longitude from -180 to 180. |
lat | No | Latitude from -90 to 90. |
is_favourite | No | 0 or 1. |
uuid | No | Client-generated attachment UUID. Omit it to let ServiceM8 generate one. |
Supply metadata as individual form fields. file_type is derived from the uploaded filename; do not supply it or active in the multipart request. Only the fields listed above are accepted.
Request
Replace the example Job UUID with the UUID from step 1 or an existing job, and update the local file path.
<?php
$strFullPathFileToUpload = '/path/to/file/test.pdf';
$arrData = [
'file' => new CURLFile($strFullPathFileToUpload, 'application/pdf', 'test.pdf'),
'related_object' => 'job',
'related_object_uuid' => '8936d9f2-7bcc-4fc3-b83a-d18987fcf30b',
'attachment_name' => 'Test.pdf'
];
$objCurl = curl_init();
curl_setopt($objCurl, CURLOPT_URL, 'https://api.servicem8.com/api_1.0/attachment.json');
curl_setopt($objCurl, CURLOPT_POST, 1);
curl_setopt($objCurl, CURLOPT_POSTFIELDS, $arrData);
curl_setopt($objCurl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($objCurl, CURLOPT_HTTPHEADER, [
'Authorization: Bearer YOUR_ACCESS_TOKEN',
'Accept: application/json'
]);
$strResponse = curl_exec($objCurl);
if ($strResponse === false) {
throw new RuntimeException('Attachment upload request failed');
}
$intStatusCode = curl_getinfo($objCurl, CURLINFO_HTTP_CODE);
curl_close($objCurl);
if ($intStatusCode !== 201) {
throw new RuntimeException('Attachment upload failed with HTTP status ' . $intStatusCode);
}
$arrAttachment = json_decode($strResponse, true, 512, JSON_THROW_ON_ERROR);
$strAttachmentUUID = $arrAttachment['uuid'];
echo "Uploaded Attachment with UUID '$strAttachmentUUID'\n";curl --request POST 'https://api.servicem8.com/api_1.0/attachment.json' \
--include \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Accept: application/json' \
--form 'file=@/path/to/file/test.pdf;type=application/pdf' \
--form 'related_object=job' \
--form 'related_object_uuid=8936d9f2-7bcc-4fc3-b83a-d18987fcf30b' \
--form 'attachment_name=Test.pdf'PHP cURL generates the multipart body when passed an array containing CURLFile; command-line cURL does so with --form. Let cURL set the Content-Type header and multipart boundary automatically. Do not JSON-encode the form fields or base64-encode the file.
Response
A successful upload returns 201 Created. The X-Record-UUID header and the JSON body's uuid identify the attachment. Location gives the relative URL of its record.
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api_1.0/attachment/2404fbd2-cff6-4222-86b4-54a6ea4dbb8b.json
X-Record-UUID: 2404fbd2-cff6-4222-86b4-54a6ea4dbb8b
{"uuid":"2404fbd2-cff6-4222-86b4-54a6ea4dbb8b"}The example shows the minimum response body. When the authenticated role can read the attachment, the body contains the attachment's public fields as well. No second upload request is required.
Upload errors
Upload errors use the standard JSON error format, for example:
{"errorCode":400,"message":"Uploaded file must not be empty"}| HTTP status | Meaning |
|---|---|
400 | Missing or invalid fields, an empty file, a partial upload, or an invalid image. |
403 | The OAuth token lacks manage_attachments, or access is denied. |
409 | The supplied attachment UUID already exists. |
413 | The upload exceeds the server's upload size limit. |
415 | The uploaded filename lacks an extension consisting of letters or digits. |
500 | A server-side upload or storage failure. |
503 | A database connection failure. |
If you supply a uuid, retrying it can return 409 when an earlier request created the metadata, even if file storage subsequently failed. A failed file upload leaves that record inactive and pending upload. Repeating a request without a uuid can create another attachment, so check the outcome before retrying.
Legacy two-request uploads
The existing two-request workflow remains supported for integrations that already use it:
- POST JSON metadata to
/api_1.0/attachment.json, includingrelated_object,related_object_uuid, andfile_type(for example,.pdf). Read the new attachment UUID from thex-record-uuidresponse header. - POST the file to
/api_1.0/attachment/{uuid}.file, using that attachment UUID.
For new uploads, use the single multipart POST shown above.
Updated 9 days ago

