Skip to main content
This how-to explains how an integration detects available Export Jobs in Pleo and claims a job for processing. Detecting and claiming 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 claimed by any integration. Your integration must:
  • Detect available Export Jobs
  • Select the appropriate job to process
  • Claim 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. Two 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. Filter Eligible Export Jobs

API Endpoint: GET /v3/export-jobs Example parameters: companyId: 12abc3d4-e567-890e-1234-abc56e78fabc Fetch Export Jobs from the API and filter for eligible statuses (pending and in_progress ) only. Your integration must ignore:
  • failed jobs
  • completed jobs
  • completed_with_errors jobs
Example Pseudo:
jobs = fetchExportJobs()

eligibleJobs = filter jobs where status in ["pending", "in_progress"]

if eligibleJobs is empty:
    exit workflow

Example Request

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",
          "in_progress"
        ]
      }
    },
    "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 Is Not Already Being Processed

API Endpoint: GET /v3/export-jobs/{jobId} Example parameters: jobId: 8eb648ab-464b-42a0-ba17-eda703657e33 Before claiming the job, confirm another worker or integration instance has not already started processing it. Example:
if job.status == "pending":
    continue
else:
    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. Claim the Export Job

API Endpoint: POST /v3/export-job-events Once verified, mark the Export Job event as started. This signals 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 Claim Conflicts Safely

In distributed systems, multiple workers may attempt to claim the same job. If the API returns a 422 INVALID_EXPORT_JOB_STATUS_CHANGE response, the job has already been claimed by another worker.
  • Treat the response as expected behaviour
  • Do not retry aggressively
  • Fetch jobs again and continue normally
Example:
try:
    claim(job)
catch ConflictError:
    log("Job already claimed")
    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 claiming 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 claimed responsibility for processing
  • The export workflow can proceed

What Comes Next?


this how-to is part of: