> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twine.se/llms.txt
> Use this file to discover all available pages before exploring further.

# Hibob

export const IntegrationMeta = ({homepageUrl, apiReferenceUrls, contactEmail, transport, status, domains, capabilities, notes}) => {
  const hasLinks = homepageUrl || apiReferenceUrls && apiReferenceUrls.length > 0 || contactEmail;
  const hasDomains = domains && domains.length > 0;
  const hasTransport = !!transport;
  const hasStatus = status === 'planned' || status === 'deprecated';
  const hasCapabilities = capabilities && capabilities.length > 0;
  const hasNotes = !!notes;
  if (!hasLinks && !hasDomains && !hasTransport && !hasStatus && !hasCapabilities && !hasNotes) return null;
  const pill = (bg, color, text) => <span style={{
    display: 'inline-flex',
    alignItems: 'center',
    fontSize: '11px',
    fontWeight: 600,
    lineHeight: 1,
    padding: '3px 8px',
    borderRadius: '999px',
    background: bg,
    color: color,
    whiteSpace: 'nowrap'
  }}>
  {text}
</span>;
  const capabilityColors = {
    'read/write': ['var(--colors-accent-light, #ede9fe)', 'var(--colors-accent, #7c3aed)'],
    'read-write': ['var(--colors-accent-light, #ede9fe)', 'var(--colors-accent, #7c3aed)'],
    'read': ['rgb(var(--primary-light) / .15)', 'var(--colors-content-secondary, #6b7280)'],
    'write': ['rgb(var(--primary-light) / .15)', 'var(--colors-content-secondary, #6b7280)']
  };
  const transportColors = {
    'file': ['var(--colors-warning-light, #fef3c7)', 'var(--colors-warning, #b45309)'],
    'api': ['var(--colors-accent-light, #ede9fe)', 'var(--colors-accent, #7c3aed)'],
    'webhook': ['var(--colors-success-light, #dcfce7)', 'var(--colors-success, #16a34a)']
  };
  const statusConfig = {
    'planned': {
      colors: ['var(--colors-warning-light, #fef3c7)', 'var(--colors-warning, #b45309)'],
      text: 'Planned'
    },
    'deprecated': {
      colors: ['rgb(var(--primary-light) / .15)', 'var(--colors-content-secondary, #6b7280)'],
      text: 'Deprecated'
    }
  };
  const label = text => <span style={{
    fontSize: '11px',
    fontWeight: 600,
    textTransform: 'uppercase',
    letterSpacing: '0.05em',
    color: 'var(--colors-content-secondary, #6b7280)',
    whiteSpace: 'nowrap',
    alignSelf: 'start',
    paddingTop: '2px'
  }}>
  {text}
</span>;
  const cell = children => <div style={{
    display: 'flex',
    alignItems: 'center',
    flexWrap: 'wrap',
    gap: '8px'
  }}>
  {children}
</div>;
  return <>
<style>{`
  .integration-meta-link {
    font-size: 13px;
    color: var(--colors-primary);
    text-decoration: none !important;
    border-bottom: none !important;
    box-shadow: none !important;
    position: relative;
  }
  .integration-meta-link:hover {
    text-decoration: none !important;
    border-bottom: none !important;
    box-shadow: none !important;
  }
  .integration-meta-link::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: -1px;
    width: 100%;
    height: 1px;
    background: rgb(var(--primary-light));
    opacity: 0.5;
    transition: opacity 0.15s;
  }
  .integration-meta-link:hover::after {
    opacity: 1;
  }
`}</style>
<div style={{
    display: 'grid',
    gridTemplateColumns: 'max-content 1fr',
    alignItems: 'center',
    gap: '8px 16px',
    padding: '14px 16px',
    borderRadius: '8px',
    background: 'rgb(var(--primary-light) / .05)',
    border: '1px solid rgb(var(--primary-light) / .1)',
    marginBottom: '32px'
  }}>
{hasLinks && <>
{label('Links')}
{cell(<>
{homepageUrl && <a href={homepageUrl} target="\_blank" rel="noopener noreferrer" className="integration-meta-link">
Website
</a>}
{apiReferenceUrls && apiReferenceUrls.map(({url, label: l}) => <a key={url} href={url} target="\_blank" rel="noopener noreferrer" className="integration-meta-link">
{l || 'API Reference'}
</a>)}
{contactEmail && <a href={`mailto:${contactEmail}`} className="integration-meta-link">
{contactEmail}
</a>}
</>)}
</>}

      {hasStatus && <>
        {label('Status')}
        {cell(pill(...statusConfig[status].colors, statusConfig[status].text))}
      </>}

      {hasTransport && <>
        {label('Transport')}
        {cell(pill(...transportColors[transport] || transportColors['api'], transport === 'file' ? 'File' : transport === 'webhook' ? 'Webhook' : 'API'))}
      </>}

      {hasDomains && <>
        {label('Domains')}
        {cell(domains.map(({domain, capability}) => {
    const [capBg, capColor] = capabilityColors[capability] || capabilityColors['read'];
    return <span key={domain} style={{
      display: 'inline-flex',
      alignItems: 'stretch',
      borderRadius: '999px',
      overflow: 'hidden',
      whiteSpace: 'nowrap',
      fontSize: '12px',
      fontWeight: 600,
      lineHeight: 1
    }}>
              <span style={{
      padding: '4px 8px',
      background: 'rgb(var(--primary-light) / .3)',
      color: 'inherit',
      fontFamily: 'monospace'
    }}>{domain}</span>
              <span style={{
      padding: '4px 8px',
      background: capBg,
      color: capColor
    }}>{capability}</span>
            </span>;
  }))}
      </>}

      {hasCapabilities && <>
        {label('Capabilities')}
        <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '4px'
  }}>
          {capabilities.map(({label: capLabel, supported}) => <div key={capLabel} style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    fontSize: '13px'
  }}>
              <span style={{
    fontWeight: 700,
    fontSize: '12px',
    color: supported ? 'var(--colors-success, #16a34a)' : 'var(--colors-error, #dc2626)',
    width: '12px',
    flexShrink: 0
  }}>{supported ? '✓' : '✗'}</span>
              <span style={{
    color: 'inherit'
  }}>{capLabel}</span>
            </div>)}
        </div>
      </>}

      {hasNotes && <>
        {label('Notes')}
        <span style={{
    fontSize: '13px',
    color: 'var(--colors-content-secondary, #6b7280)',
    fontStyle: 'italic'
  }}>{notes}</span>
      </>}
    </div>

