Ledgentic Partner Integration Guide
Integrate your application with Ledgentic to automate supplier-invoice ingestion, bookkeeping analysis, and status synchronization across your own systems.
Introduction
The Partner Integration API enables you to upload supplier invoices, receive status updates through signed webhooks, and fetch granular bookkeeping results once processing is finished. This guide walks you through the concepts, workflow, and endpoints required for a full integration scenario.
Prerequisites
Make sure you have the following prerequisites before you start:
- Organizations – You need to have at least one organization in the admin interface.
- API Key – Generated an API key in the admin interface.
- Organization External ID Mappings: Make sure you add the external mapping ID to each Organization in the amdin interface that you want to use the API for.
- Webhooks: Set a webhook url and save the webhook sercret securely. Child organizations can inherit a parent webhook which is the recommended approach.
- API Key Access Scopes: Make sure the api key has the correct access scopes to the parent and child orginaizations that will use the API.
Key Concepts
- Organizations & delegated access – An API key is scoped to a root organization and can optionally include access to selected child organizations. Configure child access for each key inside the Ledgentic admin app before calling the API.
- External organization ID (
externalOrgId) – Provision the identifier in the Ledgentic admin interface during onboarding before you attempt any API calls. Provide it as the path parameter on every request so traffic is routed to the correct organization. - Invoice external ID (
externalInvoiceId) – The unique identifier for each supplier invoice in your system. Supply it in the payload when creating an invoice and reuse the same value wherever the API path references{externalInvoiceId}. - Signed webhooks – When processing completes, Ledgentic calls the webhook configured for the organization in the admin interface (child organizations can inherit a parent webhook). Each payload is signed with the webhook secret and delivered in the
X-Webhook-Signatureheader (t={timestamp},v1={signature}); verify it before acting on the request. - Processing lifecycle – Invoices transition through data extraction, accounting analysis, optional review, and completion. Use the provided endpoints to monitor progress and consume the bookkeeping output.
Authentication & Base URL
All endpoints accept API key authentication only. Include your key on every request via the X-API-Key header.
Base URL: https://api.ledgentic.com
Header: X-API-Key: YOUR_API_KEY
Rate Limiting
Partner endpoints enforce per-API-key rate limits. When a limit is exceeded the API returns HTTP 429 along with a Retry-After header.
- Default production quota (subject to change): 500 requests per minute per API key. Contact us if you need a higher quota.
- Upload, read, and change-request endpoints share the same quota; distribute calls accordingly.
- Implement client-side backoff when you receive HTTP 429 responses and surface metrics so you can request higher limits if needed.
Integration Workflow
- Submit invoice for processing – Upload the supplier invoice document for a given
externalOrgId. - Handle the completion webhook – Ledgentic notifies your webhook endpoint when processing finishes (successfully or with errors).
- Retrieve bookkeeping data – Fetch the processed invoice and journal entry data using the original
externalInvoiceId.
Repeat the workflow for each invoice you need to process. You can also list invoices, submit bookkeeping change requests, and delete drafts when required.
Submit a Supplier Invoice
Endpoint
POST /partner/v2/organizations/{externalOrgId}/supplier-invoices
Ensure the organization is onboarded and the externalOrgId is registered in the admin interface before making this request.
curl -X POST \
"https://api.ledgentic.com/partner/v2/organizations/acme-001/supplier-invoices" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalInvoiceId": "inv-87342",
"fileName": "supplier_invoice_87342.pdf",
"base64File": "JVBERi0xLjQKJcfs..."
}'
| Field | Type | Required | Description |
|---|---|---|---|
externalInvoiceId | string | Yes | Your unique invoice identifier (externalInvoiceId). |
fileName | string | Yes | Name of the uploaded invoice file. |
base64File | string | Yes | Base64-encoded binary contents (PDF, image). |
Response 201
{
"id": "inv_abc123",
"externalOrgId": "acme-001",
"fileName": "supplier_invoice_87342.pdf",
"state": "PENDING",
"status": "NOT_STARTED",
"createdAt": "2024-01-15T10:30:00Z",
"message": "Invoice uploaded successfully for organization 'acme-001'",
"metadata": {
"source": "PARTNER_API",
"partnerOrgId": "partner-42",
"uploadedAt": "2024-01-15T10:30:00Z"
}
}
Note: Batch uploads are not yet supported. Submit one invoice per request and parallelize responsibly within your rate limits.
Supplier Invoice States
The state field represents the business-level state of an invoice. Use this field for all business logic decisions.
| State | Phase | What It Means | Your Action |
|---|---|---|---|
PENDING | Processing | Invoice uploaded, waiting to start | Wait for webhook |
PROCESSING | Processing | AI analysis in progress | Wait for webhook |
PROCESSED | Processing | Fully processed | Call GET /bookkeeping |
PARTIALLY_PROCESSED | Processing | Partially processed | Call GET /bookkeeping (some data available) |
ERROR | Processing | Failed | Check error, re-process if needed |
INVALID | Processing | Not an invoice | Delete and re-upload correct doc |
NEEDS_REVIEW | Processing | Human review needed | Wait for manual resolution |
IN_APPROVAL | Approval | Awaiting approval | Wait (or approve via UI) |
APPROVED | Approval | Approved | Ready for payment/export |
REJECTED | Approval | Rejected | Review and resubmit |
CANCELLED | Approval | Cancelled | No action needed |
PAID | Payment | Payment complete | Archive |
PARTIALLY_PAID | Payment | Partial payment | Track remaining |
OVERDUE | Payment | Past due date | Follow up |
Note: The
analysisStatusfield provides internal processing details and should only be used for diagnostics. See Analysis Status Reference if needed.
Webhook Events
Ledgentic delivers webhook events to notify your system of invoice processing updates. Configure webhook endpoints in the Ledgentic admin app and store the webhook secret securely.
Event Types
| Event Type | Description |
|---|---|
PROCESSING_COMPLETE | Recommended. Unified event for all terminal processing states. |
INVOICE_RECEIVED | Invoice received and queued for processing. |
OCR_COMPLETED | OCR extraction completed successfully. |
SUPPLIER_MATCHED | Supplier identified and matched to master data. |
BOOKKEEPING_READY | Journal entry ready for retrieval. |
PROCESSING_ERROR | Processing failed at some step. |
INVALID_DOCUMENT | Document classified as not a valid invoice. |
TEST | Test webhook for connectivity verification. |
Recommendation: Subscribe to
PROCESSING_COMPLETEfor a simplified integration. This event fires for all terminal states (PROCESSED,PARTIALLY_PROCESSED,ERROR,INVALID) and includes all necessary data to determine the next action.
Webhook Payload Structure
{
"eventType": "PROCESSING_COMPLETE",
"externalOrgId": "acme-001",
"externalInvoiceId": "inv-87342",
"state": "PROCESSED",
"analysisStatus": "SUCCESS",
"message": "Invoice processing completed successfully, ready for sync",
"dataEndpoint": "/partner/v2/organizations/acme-001/supplier-invoices/inv-87342/bookkeeping",
"timestamp": "2024-01-15T10:45:00Z",
"eventId": "wh_evt_123456"
}
Webhook Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
eventType | string | Yes | The type of event (see Event Types table above). |
externalOrgId | string | Yes | Your organization identifier. |
externalInvoiceId | string | Yes | Your invoice identifier (the externalInvoiceId you provided at upload). |
invoiceId | string | Yes | Internal Ledgentic invoice ID. Deprecated — use externalInvoiceId. |
journalEntryId | string | No | Journal entry ID, if available. Deprecated — available in bookkeeping response. |
status | string | Yes | Event outcome (e.g. SUCCESS, PROCESSING_ERROR, INVALID_DOCUMENT). Deprecated — use state. |
state | string | No | Current business state of the invoice (see Supplier Invoice States). Recommended. |
analysisStatus | string | No | Detailed processing status (see Analysis Status Reference). |
message | string | No | Human-readable description of the event (includes error details on failure). |
error | string | No | Error message if processing failed. Deprecated — use message. |
failedStep | string | No | Step that failed during processing. Deprecated — use analysisStatus. |
dataEndpoint | string | Yes | API path to retrieve the bookkeeping data. |
timestamp | string | Yes | ISO 8601 timestamp when the event occurred. |
eventId | string | No | Unique identifier for this webhook event (use for idempotency). |
Handling PROCESSING_COMPLETE
The PROCESSING_COMPLETE event is the recommended way to handle invoice processing results. Use the state field to determine the appropriate action:
app.post("/webhooks/ledgentic", async (req, res) => {
const rawBody = req.rawBody;
const signature = req.headers["x-webhook-signature"];
// Verify signature first (see Webhook Signature Verification)
const verification = verifyWebhookSignature(rawBody, signature, WEBHOOK_SECRET);
if (!verification.valid) {
return res.status(401).json({ error: verification.reason });
}
const event = req.body;
// Use eventId for idempotency
if (await isEventAlreadyProcessed(event.eventId)) {
return res.status(200).json({ status: "already_processed" });
}
if (event.eventType === "PROCESSING_COMPLETE") {
switch (event.state) {
case "PROCESSED":
case "PARTIALLY_PROCESSED":
// Data available - fetch the bookkeeping data
// PARTIALLY_PROCESSED means some steps completed; data is still usable
const bookkeepingData = await fetchBookkeeping(
event.externalOrgId,
event.externalInvoiceId
);
await syncToAccountingSystem(bookkeepingData);
break;
case "ERROR":
// Processing failed early
console.error(`Invoice ${event.externalInvoiceId} failed: ${event.message}`);
await handleProcessingError(event);
break;
case "INVALID":
// Not a valid invoice document
console.warn(`Document ${event.externalInvoiceId} is not a valid invoice`);
await markAsInvalidDocument(event);
break;
default:
console.log(`Unhandled invoice state: ${event.state}`);
}
}
await markEventAsProcessed(event.eventId);
res.status(200).json({ status: "ok" });
});
Example Webhook Payloads
PARTIALLY_PROCESSED example (some steps completed, data available):
{
"eventType": "PROCESSING_COMPLETE",
"externalOrgId": "acme-001",
"externalInvoiceId": "inv-87342",
"state": "PARTIALLY_PROCESSED",
"analysisStatus": "ACCOUNTING_ERROR",
"message": "Invoice data extracted, journal entry incomplete — unable to determine VAT classification for line item",
"dataEndpoint": "/partner/v2/organizations/acme-001/supplier-invoices/inv-87342/bookkeeping",
"timestamp": "2024-01-15T10:45:00Z",
"eventId": "wh_evt_123457"
}
Note: Even with
PARTIALLY_PROCESSED, thedataEndpointis available. You can fetch extracted invoice data; only specific steps (like journal entry generation) may be incomplete.
ERROR example (early processing failure):
{
"eventType": "PROCESSING_COMPLETE",
"externalOrgId": "acme-001",
"externalInvoiceId": "inv-87343",
"state": "ERROR",
"analysisStatus": "OCR_ERROR",
"message": "Failed to extract text from document — document appears to be corrupted or encrypted",
"dataEndpoint": "/partner/v2/organizations/acme-001/supplier-invoices/inv-87343/bookkeeping",
"timestamp": "2024-01-15T10:46:00Z",
"eventId": "wh_evt_123458"
}
INVALID example (not a valid invoice):
{
"eventType": "PROCESSING_COMPLETE",
"externalOrgId": "acme-001",
"externalInvoiceId": "inv-87344",
"state": "INVALID",
"analysisStatus": "SUCCESS",
"message": "Document classified as not a valid invoice — document appears to be a receipt, not a supplier invoice",
"dataEndpoint": "/partner/v2/organizations/acme-001/supplier-invoices/inv-87344/bookkeeping",
"timestamp": "2024-01-15T10:47:00Z",
"eventId": "wh_evt_123459"
}
Webhook Signature Verification
Ledgentic computes the webhook signature as HMAC_SHA256( {timestamp}.{raw_request_body} ) with your webhook secret.
- Read the
X-Webhook-Signatureheader and parset={timestamp},v1={signature}pairs. - Concatenate the timestamp and raw JSON body with a dot separator to form the signed message.
- Hash the message with HMAC-SHA256 using your webhook secret.
- Compare the computed digest with the provided signature using a constant-time equality check.
- Reject requests where the timestamp is older than your configured tolerance (typically ≤5 minutes) to prevent replay attacks.
Node.js example
import crypto from "node:crypto";
export function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const parts = signatureHeader.split(",");
const timestampPart = parts.find((part) => part.startsWith("t="));
const signaturePart = parts.find((part) => part.startsWith("v1="));
if (!timestampPart || !signaturePart) {
return { valid: false, reason: "Missing timestamp or signature" };
}
const timestamp = timestampPart.replace("t=", "");
const timestampNumber = Number(timestamp);
if (!Number.isFinite(timestampNumber)) {
return { valid: false, reason: "Invalid timestamp" };
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestampNumber) > 5 * 60) {
return { valid: false, reason: "Timestamp outside tolerance" };
}
const providedSignature = signaturePart.replace("v1=", "");
const message = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(message)
.digest("hex");
const providedBuffer = Buffer.from(providedSignature, "hex");
const expectedBuffer = Buffer.from(expectedSignature, "hex");
const signaturesMatch =
providedBuffer.length === expectedBuffer.length &&
crypto.timingSafeEqual(providedBuffer, expectedBuffer);
return { valid: signaturesMatch, timestamp: timestampNumber };
}
Implement these checks in your webhook handler using your preferred language and ensure stale or invalid signatures are rejected before business logic runs.
Retrieve Processed Invoice & Bookkeeping Data
Endpoint
GET /partner/v2/organizations/{externalOrgId}/supplier-invoices/{externalInvoiceId}/bookkeeping
Use your invoice externalInvoiceId in place of {externalInvoiceId} when building the request URL.
curl \
"https://api.ledgentic.com/partner/v2/organizations/acme-001/supplier-invoices/inv-87342/bookkeeping" \
-H "X-API-Key: YOUR_API_KEY"
Bookkeeping Response Structure
The response contains four main objects:
supplierInvoice- Extracted invoice data including amounts, dates, line items, and payment methodssupplier- Matched supplier master data (name, VAT number, address)bookkeeping- Aggregated journal entry ready for GL postingdetailedBookkeeping- Pre-aggregation view with line-item detail (useful for reconciliation)
{
"supplierInvoice": { /* Invoice details */ },
"supplier": { /* Supplier information */ },
"bookkeeping": { /* Aggregated journal entry */ },
"detailedBookkeeping": { /* Pre-aggregation view */ }
}
Tip: Use the
statefield onsupplierInvoiceto determine if data is ready. See the API Reference for complete field documentation.
List Supplier Invoices
Endpoint
GET /partner/v2/organizations/{externalOrgId}/supplier-invoices
Use this endpoint to page through all invoices for an organization or filter by state.
| Query | Type | Description |
|---|---|---|
page | number | 1-based page index (default: 1). |
limit | number | Items per page (default: 20, max: 100). |
state | string | Recommended. Filter by invoice state (see Supplier Invoice States). |
status | string | Deprecated. Filter by analysis status. Use state instead. |
showAll | boolean | Set to true to include invoices without an external ID (default: false). |
The response returns an items array and meta pagination object so you can iterate through result pages.
Submit Bookkeeping Change Requests
When you need to adjust extracted data or journal entries, submit a change request.
Endpoint
PATCH /partner/v2/organizations/{externalOrgId}/supplier-invoices/{externalInvoiceId}/bookkeeping
Use the same externalInvoiceId value you supplied during upload in the {externalInvoiceId} placeholder.
curl -X PATCH \
"https://api.ledgentic.com/partner/v2/organizations/acme-001/supplier-invoices/inv-87342/bookkeeping" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"supplierInvoice": {
"invoiceNumber": "87342",
"dueDate": "2025-05-28",
"currency": "SEK"
},
"bookkeeping": {
"lines": [
{
"accountNumber": "2440",
"credit": "12500.00",
"description": "Accounts payable"
}
]
}
}'
The API responds with a CreateChangeRequestResponseDto containing the change request ID and current status. You can poll for updates using:
GET /partner/v2/organizations/{externalOrgId}/supplier-invoices/{externalInvoiceId}/change-requestsGET /partner/v2/organizations/{externalOrgId}/supplier-invoices/{externalInvoiceId}/change-requests/{changeRequestId}
Use your invoice externalInvoiceId for the {externalInvoiceId} placeholder when calling these endpoints.
Multiple change requests can coexist for the same invoice. When a new request is submitted, it joins the review queue independently—track each changeRequestId until it resolves to APPLIED, REJECTED, or EXPIRED.
Delete an Invoice
Delete invoices that are still in draft bookkeeping state with:
DELETE /partner/v2/organizations/{externalOrgId}/supplier-invoices/{externalInvoiceId}
Replace {externalInvoiceId} with your invoice externalInvoiceId to target the correct record.
A successful deletion returns HTTP 200. Attempts to delete invoices with posted or reviewed journal entries result in HTTP 403, and unknown invoices return HTTP 404.
Analysis Status Reference (Internal)
Deprecation Notice: The
analysisStatusfield provides internal processing details and should be used for diagnostics only. For business logic and workflow decisions, use thestatefield instead (see Supplier Invoice States).
Processing States
These statuses indicate active processing steps:
| Status | Meaning |
|---|---|
NOT_STARTED | Invoice accepted and queued. |
SUPPLIER_EXTRACTION_PROCESSING | Supplier data extraction in progress. |
OCR_PROCESSING | OCR is running. |
PROCESSING | Bookkeeping analysis running. |
ACCOUNTING_PROCESSING | Journal entry construction in progress. |
Terminal States
These statuses indicate processing has completed or failed:
| Status | Meaning |
|---|---|
SUCCESS | Processing complete; data available. |
OCR_DONE | OCR finished successfully. |
ACCOUNTING_DONE | Accounting step finished. |
WAITING_FOR_HUMAN | Manual review required. |
Error States
| Status | Meaning |
|---|---|
SUPPLIER_EXTRACTION_ERROR | Supplier extraction failed. |
OCR_ERROR | OCR failed. |
PROCESSING_ERROR | Bookkeeping analysis failed. |
ACCOUNTING_ERROR | Accounting step failed. |
ERROR | General processing failure. |
Migration Guide
This section helps existing integrators update their implementations to use the v2 API fields and patterns.
Field Renames
The following fields have been renamed for clarity:
| Old Field | New Field | Notes |
|---|---|---|
externalId | externalInvoiceId | In webhook payloads and request body. |
bookkeepingEndpoint | dataEndpoint | In webhook payloads. |
completedAt | timestamp | In webhook payloads. |
Deprecated Fields
The following fields are still present in webhook payloads but are deprecated and will be removed in a future version. Migrate to the recommended replacements at your convenience.
| Deprecated Field | Replacement | Notes |
|---|---|---|
invoiceId | externalInvoiceId | Use externalInvoiceId to reference invoices. |
journalEntryId | — | Available in the bookkeeping response instead. |
status | state | Use state for business logic decisions. |
error | message | Error details are now included in message. |
failedStep | analysisStatus | Use analysisStatus for diagnostics. |
analysisStatus (in API responses) | state | Use state for workflow; keep analysisStatus for diagnostics only. |
New Fields Added
The following fields are new in v2:
| Field | Location | Description |
|---|---|---|
eventType | Webhook payload | Type of webhook event. |
state | Webhook payload & API responses | Business state of the invoice. |
message | Webhook payload | Human-readable event description (includes error details on failure). |
eventId | Webhook payload | Unique event ID for idempotency. |
Migration Checklist
- Update webhook handlers to parse the new payload structure (new fields are additive)
- Start using
stateinstead ofstatusfor determining next actions (both fields are still sent) - Start using
messageinstead oferrorfor failure details (both fields are still sent) - Migrate away from deprecated fields (
invoiceId,journalEntryId,status,error,failedStep) — they are still present but will be removed in a future version - Implement idempotency using the
eventIdfield - Update list queries to use
stateparameter instead ofstatus - Handle new event types especially
PROCESSING_COMPLETE
Error Handling & Retries
- Rely on HTTP status codes for coarse error handling (
400invalid payload,401/403authentication issues,404resource missing,429rate limit,500unexpected error). - Responses include structured error payloads to help you recover quickly; log them for diagnostics.
- Webhooks should be idempotent. If Ledgentic does not receive a 2xx response, it will retry delivery with exponential backoff. Always verify the signature on each retry.
- HTTP 429 responses contain a
Retry-Afterheader (seconds) you can honor before resubmitting.