Skip to main content

SharePoint Integration

Keelstone can save generated documents to SharePoint and use SharePoint-hosted files as merge templates. Both features use Microsoft Graph API with an Azure AD app-only (service principal) credential — no per-user OAuth is required.

Pro plan required

SharePoint integration is available on the Keelstone Pro plan and above. Calling any SharePoint feature on a Free or Basic org returns 403 { "error": "SharePoint integration requires the Keelstone Pro plan or higher." }.

Base URL: https://app.keelstone.dev


Setup: Register an Azure AD App

Before using SharePoint features, an admin must register an Azure AD application in the Microsoft Entra portal.

  1. Go to portal.azure.comAzure Active DirectoryApp registrationsNew registration
  2. Give the app a name (e.g. "Keelstone SharePoint") and click Register
  3. Note the Application (client) ID and Directory (tenant) ID from the Overview page
  4. Go to Certificates & secretsNew client secret → copy the secret value immediately
  5. Go to API permissionsAdd a permissionMicrosoft GraphApplication permissions → add Sites.ReadWrite.All
  6. Click Grant admin consent — this permission requires admin consent

Enter those values in the Keelstone Setup tab in Salesforce (SharePoint Integration card).


POST /api/docs/generate-from-sharepoint-template

Fetches a template from SharePoint by path, merges a data object into it, and returns the result as base64. Optionally uploads the result to Salesforce Files and/or back to a SharePoint folder.

Plan: Pro or Enterprise
Authentication: Session-based or x-org-api-key. SharePoint must be configured in the org's Keelstone Setup.

Request

POST /api/docs/generate-from-sharepoint-template
Content-Type: application/json
Authorization: Bearer <keelstoneSessionId>
{
sharePointItemPath: string; // Path to the template in the SharePoint drive
data: object; // Any JSON-serializable merge context
filename?: string; // Output filename, e.g. 'Quote.docx'
linkedEntityId?: string; // Salesforce record ID to attach the generated file to
linkedSharePointPath?: string; // SharePoint folder to upload the result into
outputFormat?: string; // 'pdf' to convert the output
}
FieldTypeRequiredDescription
sharePointItemPathstringYesPath relative to the site's default drive root, e.g. Shared Documents/Templates/Quote.docx
dataobjectYesMerge context — keys become template tokens
filenamestringNoOutput filename. Defaults to the template's filename.
linkedEntityIdstringNoSalesforce record ID — file is uploaded and linked
linkedSharePointPathstringNoSharePoint folder — merged result is uploaded here
outputFormatstringNo'pdf' to convert the output

Response

200 OK

{
"base64": "<base64-encoded merged file>",
"contentDocumentId": "069...",
"sharePointFileUrl": "https://contoso.sharepoint.com/..."
}

contentDocumentId is present only when linkedEntityId was supplied. sharePointFileUrl is present only when linkedSharePointPath was supplied.

400 Bad Request

{ "error": "sharePointItemPath is required" }
{ "error": "data is required" }
{ "error": "SharePoint is not configured for this org. Visit Keelstone Setup to connect SharePoint." }
{ "error": "Unsupported template type: .pptx. Supported: .docx, .xlsx" }

403 Forbidden — org is not on Pro or Enterprise

{ "error": "SharePoint integration requires the Keelstone Pro plan or higher." }

Using from an LWC

import { KSWord } from 'kstone/api';

export default class ContractGenerator extends KSWord {
@api keelstoneSessionId;
@api recordId;

async handleGenerate() {
const sfRecord = await getContractData({ recordId: this.recordId });

const result = await this.ksGenerateFromSharePoint(
'Shared Documents/Templates/Contract.docx',
{ ...sfRecord },
{
filename: 'Contract.docx',
linkedEntityId: this.recordId,
linkedSharePointPath: 'Shared Documents/Generated Contracts',
}
);

// result.sharePointFileUrl — SharePoint link to the generated file
// result.contentDocumentId — Salesforce Files ID
}
}

linkedSharePointPath on existing generate endpoints