</>;
};

export const IntegrationHeader = ({name, category, logoUrl}) => <div style={{
  display: 'flex',
  alignItems: 'center',
  gap: '16px',
  marginBottom: '24px'
}}>
    {logoUrl && <div style={{
  width: '64px',
  height: '64px',
  borderRadius: '12px',
  background: '#ffffff',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  flexShrink: 0,
  boxShadow: '0 0 0 1px rgba(0,0,0,0.08)'
}}>
        <img src={logoUrl} alt={`${name} logo`} style={{
  width: '40px',
  height: '40px',
  objectFit: 'contain'
}} />
      </div>}
    <div>
      <h1 style={{
  margin: 0
}}>{name}</h1>
      <span style={{
  color: 'var(--colors-content-secondary)',
  fontSize: '14px'
}}>
        {category}
      </span>
    </div>
  </div>;

<IntegrationHeader name="Hibob" category="HRIS" logoUrl="https://kbmni.upcloudobjects.com/public/systemlogos/hibob-logo.png" />

<IntegrationMeta
  homepageUrl="https://www.hibob.com"
  apiReferenceUrls={[
{
  label: 'API',
  url: 'https://apidocs.hibob.com',
},
]}
  transport="api"
  domains={[
{ domain: 'employee', capability: 'read/write' },
{ domain: 'time_report', capability: 'read/write' },
{ domain: 'org_unit', capability: 'read' },
{ domain: 'schedule', capability: 'read' },
{ domain: 'file_transfer', capability: 'write' },
]}
  notes="Schedule information is extracted from Employee data. Writing to employees covers their own fields and new salary entries — see Writing employees to Hibob."
/>

<Note>
  This page is part of an early release. Detailed information about Twine's integration with this system is being prepared and will be filled in shortly.
</Note>

## Writing employees to Hibob

Hibob can be the **target** of an employee domain mapping, not just a source. Twine writes two
things: the employee's own fields, and new entries in their salary table.

| What                                                                                       | Twine can                                      |
| ------------------------------------------------------------------------------------------ | ---------------------------------------------- |
| Names, and the About, Home, Address, Personal, Financial, Identification and Work sections | Update mapped fields                           |
| Salary table                                                                               | **Add** entries, including historical ones     |
| Email address                                                                              | Update, but only if you turn it on — see below |
| Employment history, work history, bank accounts, variable payments, custom fields          | Read only                                      |

Only the fields you have mapped are written. Everything else on the employee is left exactly as it
is, so mapping one field will never blank the rest of the record.

### The employee must already exist in Hibob

Twine does not create people in Hibob. An employee has to exist there and be matched to the Twine
employee before anything is written; unmatched employees are reported instead of created.

This is because Hibob's API has no way to record a hire event. An employee created through it would
have no lifecycle history, and would immediately read back as inactive on the next sync. Create
people in Hibob, then let Twine keep their details up to date.

### Salary history can be added to, but not changed

Twine can add salary entries that Hibob is missing — including ones dated before every entry Hibob
already holds, which is the usual case when moving payroll history in from another system. Hibob
works out each entry's end date itself, so a backfilled salary slots into the timeline correctly.

Twine will **not** modify or remove an existing salary entry. Hibob offers no way to edit one in
place; the only route would be to delete it and add it back, and between those two steps Hibob
treats the employee as being back on their previous salary. Rather than risk that, a changed salary
is reported as a warning naming the employee and the date, and the entry is left for you to correct
in Hibob.

<Note>
  If a salary looks wrong in Hibob, fix it there. Twine will not fight you for it, and the next sync
  will not undo your correction.
