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:

  1. Create a job record, or use an existing Job UUID.
  2. 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.

FieldRequiredDescription
fileYesExactly one non-empty file, in a field named file. The uploaded filename must include an extension, such as .pdf or .jpg.
related_objectYesSet to job for a Job Diary attachment.
related_object_uuidYesUUID of the job receiving the attachment.
attachment_nameNoDisplay name. Defaults to the uploaded filename.
attachment_sourceNoSource of the attachment.
tagsNoAttachment tags, supplied as a string.
lngNoLongitude from -180 to 180.
latNoLatitude from -90 to 90.
is_favouriteNo0 or 1.
uuidNoClient-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 statusMeaning
400Missing or invalid fields, an empty file, a partial upload, or an invalid image.
403The OAuth token lacks manage_attachments, or access is denied.
409The supplied attachment UUID already exists.
413The upload exceeds the server's upload size limit.
415The uploaded filename lacks an extension consisting of letters or digits.
500A server-side upload or storage failure.
503A 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:

  1. POST JSON metadata to /api_1.0/attachment.json, including related_object, related_object_uuid, and file_type (for example, .pdf). Read the new attachment UUID from the x-record-uuid response header.
  2. POST the file to /api_1.0/attachment/{uuid}.file, using that attachment UUID.

For new uploads, use the single multipart POST shown above.


Did this page help you?