---
title: "Connect a job source to the import"
manual: "Job board"
version: "main"
permalink: "https://docs.typo3.org/permalink/jweiland/jobboard:developer-import-api@main"
source: "Developer/ImportApi.rst"
rendered: "2026-10-22T13:15:25+00:00"
---

# Connect a job source to the import {#developer-import-api}

The [job import](https://docs.typo3.org/permalink/jweiland/jobboard:import@main) reads jobs from XML sources. Each source is
a PHP class that implements
`\JWeiland\Jobboard\ApiModel\ApiModelInterface` and is tagged with
`api.model`. The extension ships no source, add your classes to a
site package.

## Architecture {#developer-import-architecture}

**Flow of the console command jobboard:import:jobs:mhm**

```text
ImportJobsMhm (console command, argument: storage page UID)
 └── ImportService::import()
      ├── for each service tagged "api.model"
      │    ├── XmlClient: GET request to getApiEndpoint()
      │    │    (failed request or invalid XML: source skipped,
      │    │     all its jobs are kept)
      │    ├── every child element of the XML root element = one job
      │    │    (jobs without vacancy_id are skipped)
      │    ├── TtAddressService: address of the primary location
      │    ├── JobService: job record built from getMapping()
      │    └── DataHandler: save address and job
      └── DataHandler: delete imported jobs that a successfully
                       fetched source no longer delivers
```

## Create a source class {#developer-import-class}

**EXT:my_sitepackage/Classes/Import/ExampleJobBoardApiModel.php**

```php
<?php

declare(strict_types=1);

/*
 * This file is part of the package my-vendor/my-sitepackage.
 *
 * For the full copyright and license information, please read the
 * LICENSE file that was distributed with this source code.
 */

namespace MyVendor\MySitepackage\Import;

use JWeiland\Jobboard\ApiModel\ApiModelInterface;
use JWeiland\Jobboard\ApiModel\ApiModelTrait;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(tags: ['api.model'], shared: false)]
final class ExampleJobBoardApiModel implements ApiModelInterface
{
    use ApiModelTrait;

    private const NAME = 'example';

    private const API_ENDPOINT = 'https://example.org/api/jobs.xml';

    private const MAPPING = [
        'title' => [
            'apiPath' => 'title/de',
            'isDate' => false,
            'default' => '',
        ],
        'description' => [
            'apiPath' => 'description/de',
            'isDate' => false,
            'default' => '',
        ],
        'reference_number' => [
            'apiPath' => 'vacancy_id',
            'isDate' => false,
            'default' => '',
        ],
        'employer' => [
            'apiPath' => 'company/de',
            'isDate' => false,
            'default' => '',
        ],
        'email' => [
            'apiPath' => 'application_email',
            'isDate' => false,
            'default' => '',
        ],
        'link' => [
            'apiPath' => 'publication_link',
            'isDate' => false,
            'default' => '',
        ],
        'start_date' => [
            'apiPath' => 'start_date',
            'isDate' => true,
            'default' => 0,
        ],
        'application_deadline' => [
            'apiPath' => 'application_deadline',
            'isDate' => true,
            'default' => 0,
        ],
        'salary_mode' => [
            'apiPath' => 'salary_mode',
            'isDate' => false,
            'default' => 1,
        ],
        'salary_min' => [
            'apiPath' => 'salary/min',
            'isDate' => false,
            'default' => 0,
        ],
        'salary_max' => [
            'apiPath' => 'salary/max',
            'isDate' => false,
            'default' => 0,
        ],
        'vacancy_id' => [
            'apiPath' => 'vacancy_id',
            'isDate' => false,
            'default' => 0,
            'prefix' => self::NAME,
        ],
    ];

    private int $storagePid = 0;
}

```

The trait `\JWeiland\Jobboard\ApiModel\ApiModelTrait` implements all
methods of the interface. It needs the class constants `NAME`,
`API_ENDPOINT` and `MAPPING` and the property
`private int $storagePid = 0;`, which the trait does not declare.

-   **`NAME`**

    Short, unique name of the source. It is used as prefix of the vacancy
    ID, for example `example_4711`.

-   **`API_ENDPOINT`**

    URL of the XML file. It is requested with the TYPO3
    `RequestFactory`, so the HTTP settings of
    `$GLOBALS['TYPO3_CONF_VARS']['HTTP']` (timeout, proxy) apply.

-   **`MAPPING`**

    Maps XML values to columns of the table
    `tx_jobboard_domain_model_job`, see
    [The mapping](https://docs.typo3.org/permalink/jweiland/jobboard:developer-import-mapping@main).

> [!WARNING]
> **Attention**
>
> Do not commit endpoints with credentials or internal URLs to public
> repositories.

## Register the class {#developer-import-register}

With the attribute `#[Autoconfigure(tags: ['api.model'], shared:
false)]`, the class is registered automatically, if the
[`Configuration/Services.yaml`](https://docs.typo3.org/m/typo3/reference-coreapi/13.4/en-us/ExtensionArchitecture/FileStructure/Configuration/ServicesYaml.html#file-extension-configuration-services-yaml) of your site package enables
`autoconfigure` for this class. Without the attribute, tag the class
in `Services.yaml`:

**EXT:my_sitepackage/Configuration/Services.yaml**

```yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  MyVendor\MySitepackage\:
    resource: '../Classes/*'

  # Only needed without the #[Autoconfigure] attribute
  MyVendor\MySitepackage\Import\ExampleJobBoardApiModel:
    shared: false
    tags:
      - name: api.model

```

Flush all caches after adding or removing a source, because the list of
sources is compiled into the dependency injection container:
`vendor/bin/typo3 cache:flush`.

## The mapping {#developer-import-mapping}

Each key of `MAPPING` is a column of
`tx_jobboard_domain_model_job`. Each value is an array with these
keys:

-   **`apiPath` (required)**

    XPath of the value, relative to the job element, for example
    `title/de`.

-   **`isDate` (required)**

    `true` converts the value to a Unix timestamp. Supported formats
    are `Y-m-d H:i:s O`, `Y-m-d H:i:s` and `Y-m-d`. An
    empty value or `0` results in `0`.

-   **`default` (required)**

    Used if the XML element is missing, or if the value cannot be
    converted, for example a date in an unknown format. The default is
    stored as it is, without date conversion and without prefix. An XML
    element that exists but is empty stays empty.

-   **`prefix` (optional)**

    Stored as `<prefix>_<value>`. For `vacancy_id`, the prefix
    must be the `NAME` of the source, because the import finds
    existing jobs by `<NAME>_<vacancy ID>` and recognizes the jobs of
    a source by this prefix.

These columns are set by the import itself and need no mapping:
`pid` (the storage page of the command), `is_import`,
`address`, `job_areas` and `job_type`.

## Expected XML structure {#developer-import-xml}

The services of the import expect some elements at fixed paths:

**Example XML of a source**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<jobs>
    <job>
        <vacancy_id>4711</vacancy_id>
        <title><de>Web Developer (m/f/d)</de></title>
        <description><de>&lt;p&gt;We are looking for ...&lt;/p&gt;</de></description>
        <company><de>Sample Company Ltd.</de></company>
        <application_email>jobs@example.org</application_email>
        <publication_link>https://example.org/jobs/4711</publication_link>
        <start_date>2026-12-01</start_date>
        <application_deadline>2026-11-15 00:00:00 +0100</application_deadline>
        <salary_mode>1</salary_mode>
        <salary>
            <min>4200.00</min>
            <max>5600.00</max>
        </salary>
        <!-- Titles of occupational fields, one per line -->
        <custom_select_4><de>IT
Administration</de></custom_select_4>
        <!-- Title of the employment scope -->
        <custom_select_2><de>Full-time</de></custom_select_2>
        <manager_name>Jane Doe</manager_name>
        <manager_email>jobs@example.org</manager_email>
        <locations>
            <location primary="true">
                <technical_name>location-1</technical_name>
                <de>Sample Company Ltd.</de>
                <street>Example Street 1</street>
                <zipcode>12345</zipcode>
                <city>Sample Town</city>
            </location>
        </locations>
    </job>
</jobs>

```

-   **Root element**

    Any name. Each child element is one job.

-   **`vacancy_id`**

    ID of the job in the source, any string. Surrounding whitespace is
    removed. Required to update existing jobs instead of creating new ones;
    jobs without vacancy ID are skipped.

-   **`custom_select_4/de`**

    Titles of the occupational fields, one per line.

-   **`custom_select_2/de`**

    Title of the employment scope.

-   **`locations/location[@primary="true"]`**

    The primary location, which becomes the workplace address. Its
    `technical_name` is stored as import key of the address,
    `de` as company, plus `street`, `zipcode` and
    `city`. A job without primary location is skipped.

-   **`manager_name`, `manager_email`**

    Name and email of the address.

Occupational fields and employment scopes are found by their exact title
in the table of the job board. Titles without a matching record are
skipped, records are not created.

## Salary information of imported jobs {#developer-import-salary}

Imported jobs are only shown with salary information, like all jobs. Map
`salary_mode` to an XML element with the value `1` (free salary
entry) and map `salary_min` and `salary_max`. Required fields of
the backend form, like the reference number or the start date, are not
checked by the import.