</Note>

### Email addresses are not changed unless you ask

An employee's email address is their login identity in Hibob, so Twine leaves it alone by default
even when it is mapped and differs. Turn on **Allow email address updates** in the integration's
settings to let it through. Every other mapped field is written regardless of that setting.

### First runs are slow

Hibob accepts far fewer writes per minute than reads. A first run that backfills salary history
across a few hundred employees can take the better part of an hour. The sync is correct and picks up
where it left off — it is simply paced by Hibob. Later runs, which usually write little or nothing,
are unaffected.

## Documents

Twine can write documents to Hibob three ways, and Hibob files every one of them **under an
employee** — there is no organization-level document store, so a file with no employee cannot be
delivered here.

| Route                                                            | Where it is configured                              |
| ---------------------------------------------------------------- | --------------------------------------------------- |
| A [file transfer domain mapping](/platform/domain-mappings)      | Property mappings — see below                       |
| The [File Transfers API](/platform/other/file-transfers)         | Property mappings, same as above                    |
| A [Flow](/flow-engine/index) with Hibob's *Upload document* step | The step's own fields — see [In a Flow](#in-a-flow) |

The first two share a configuration; a Flow does not. They are described separately below because
the fields, and the rules for which value wins, are genuinely different.

### Upload settings, for domain mappings and the API

How the document is filed is not a setting on the domain mapping. It comes from
[property mappings](/platform/property-mapping) that write to Hibob's **File transfer** side:

| Hibob field        | Required | Meaning                                                                      |
| ------------------ | -------- | ---------------------------------------------------------------------------- |
| `upload_target`    | **Yes**  | Where the document is filed: `confidential`, `shared`, or a Hibob folder id. |
| `upload_file_name` | No       | The name to store it under. Defaults to the uploaded file's own name.        |
| `upload_overwrite` | No       | Whether to replace a document that already has this name. Defaults to off.   |

`upload_target` has no default because Hibob has no neutral place to put a document — confidential
and shared are different visibility levels, and picking one for you would be a guess about who is
allowed to see the file. A mapping that does not set it fails the delivery with a message saying so,
before anything is sent.

A constant is usually the right mapping for all three. Map them from the employee only when the
answer genuinely differs per person — for example filing contracts as confidential but certificates
as shared.

#### Letting the caller choose

To decide per upload rather than per integration, send the choice in the
[File Transfers API](/platform/other/file-transfers)'s `metadata` and read it back out:

```json theme={null}
POST /v1/org/file-transfers
{ "data": { "file_name": "first-aid-2026.pdf", "content_base64": "…",
            "metadata": { "target": "shared" } } }
```

Map `upload_target` from **File transfer → File transfer** with an Attribute node on `metadata`
followed by an Extract node on `target`. Anything the caller sends in `metadata` is readable this
way, so the same pattern works for `upload_file_name` and `upload_overwrite`.

<Note>
  Give the mapping a fallback if you use this — an upload that omits the key produces no value, and
  a missing `upload_target` fails the delivery.
</Note>

### Two mappings can set them, and one wins

These fields can be written from either of two property mappings, because both have Hibob's
**File transfer** as their target side:

* **File transfer → File transfer**, evaluated against the uploaded file, so it can read the file's
  own name, size and content type.
* **Employee → File transfer**, evaluated against the employee the document belongs to.

<Warning>
  Where both map the same field, **Employee → File transfer wins** — including when it produces no
  value. A mapping on the employee side that evaluates to empty for a particular employee overrides
  the file-side value rather than falling back to it, so that employee's upload fails on a missing
  `upload_target` while everyone else's succeeds.

  If you map a field on the employee side, make sure it produces a value for every employee.
</Warning>

Use one side or the other for a given field unless you specifically want that override behaviour.

### In a Flow

Hibob's *Upload document* step is configured on the step itself and ignores the property mappings
above entirely. It takes the file from an assign and offers two ways to say where it goes:

* **Target** — a dropdown listing the Shared and Confidential folders plus any custom folders in
  your Hibob account.
* **Target assign** — an assign holding a folder's id or its exact name. Reserved words `shared` and
  `confidential` work here too.

Where both are set, **Target assign wins**; the dropdown is ignored. Setting neither fails the step
with a message saying so, as does a Target assign naming a folder Hibob does not have.

Overwrite behaviour is not offered here: the step always uploads. Filing a document under a name
that already exists leaves you with two of them.

### Documents that already exist

This applies to the domain mapping and API routes only; a Flow step always uploads.

If a document with the same name is already filed against the employee and `upload_overwrite` is
off, Twine does not upload it a second time. Over the API the transfer still reports `delivered` —
the document you wanted in Hibob is in Hibob — but with no `remote_id`, because no upload happened
and Hibob was never asked for an identifier. `remote_metadata.note` says as much. Turn overwrite on
if you need the identifier back every time.

<Card title="Hibob changelog" icon="list" href="/changelog/hibob">
  Notable changes to the Hibob integration over time.
</Card>
