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

# Get Document Download URL

> **Beta.** The Documents API is in beta and its contract may change.

Returns an expiring, signed URL from which the document file can be downloaded, along with its filename, MIME type and size. This is a separate call from Get Document By Id because the URL expires within minutes while the document metadata stays cacheable for hours, and because downloading a signed W-4 warrants its own permission and its own audit event.

export const IntegrationSupport = ({category = "HRIS", model, direction = "read"}) => {
  const SHEET_BASE = "https://docs.google.com/spreadsheets/d/e/2PACX-1vTx0G0yItXlZKO4Zep8wstZuvvO7bgOxFXBVK_1vvQnxpG8H2hP9n9M8kmZMfIoo7ZO4e7_utrz3_XB/pub";
  const SHEET_GIDS = {
    HRIS: {
      read: "0",
      write: "1626747715"
    },
    ATS: {
      read: "1962490874"
    },
    LMS: {
      read: "1106223511"
    }
  };
  const LOGO_ROW = "logo link";
  const HEADER_ROW = "model";
  const TYPE_ROW = "connection type";
  const parseCsv = text => {
    const rows = [];
    let row = [];
    let field = "";
    let quoted = false;
    for (let i = 0; i < text.length; i++) {
      const c = text[i];
      if (quoted) {
        if (c === '"') {
          if (text[i + 1] === '"') {
            field += '"';
            i++;
          } else {
            quoted = false;
          }
        } else {
          field += c;
        }
      } else if (c === '"') {
        quoted = true;
      } else if (c === ",") {
        row.push(field);
        field = "";
      } else if (c === "\n" || c === "\r") {
        if (c === "\r" && text[i + 1] === "\n") i++;
        row.push(field);
        rows.push(row);
        row = [];
        field = "";
      } else {
        field += c;
      }
    }
    if (field !== "" || row.length) {
      row.push(field);
      rows.push(row);
    }
    return rows;
  };
  const cell = (row, i) => (row && row[i] || "").trim();
  const parseSheet = (csv, wanted) => {
    const rows = parseCsv(csv);
    let header = null;
    let labelCol = -1;
    rows.slice(0, 12).forEach(r => {
      if (header) return;
      for (let i = 0; i < Math.min(r.length, 6); i++) {
        if (cell(r, i).toLowerCase() === HEADER_ROW) {
          header = r;
          labelCol = i;
          return;
        }
      }
    });
    const find = label => rows.find(r => cell(r, labelCol).toLowerCase() === label);
    const types = header ? find(TYPE_ROW) : null;
    const logos = header ? find(LOGO_ROW) : null;
    if (!header || !types) return null;
    const providers = [];
    for (let i = labelCol + 1; i < header.length; i++) {
      const name = cell(header, i);
      const t = cell(types, i).toUpperCase();
      if (!name || t !== "A" && t !== "S") continue;
      providers.push({
        name,
        type: t === "S" ? "SFTP" : "API",
        col: i,
        uid: name + "#" + i,
        logo: cell(logos, i)
      });
    }
    const target = String(wanted || "").toLowerCase();
    const row = rows.find(r => cell(r, labelCol).toLowerCase() === target);
    if (!providers.length || !row) return null;
    const pool = providers;
    const supported = [];
    pool.forEach(p => {
      const v = cell(row, p.col).toUpperCase();
      if (v === "Y" || v === "B") supported.push({
        ...p
      });
    });
    if (!supported.length) return {
      none: true
    };
    return {
      total: pool.length,
      supported
    };
  };
  const CSS = `
  .bb-is {
    --bb-border: #e5e3df;
    --bb-border-strong: #d4d1cc;
    --bb-bg: #fcfcfb;
    --bb-bg-sub: #f5f4f3;
    --bb-text: #1c1b1a;
    --bb-text-dim: #6e6c68;
    --bb-accent: #f57e21;
    --bb-chip: #eceae7;
    font-size: 14px;
    color: var(--bb-text);
    border: 1px solid var(--bb-border);
    border-radius: 6px;
    background: var(--bb-bg);
    margin: 1.5rem 0;
    overflow: hidden;
  }
  html.dark .bb-is {
    --bb-border: #242424;
    --bb-border-strong: #333333;
    --bb-bg: #090909;
    --bb-bg-sub: #141414;
    --bb-text: #ededed;
    --bb-text-dim: #a1a1a1;
    --bb-chip: #1b1b1b;
  }

  .bb-is-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    padding: 11px 14px;
    background: var(--bb-bg-sub);
    border-bottom: 1px solid var(--bb-border);
  }
  .bb-is-title {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-weight: 600;
    font-size: 16px;
  }
  /* Mintlify's <Icon> paints itself via a mask, so this makes it take the
     title's colour instead of staying its own. */
  .bb-is-title .icon { background-color: currentColor !important; }
  .bb-is-count {
    font-size: 12px;
    color: var(--bb-text-dim);
  }

  .bb-is-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    padding: 12px 14px;
  }
  /* Collapsed by default: one row of chips, the next row fading out under a
     gradient so it reads as "there is more" rather than as a hard crop. The
     wrapper does the clipping so the chips keep their own padding. */
  .bb-is-chips-wrap { position: relative; }
  .bb-is-chips-wrap[data-open="false"] .bb-is-chips {
    max-height: 47px;
    overflow: hidden;
  }
  .bb-is-fade {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 28px;
    pointer-events: none;
    background: linear-gradient(to bottom, rgba(252, 252, 251, 0), var(--bb-bg));
  }
  html.dark .bb-is-fade {
    background: linear-gradient(to bottom, rgba(9, 9, 9, 0), var(--bb-bg));
  }
  .bb-is-toggle {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 10px;
    border: 1px solid var(--bb-border-strong);
    border-radius: 999px;
    background: var(--bb-bg);
    color: var(--bb-text-dim);
    font-size: 12.5px;
    font-weight: 500;
    line-height: 1.5;
    cursor: pointer;
  }
  .bb-is-toggle:hover {
    color: var(--bb-text);
    border-color: var(--bb-text-dim);
  }
  .bb-is-headright {
    display: inline-flex;
    align-items: center;
    gap: 10px;
  }
  .bb-is-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 9px;
    border: 1px solid var(--bb-border);
    border-radius: 999px;
    background: var(--bb-bg-sub);
    font-size: 12.5px;
    line-height: 1.4;
    white-space: nowrap;
  }
  .bb-is-logo {
    width: 15px;
    height: 15px;
    object-fit: contain;
    border-radius: 3px;
    flex: none;
  }
  .bb-is-badge {
    font-size: 9.5px;
    font-weight: 600;
    letter-spacing: 0.03em;
    padding: 1px 5px;
    border-radius: 3px;
    background: var(--bb-chip);
    color: var(--bb-text-dim);
  }
  /* Muted on purpose - the provider name should catch the eye in this list,
     not its connection-type badge. --bb-chip sat too close to the chip's own
     --bb-bg-sub to read as a separate shape, so the fill is a step further
     from it in each theme - darker in light mode, and lighter (not literally
     "darker") in dark mode, since going darker there would fade it into the
     near-black page instead of separating it. */
  .bb-is-badge[data-type="SFTP"] { color: var(--bb-text-dim); background: #ddd9d3; }
  html.dark .bb-is-badge[data-type="SFTP"] { background: #292929; }

  .bb-is-foot {
    padding: 9px 14px;
    border-top: 1px solid var(--bb-border);
    font-size: 14px;
    color: var(--bb-text-dim);
  }
  .bb-is-foot a { color: var(--bb-accent); text-decoration: none; }
  .bb-is-foot a:hover { text-decoration: underline; }

  .bb-is-none {
    padding: 16px 14px;
    font-size: 14px;
    line-height: 1.5;
    color: var(--bb-text-dim);
  }
  .bb-is-none a { color: var(--bb-accent); text-decoration: none; }
  .bb-is-none a:hover { text-decoration: underline; }

  .bb-is-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 28px 14px;
    font-size: 12.5px;
    color: var(--bb-text-dim);
  }
  .bb-is-spinner {
    width: 18px;
    height: 18px;
    border: 2px solid var(--bb-border);
    border-top-color: var(--bb-accent);
    border-radius: 50%;
    animation: bb-is-spin 0.7s linear infinite;
    flex: none;
  }
  @keyframes bb-is-spin {
    to { transform: rotate(360deg); }
  }
  /* Keep a motion cue for anyone who asked for less of it, just a calmer one. */
  @media (prefers-reduced-motion: reduce) {
    .bb-is-spinner { animation-duration: 2.4s; }
  }
  `;
  const [data, setData] = useState(null);
  const [open, setOpen] = useState(false);
  useEffect(() => {
    let live = true;
    const tabs = SHEET_GIDS[category] || ({});
    const gid = tabs[direction];
    if (!gid) return;
    const url = SHEET_BASE + "?gid=" + encodeURIComponent(gid) + "&single=true&output=csv&_=" + Date.now();
    fetch(url, {
      cache: "no-store"
    }).then(res => res.ok ? res.text() : Promise.reject(res.status)).then(text => {
      if (live) setData(parseSheet(text, model) || ({
        hide: true
      }));
    }).catch(() => {
      if (live) setData({
        hide: true
      });
    });
    return () => {
      live = false;
    };
  }, [category, model, direction]);
  if (data && data.hide) return null;
  const none = !!(data && data.none);
  const verb = direction === "write" ? "writing" : "reading";
  const mailto = "mailto:support@bindbee.dev?subject=" + encodeURIComponent("Integration request: " + verb + " " + model + " (" + category + ")");
  const collapsible = !!(data && !none && data.supported.length > 4);
  return <div className="bb-is not-prose">
      <style>{CSS}</style>
      <div className="bb-is-head">
        <span className="bb-is-title">
          <Icon icon="git-fork" size={15} />
          Supported integrations
        </span>
        {data && !none ? <span className="bb-is-headright">
            <span className="bb-is-count">
              {data.supported.length} integration{data.supported.length === 1 ? "" : "s"}
            </span>
            {collapsible ? <button type="button" className="bb-is-toggle" aria-expanded={open} onClick={() => setOpen(!open)}>
                {open ? "Show less" : "Show all"}
              </button> : null}
          </span> : null}
      </div>

      {!data ? <div className="bb-is-loading" role="status">
          <span className="bb-is-spinner" aria-hidden="true" />
          <span>Loading integration support…</span>
        </div> : none ? <div className="bb-is-none">
          No integration supports {verb} {model} yet.{" "}
          <a href={mailto}>Tell us which one you need</a>
        </div> : <div className="bb-is-chips-wrap" data-open={collapsible ? open : true}>
          <div className="bb-is-chips">
            {data.supported.map(p => <span className="bb-is-chip" key={p.uid}>
                {p.logo ? <img className="bb-is-logo" src={p.logo} alt="" loading="lazy" onError={e => {
    e.currentTarget.style.display = "none";
  }} /> : null}
                <span>{p.name}</span>
                {p.type === "SFTP" ? <span className="bb-is-badge" data-type="SFTP">
                    SFTP
                  </span> : null}
              </span>)}
          </div>
          {collapsible && !open ? <span className="bb-is-fade" /> : null}
        </div>}

      <div className="bb-is-foot">
        Coverage varies by integrations {" "}
        <a href="/get-started/model-availability">See the full availability matrix</a>
      </div>
    </div>;
};

