Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.pleo.io/llms.txt

Use this file to discover all available pages before exploring further.

This how-to explains how an integration detects available Export Jobs in Pleo and starts a job for processing. Detecting and starting an Export Job is the first step in the export workflow. Export Jobs group expenses that are ready to be exported to an Accounting System. At this stage, an Export Job exists with status pending and has not yet been started by any integration. Your integration must:
  • Detect available Export Jobs
  • Select the appropriate job to process
  • Start the job by marking it as started
This ensures export jobs are processed in a controlled and predictable way.

Prerequisites

Before you begin:

Steps

1. Detect Available Export Jobs

Detect new Export Jobs as they become available. Three discovery strategies are supported: Subscribe to the export-job.created webhook. When the export-job.created event is received, proceed to step 2.

Option B — Polling

If webhooks are not supported, periodically request Export Jobs. Polling should run on a controlled schedule (for example, every few minutes). See step 2 for an example request.

Option C — Ad Hoc Trigger (Optional)

If your integration supports user-initiated processing, expose a manual trigger that immediately runs the same polling logic as Option B. This option is used alongside scheduled polling — not instead of it. It allows users to check for pending Export Jobs on demand rather than waiting for the next scheduled interval. When the trigger fires, proceed to step 2.

2. Confirm Eligible Job Status

API Endpoint: GET /v3/export-jobs Example parameters: companyId: 12abc3d4-e567-890e-1234-abc56e78fabc Request pending Export Jobs from the API. If recovering from an interruption, also include in_progress to locate a job previously started but not finished. Example Pseudo:
// Normal operation
jobs = fetchExportJobs(statuses=["pending"])

// On startup or after reconnection: also check for an interrupted job
if isRecoveryStart:
    jobs = fetchExportJobs(statuses=["pending", "in_progress"])

if jobs is empty:
    exit workflow

Example Request — Normal Operation

curl -X GET "https://external.staging.pleo.io/v3/export-jobs?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&statuses=pending" \
  -H "Authorization: Bearer <access_token>"

Example Request — Recovery

curl -X GET "https://external.staging.pleo.io/v3/export-jobs?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&statuses=pending&statuses=in_progress" \
  -H "Authorization: Bearer <access_token>"

Example Response

{
  "data": [
    {
      "id": "8eb648ab-464b-42a0-ba17-eda703657e33",
      "createdBy": "f1b5d950-1dbd-4493-8e8c-59fcfe13964f",
      "companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
      "numberOfItems": 4,
      "status": "pending",
      "expiresIn": 3600,
      "createdAt": "2026-04-13T14:36:50Z",
      "startedAt": null,
      "lastUpdatedAt": "2026-04-13T14:36:50Z",
      "completedAt": null,
      "expiredAt": null,
      "failureReasonType": null,
      "failureReason": null,
      "isInteractive": true,
      "vendorBasedBookkeeping": false
    }
  ],
  "pagination": {
    "hasPreviousPage": false,
    "hasNextPage": false,
    "currentRequestPagination": {
      "sortingKeys": [],
      "sortingOrder": [],
      "parameters": {
        "company_id": [
          "12abc3d4-e567-890e-1234-abc56e78fabc"
        ],
        "statuses": [
          "pending"
        ]
      }
    },
    "startCursor": "AAAAAADJ3T7YEMLAE3UA=R23ERK2GJNBKBOQX5WTQGZL6GM",
    "endCursor": "AAAAAADJ3T7YEMLAE3UA=R23ERK2GJNBKBOQX5WTQGZL6GM",
    "total": 1
  }
}


3. Select the Oldest Export Job

From the filtered results, always select the oldest eligible job. Never process jobs in parallel. Maintain a single export worker to guarantee sequential execution. Example Pseudo:
jobToProcess = sortByCreatedAt(eligibleJobs).first()

4. Verify the Job Status

API Endpoint: GET /v3/export-jobs/{jobId} Example parameters: jobId: 8eb648ab-464b-42a0-ba17-eda703657e33 Fetch the individual job to confirm its current status before proceeding. The status determines which path to take next. Example:
if job.status == "pending":
    // New job — proceed to Step 5 to start it
    continue to start job

elif job.status == "in_progress":
    // Recovery — this job was previously started by this integration
    // Skip Step 5 and proceed directly to pre-export validation
    continue to pre-export validation

else:
    // Job is completed, failed, or expired — nothing to do
    skip job

Example Request

curl -X GET "https://external.staging.pleo.io/v3/export-jobs/8eb648ab-464b-42a0-ba17-eda703657e33" \
  -H "Authorization: Bearer <access_token>"

Example Response

{
  "data": {
    "id": "8eb648ab-464b-42a0-ba17-eda703657e33",
    "createdBy": "f1b5d950-1dbd-4493-8e8c-59fcfe13964f",
    "companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
    "numberOfItems": 4,
    "status": "pending",
    "expiresIn": 3600,
    "createdAt": "2026-04-13T14:36:50Z",
    "startedAt": null,
    "lastUpdatedAt": "2026-04-13T14:36:50Z",
    "completedAt": null,
    "expiredAt": null,
    "failureReasonType": null,
    "failureReason": null,
    "isInteractive": true,
    "vendorBasedBookkeeping": false
  }
}

5. Start the Export Job

API Endpoint: POST /v3/export-job-events Only applies if job.status == "pending" (from Step 4). If the job is already in_progress, skip this step and proceed directly to pre-export validation. Mark the Export Job event as started to signal to Pleo that your integration has taken responsibility for processing. Example:
markExportJobStarted(job.id)

Example Request

curl -X POST "https://external.staging.pleo.io/v3/export-job-events" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json;charset=UTF-8" \
  -d '{
        "event": "started",
        "jobId": "8eb648ab-464b-42a0-ba17-eda703657e33"
      }'

Example Response

If the request succeeds, rerunning the Step 4 curl command will return an updated job status of in_progress.
{
  "status": "in_progress"
}

6. Handle Concurrent Start Conflicts Safely

Integrations are designed to run a single export worker — but infrastructure doesn’t always guarantee this. Rolling deployments, double-firing scheduled jobs, or a restarting worker can briefly produce two instances that both attempt to start the same job at the same moment. If the API returns a 422 INVALID_EXPORT_JOB_STATUS_CHANGE response, the job has already been started by another instance.
  • Treat the response as expected behaviour
  • Do not retry aggressively
  • Fetch jobs again and continue normally
Example:
try:
    markJobStarted(job)
catch ConflictError:
    log("Job already started")
    restart discovery cycle

7. Process the Job in a Timely Manner

Export jobs expire after expiresIn seconds (e.g., 3600) since the last state-changing update (lastUpdatedAt). To avoid expiration:
  • Begin processing immediately after starting the job
  • Ensure progress is made before the expiry window elapses
  • Update the job status as work progresses to refresh lastUpdatedAt (e.g., move from pending to in_progress)
If a job expires:
  • The status moves to expired
  • The job must be resubmitted from Pleo’s Web App
  • A new job with a new jobId is added to the queue
Integrations should ensure their processing is idempotent, as items from an expired job may be included again in a newly submitted job.

Result

After completing these steps:
  • An Export Job has been safely detected
  • The oldest eligible job has been selected
  • Your integration has taken responsibility for processing
  • The export workflow can proceed

What Comes Next?


this how-to is part of: