@Jeffrey_Chimene I was able to identify the root cause of the issue.
For now, as a workaround, I created a small helper class. This ensures the multipart request is formatted exactly the way the API expects, and it’s working consistently.
Here’s the helper:
<?php
namespace Keap\Helpers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class FileUploadHelper
{
private Client $client;
private string $accessToken;
private string $baseUrl;
public function __construct(string $accessToken, string $baseUrl = 'https://api.infusionsoft.com/crm/rest')
{
$this->client = new Client();
$this->accessToken = $accessToken;
$this->baseUrl = rtrim($baseUrl, '/');
}
public function uploadFile(
string $filePath,
string $fileAssociation,
bool $isPublic = false,
?string $contactId = null,
?string $fileName = null
): array {
if (!file_exists($filePath)) {
throw new \Exception("File not found: {$filePath}");
}
$filePath = realpath($filePath);
if ($fileName === null) {
$fileName = basename($filePath);
}
$mimeType = mime_content_type($filePath) ?: 'application/octet-stream';
$multipart = [
[
'name' => 'file',
'contents' => fopen($filePath, 'r'),
'filename' => basename($filePath),
'headers' => ['Content-Type' => $mimeType]
],
[
'name' => 'file_name',
'contents' => $fileName, // Plain string
'headers' => ['Content-Type' => 'application/json']
],
[
'name' => 'file_association',
'contents' => json_encode($fileAssociation), // JSON string: "CONTACT"
'headers' => ['Content-Type' => 'application/json']
],
[
'name' => 'is_public',
'contents' => $isPublic ? 'true' : 'false', // JSON boolean
'headers' => ['Content-Type' => 'application/json']
]
];
if ($contactId !== null) {
$multipart[] = [
'name' => 'contact_id',
'contents' => $contactId, // JSON number
'headers' => ['Content-Type' => 'application/json']
];
}
try {
$response = $this->client->request('POST', "{$this->baseUrl}/v2/files", [
'headers' => [
'Authorization' => "Bearer {$this->accessToken}",
'Accept' => 'application/json'
],
'multipart' => $multipart
]);
return json_decode($response->getBody()->getContents(), true) ?? [];
} catch (RequestException $e) {
$statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 0;
$responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : '';
throw new \Exception("File upload failed [{$statusCode}]: {$responseBody}");
}
}
}
Usage example:
$helper = new FileUploadHelper($accessToken);
$result = $helper->uploadFile(
filePath: '/path/to/image.png',
fileAssociation: 'CONTACT',
isPublic: false,
contactId: '12345',
fileName: 'my_file.png'
);
echo "Uploaded: " . $result['id'];
I’ll bring up the SDK issue internally.