<IntegrationSupport category="HRIS" model="Document" />


## OpenAPI

````yaml get /api/hris/v1/documents/{id}/download
openapi: 3.1.0
info:
  title: Bindbee APIs
  version: 0.1.0
servers:
  - url: https://api.bindbee.dev
  - url: https://api-eu.bindbee.dev
security: []
paths:
  /api/hris/v1/documents/{id}/download:
    get:
      tags:
        - Documents
      summary: Get Document Download URL
      description: >-
        **Beta.** The Documents API is in beta and its contract may change.


        Returns an expiring, signed URL from which the document file can be
        downloaded, along with its filename, MIME type and size. This is a
        separate call from Get Document By Id because the URL expires within
        minutes while the document metadata stays cacheable for hours, and
        because downloading a signed W-4 warrants its own permission and its own
        audit event.
      operationId: get_document_download_url_api_hris_v1_documents__id__download_get
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
            title: Id
        - name: ttl_seconds
          in: query
          required: false
          schema:
            type: integer
            maximum: 3600
            minimum: 60
            description: >-
              How long the returned download URL stays valid, in seconds.
              Maximum is 3600.
            examples:
              - 300
            default: 300
            title: Ttl Seconds
          description: >-
            How long the returned download URL stays valid, in seconds. Maximum
            is 3600.
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HrisDocumentDownload'
        '401':
          description: Missing or invalid bearer authentication credentials.
          headers:
            WWW-Authenticate:
              description: Bearer authentication challenge.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: >-
            The credentials are valid but do not permit access to this resource,
            e.g. a connector token used on a different API category or a model
            whose writes are disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '429':
          description: Rate limit exceeded.
          headers:
            X-RateLimit-Limit:
              description: Maximum requests allowed in the current window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: >-
                Unix timestamp (seconds since epoch) at which the current
                rate-limit window resets.
              schema:
                type: integer
            Retry-After:
              description: Seconds to wait before retrying the request.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - HTTPBearer: []
          ConnectorToken: []