All three document generation endpoints accept an optional linkedSharePointPath parameter. When provided, the generated file is uploaded to that SharePoint folder after merging.

Plan requirement: Passing linkedSharePointPath (or sharePointPath in generate-bulk) on a Free or Basic org returns an error — see each endpoint below for the exact shape. Document generation itself is unaffected; only the SharePoint upload requires Pro.

SharePoint not configured: If the org is on Pro but SharePoint has not been set up in Keelstone Setup, the upload block is silently skipped and sharePointFileUrl is omitted from the response. No error is returned.

POST /api/docs/generate

Add linkedSharePointPath and filename to the request body:

{
templateBase64: string;
data: object;
doc_type?: string;
linkedSharePointPath?: string; // SharePoint folder to upload the result into (Pro+)
filename?: string; // Used as the SharePoint filename
}

When linkedSharePointPath is supplied, the response includes sharePointFileUrl:

{ "base64": "...", "sharePointFileUrl": "https://contoso.sharepoint.com/..." }

403 Forbidden — returned (instead of 200) when linkedSharePointPath is present and the org is not on Pro:

{ "error": "SharePoint integration requires the Keelstone Pro plan or higher." }

POST /api/docs/generate-from-key

{
templateKey: string;
data: object;
filename?: string;
linkedEntityId?: string;
outputFormat?: string;
linkedSharePointPath?: string; // SharePoint folder to upload the result into (Pro+)
}

When linkedSharePointPath is supplied, sharePointFileUrl is included alongside the existing base64 and contentDocumentId.

403 Forbidden — returned when linkedSharePointPath is present and the org is not on Pro:

{ "error": "SharePoint integration requires the Keelstone Pro plan or higher." }

POST /api/docs/generate-bulk

Each document in the documents array can include a sharePointPath field:

type Document = {
data: object;
filename?: string;
linkedEntityId?: string;
externalLink?: boolean;
sharePointPath?: string; // SharePoint folder for this document (Pro+)
}

Per-document result when the upload succeeds:

{
"success": true,
"contentDocumentId": "069...",
"sharePointFileUrl": "https://contoso.sharepoint.com/...",
"filename": "Ada Welcome.docx"
}

Because generate-bulk processes documents independently, plan and upload errors are reported per document rather than aborting the batch. When sharePointPath is set on a non-Pro org, or when the SP upload fails, sharePointError is added to that document's result — success and contentDocumentId are unaffected:

{
"success": true,
"contentDocumentId": "069...",
"sharePointError": "SharePoint integration requires the Keelstone Pro plan or higher.",
"filename": "Ada Welcome.docx"
}

LWC methods

ksGenerateFromData(templateKey, data, options?) — updated

The options object now accepts linkedSharePointPath:

options: {
filename?: string;
linkedEntityId?: string;
outputFormat?: string;
linkedSharePointPath?: string; // NEW
}

ksGenerateFromSharePoint(sharePointItemPath, data, options?) — new

Available on KSExcel and KSWord.

async ksGenerateFromSharePoint(
sharePointItemPath: string,
data: object,
options?: {
filename?: string;
linkedEntityId?: string;
linkedSharePointPath?: string;
outputFormat?: string;
}
)Promise<{ base64: string, contentDocumentId?: string, sharePointFileUrl?: string }>
ParamDescription
sharePointItemPathPath to the template, relative to the drive root, e.g. Shared Documents/Templates/Quote.xlsx
dataMerge context — same token syntax as Salesforce-hosted templates
options.linkedSharePointPathFolder to upload the result into
options.linkedEntityIdSalesforce record to attach the Salesforce File to

How SharePoint auth works

Keelstone uses app-only credentials (client credentials flow) — no per-user OAuth or delegated permissions. The Azure AD app must have Sites.ReadWrite.All as an application permission (not delegated) with admin consent granted.

Tokens are cached in the server process for 55 minutes (tokens are valid for 60 min; Keelstone refreshes 5 minutes early). The SharePoint site's drive ID is resolved once and cached in the org's configuration to avoid a Graph API lookup on every generation request.