components:
  schemas:
    HrisDocumentDownload:
      properties:
        url:
          anyOf:
            - type: string
              maxLength: 2083
              minLength: 1
              format: uri
            - type: 'null'
          title: Url
          description: >-
            A short-lived, signed URL from which the file can be downloaded.
            Fetch the file before `expires_at`; do not store or share the URL.
          examples:
            - https://files.bindbee.dev/documents/018b18ef...?signature=...
        expires_at:
          anyOf:
            - type: string
            - type: 'null'
          title: Expires At
          description: >-
            The datetime at which the download URL stops working. Format:
            DateTime (ISO 8601)
          examples:
            - '2026-08-07T12:05:00Z'
        filename:
          anyOf:
            - type: string
            - type: 'null'
          title: Filename
          description: The name of the document file in the third-party system.
          examples:
            - Charlotte_Danielle_Abbott_W-4.pdf
        content_type:
          anyOf:
            - type: string
            - type: 'null'
          title: Content Type
          description: The MIME type of the file behind the URL.
          examples:
            - application/pdf
        size_bytes:
          anyOf:
            - type: integer
            - type: 'null'
          title: Size Bytes
          description: The size of the file in bytes.
          examples:
            - 161484
      type: object
      required:
        - url
        - expires_at
        - filename
        - content_type
        - size_bytes
      title: HrisDocumentDownload
      description: >-
        An expiring link to the document file, together with the metadata a
        consumer needs in order to download and store it.
    ErrorResponse:
      type: object
      required:
        - detail
      properties:
        detail:
          type: string
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
        - loc
        - msg
        - type
      title: ValidationError
  securitySchemes:
    HTTPBearer:
      type: http
      scheme: bearer
      x-default: <BINDBEE_API_KEY>
      description: >-
        Your Bindbee API key, sent as `Authorization: Bearer <BINDBEE_API_KEY>`.
        Required on every request.
    ConnectorToken:
      type: apiKey
      in: header
      name: X-Connector-Token
      x-default: <CONNECTOR_TOKEN>
      description: >-
        The connector token for one customer's connection. This is not your
        Bindbee API key. Required on requests that read or write a single
        customer's data.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.