SafetyDataSheetAPI Bulk API

Asynchronous multi-document extraction jobs with optional signed webhooks.

GitHub OpenAPI

Operations 2

POST /extract-sds/bulk Submit an asynchronous bulk extraction job #
GET /extract-sds/bulk/{jobId} Get bulk job status and per-item results #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/safetydatasheetapi:safetydatasheetapi-bulk-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

safetydatasheetapi-bulk-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: SafetyDataSheet Bulk API
  version: 1.0.0
  summary: Extract Safety Data Sheet (SDS/MSDS) PDFs into structured JSON, XML, and CSV.
  description: 'SafetyDataSheetAPI converts Safety Data Sheet PDFs into normalized, 16-section compliance records

    in JSON, XML, and CSV.


    **Authentication.** Production extraction endpoints require an API key, sent as

    `Authorization: Bearer ` or as an `X-API-Key` header. Request a key through the

    self-serve signup on https://safetydatasheetapi.com/#self-serve-signup.


    **Rate limits.** Extraction responses include `X-RateLimit-Remaining` and, when a limit applies,

    `X-RateLimit-Reset` (Unix epoch seconds). When the limit is exceeded the API returns `429` with

    error code `rate_limited`, a standard `Retry-After` header (seconds to wait), and

    `retry_after_epoch_seconds` in the body.


    **Errors.** Every error uses one envelope: `{"ok": false, "error": {"code": "...", "message": "..."}}`,

    optionally with extra detail fields. Branch on `error.code`, which is stable; `message` is for humans.


    **Async bulk jobs.** `POST /extract-sds/bulk` returns `202` with a `job_id`. Poll

    `GET /extract-sds/bulk/{jobId}` or pass `callback_url` to receive a signed webhook

    (`X-Webhook-Signature`). Send an `Idempotency-Key` header to make job submission safe to retry.'
  termsOfService: https://safetydatasheetapi.com/terms/
  license:
    name: Proprietary (see Terms of Service)
    url: https://safetydatasheetapi.com/terms/
  contact:
    name: SafetyDataSheetAPI Support
    url: https://safetydatasheetapi.com/contact/
    email: sales@safetydatasheetapi.com
servers:
- url: https://safetydatasheetapi.com
  description: Hosted API (free sample upload, and self-serve API keys). Enterprise customers use the dedicated base URL provided at onboarding.
security:
- bearerAuth: []
- apiKeyAuth: []
tags:
- name: Bulk
  description: Asynchronous multi-document extraction jobs with optional signed webhooks.
paths:
  /extract-sds/bulk:
    post:
      tags:
      - Bulk
      summary: Submit an asynchronous bulk extraction job
      description: 'Submit up to `BULK_MAX_FILES_PER_JOB` PDFs as repeated `file` fields. The job is processed in the

        background; poll `status_url` or supply `callback_url` for a signed webhook on completion.

        Per-file and total size limits apply (`413`). Repeating a request with the same `Idempotency-Key`

        returns the existing job with `idempotent_replay: true` instead of creating a duplicate.'
      operationId: extractSdsBulk
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        description: Caller-chosen unique key. A repeat with the same key (per API key) replays the existing job instead of creating a duplicate.
        schema:
          type: string
          maxLength: 255
        example: 7d3a1b9e-bulk-2026-10-23-001
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
              - file
              properties:
                file:
                  type: array
                  items:
                    type: string
                    format: binary
                  minItems: 1
                  description: One or more PDF files (repeated `file` fields).
                callback_url:
                  type: string
                  format: uri
                  description: Optional `https://` URL to receive a signed webhook when the job finishes. Rejected with `400` if not https or if it resolves to a local or private host.
                  examples:
                  - https://example.com/hooks/sds
                language_hint:
                  type: string
                  description: Optional language hint, for example `en`.
                schema_version:
                  type: string
                  description: Optional. Pins the output schema version. Only `1.0` exists; any other value returns 400 `unsupported_schema_version`.
      responses:
        '202':
          description: Bulk job accepted and queued for background processing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkJobAccepted'
              examples:
                accepted:
                  summary: Job queued
                  value:
                    ok: true
                    job_id: 9b1e3c0a-5d4f-4a7e-8f21-0c6d2e7a4b88
                    status: queued
                    total_items: 3
                    status_url: https://safetydatasheetapi.com/extract-sds/bulk/9b1e3c0a-5d4f-4a7e-8f21-0c6d2e7a4b88
        '200':
          description: An existing job was replayed because the `Idempotency-Key` was already used.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkJobStatus'
              examples:
                replay:
                  summary: Replay of an existing job
                  value:
                    ok: true
                    idempotent_replay: true
                    job_id: 9b1e3c0a-5d4f-4a7e-8f21-0c6d2e7a4b88
                    status: processing
                    total_items: 3
                    completed_items: 1
                    failed_items: 0
                    webhook_status: null
                    items: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /extract-sds/bulk/{jobId}:
    get:
      tags:
      - Bulk
      summary: Get bulk job status and per-item results
      description: Returns the job status, counters, webhook delivery status, and per-file results. Only jobs created by the calling API key are visible.
      operationId: getBulkJobStatus
      parameters:
      - name: jobId
        in: path
        required: true
        description: The `job_id` returned when the job was submitted.
        schema:
          type: string
        example: 9b1e3c0a-5d4f-4a7e-8f21-0c6d2e7a4b88
      responses:
        '200':
          description: Job status with per-item results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkJobStatus'
              examples:
                completed:
                  summary: Completed job
                  value:
                    ok: true
                    job_id: 9b1e3c0a-5d4f-4a7e-8f21-0c6d2e7a4b88
                    status: completed
                    total_items: 1
                    completed_items: 1
                    failed_items: 0
                    webhook_status: delivered
                    items:
                    - file_name: acetone-sds.pdf
                      status: success
                      confidence_score: 0.94
                      warnings: []
                      data:
                        product_identification:
                          product_name: Acetone
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
webhooks:
  bulkExtractionCompleted:
    post:
      tags:
      - Bulk
      summary: Bulk extraction job finished
      description: 'Sent to the `callback_url` supplied when the job was submitted, once every file has been processed.

        The body is signed: `X-Webhook-Signature` is `sha256=` followed by the hex HMAC-SHA256 of the raw JSON

        body, using the signing secret of your account (shown in the dashboard under API keys). Verify it before trusting the payload.

        `X-Webhook-Signature-V2` signs `{X-Webhook-Id}.{X-Webhook-Timestamp}.{raw body}` the same way; prefer it, reject old

        timestamps and repeated ids to prevent replays. Respond with any 2xx status to acknowledge; a few bounded

        retries are attempted on failure, and `getBulkJobStatus` remains the source of truth.'
      operationId: bulkExtractionCompletedWebhook
      security: []
      parameters:
      - name: X-Webhook-Signature
        in: header
        required: true
        description: '`sha256=` plus the hex HMAC-SHA256 of the raw request body.'
        schema:
          type: string
        example: sha256=9f2c0b1d5e7a4c3b8a6d1f0e2b4c6d8a0f1e3d5c7b9a2c4e6f8a0b1d3c5e7f90
      - name: X-Webhook-Signature-V2
        in: header
        required: true
        description: '`sha256=` plus the hex HMAC-SHA256 of `{X-Webhook-Id}.{X-Webhook-Timestamp}.{raw request body}`.'
        schema:
          type: string
      - name: X-Webhook-Id
        in: header
        required: true
        description: Unique id of this delivery. Ignore an id you have already processed.
        schema:
          type: string
      - name: X-Webhook-Timestamp
        in: header
        required: true
        description: Unix time in seconds when the delivery was signed. Reject deliveries older than a few minutes.
        schema:
          type: string
      - name: X-Webhook-Event
        in: header
        required: true
        description: The event type.
        schema:
          type: string
          const: bulk_extraction.completed
        example: bulk_extraction.completed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkJobWebhookPayload'
            example:
              job_id: 9b1e3c0a-5d4f-4a7e-8f21-0c6d2e7a4b88
              status: completed
              total_items: 1
              completed_items: 1
              failed_items: 0
              webhook_status: null
              webhook_attempts: 0
              items:
              - file_name: acetone-sds.pdf
                status: success
                confidence_score: 0.94
                warnings: []
                data:
                  product_identification:
                    product_name: Acetone
      responses:
        '200':
          description: Acknowledged. Any 2xx status is treated as delivered.
components:
  schemas:
    Error:
      type: object
      description: Standard error envelope returned by every endpoint. Additional detail fields may be present alongside `error`.
      required:
      - ok
      - error
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
              description: Stable machine-readable error code. Branch on this value.
              examples:
              - rate_limited
            message:
              type: string
              description: Human-readable explanation. May change; do not parse.
        request_id:
          type: string
          description: Present on server-side failures (5xx). Quote it when contacting support; internal detail is logged against this id and is never returned in the response.
      additionalProperties: true
    BulkJobWebhookPayload:
      type: object
      description: The webhook body. Same job fields as the status endpoint, without the `ok` envelope field.
      required:
      - job_id
      - status
      - total_items
      - completed_items
      - failed_items
      - items
      properties:
        job_id:
          type: string
        status:
          type: string
          enum:
          - completed
          - failed
        total_items:
          type: integer
        completed_items:
          type: integer
        failed_items:
          type: integer
        webhook_status:
          type:
          - string
          - 'null'
          description: Delivery state recorded when the payload was built; usually null at this point.
        webhook_attempts:
          type: integer
        items:
          type: array
          items:
            $ref: '#/components/schemas/BulkJobItem'
    BulkJobItem:
      type: object
      properties:
        file_name:
          type: string
        status:
          type: string
          enum:
          - queued
          - success
          - error
        confidence_score:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1
        warnings:
          type: array
          items:
            type: string
        data:
          type:
          - object
          - 'null'
          additionalProperties: true
          description: Normalized SDS record when `status` is `success`.
        error_code:
          type: string
          enum:
          - provider_error
          - provider_not_configured
          description: Present when `status` is `error`. Stable machine-readable code.
        error_message:
          type: string
          description: Present when `status` is `error`. Generic human-readable text; provider detail is never exposed.
    BulkJobAccepted:
      type: object
      required:
      - ok
      - job_id
      - status
      - total_items
      - status_url
      properties:
        ok:
          type: boolean
          const: true
        job_id:
          type: string
        status:
          type: string
          enum:
          - queued
        total_items:
          type: integer
        status_url:
          type: string
          format: uri
          description: URL to poll for job status.
    BulkJobStatus:
      type: object
      required:
      - ok
      - job_id
      - status
      - total_items
      - completed_items
      - failed_items
      - items
      properties:
        ok:
          type: boolean
          const: true
        idempotent_replay:
          type: boolean
          description: Present and true when this response replays an existing job for a repeated `Idempotency-Key`.
        job_id:
          type: string
        status:
          type: string
          enum:
          - queued
          - processing
          - completed
          - failed
        total_items:
          type: integer
        completed_items:
          type: integer
        failed_items:
          type: integer
        webhook_status:
          type:
          - string
          - 'null'
          enum:
          - null
          - delivered
          - failed
          description: Webhook delivery outcome, or null if no `callback_url` was supplied or delivery has not finished.
        webhook_attempts:
          type: integer
          description: Number of webhook delivery attempts made so far.
        items:
          type: array
          items:
            $ref: '#/components/schemas/BulkJobItem'
  responses:
    RateLimited:
      description: Rate limit reached for the current window. Wait `Retry-After` seconds (or until `retry_after_epoch_seconds`), then retry.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            allOf:
            - $ref: '#/components/schemas/Error'
            - type: object
              properties:
                retry_after_epoch_seconds:
                  type: integer
                  description: Unix epoch seconds after which the request may be retried.
          example:
            ok: false
            error:
              code: rate_limited
              message: Upload limit reached for this time window.
            retry_after_epoch_seconds: 1792800000
    BadRequest:
      description: Invalid input, such as a missing file, wrong content type, unsupported file type, invalid email, too many files, or an unsafe callback URL.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            ok: false
            error:
              code: missing_file
              message: File is required in the 'file' field.
    Unauthorized:
      description: The API key is missing or invalid (`unauthorized`), or has been revoked (`api_key_revoked`). A suspended account returns 403 `account_suspended`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            ok: false
            error:
              code: unauthorized
              message: Invalid or missing API key.
    NotFound:
      description: No resource found for the supplied id.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            ok: false
            error:
              code: job_not_found
              message: No bulk job found for this id.
    Forbidden:
      description: Request blocked, either by the IP allow/deny policy (`ip_restricted`) or by failed human verification (`verification_failed`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            ok: false
            error:
              code: ip_restricted
              message: Your IP is not allowed for this endpoint.
            reason: ip_denied
    MethodNotAllowed:
      description: The HTTP method is not supported on this path.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            ok: false
            error:
              code: method_not_allowed
              message: Use POST for this endpoint.
    PayloadTooLarge:
      description: A file exceeds the per-file limit, or a bulk job exceeds the combined size limit (`file_too_large`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            ok: false
            error:
              code: file_too_large
              message: File size exceeds 10 MB limit.
    ServiceUnavailable:
      description: The endpoint or its extraction provider is not configured (`extract_api_not_configured`, `provider_not_configured`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            ok: false
            error:
              code: extract_api_not_configured
              message: This endpoint is not available. Please contact support.
    PaymentRequired:
      description: The account's prepaid credit does not cover this request (`insufficient_credit`). Buy credit in the dashboard (Billing). Nothing was processed or billed. `balance_usd`, `required_usd`, `pages` and `documents_billed` are returned beside `error`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            ok: false
            error:
              code: insufficient_credit
              message: Not enough credit for this document. Add credit to continue.
            balance_usd: 0.1
            required_usd: 0.25
            pages: 12
            documents_billed: 1
  headers:
    RateLimitRemaining:
      description: Requests remaining in the current rate-limit window. Present when a limit is enforced.
      schema:
        type: integer
        minimum: 0
    RateLimitReset:
      description: Unix epoch seconds when the current rate-limit window resets.
      schema:
        type: integer
    RetryAfter:
      description: Seconds to wait before retrying. Sent on `429` responses.
      schema:
        type: integer
        minimum: 1
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: 'API key sent as `Authorization: Bearer <api_key>`. Never send keys in the query string.'
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Alternative to the Bearer header. Send the same API key as `X-API-Key`.
externalDocs:
  description: API documentation and quickstart
  url: https://safetydatasheetapi.com/api-docs/
x-agentic-access:
  version: '1.0'
  summary: How autonomous agents and AI assistants can use this API safely.
  authentication:
    type: api_key
    schemes:
    - bearerAuth
    - apiKeyAuth
    interactive_login_required: false
    obtain_key: https://safetydatasheetapi.com/#self-serve-signup
    note: Agents must be given a key by a human; they cannot self-register. Self-serve accounts receive their own key; enterprise customers receive a dedicated deployment and base URL.
  interfaces:
    rest: https://safetydatasheetapi.com/openapi.yaml
    mcp:
      url: https://safetydatasheetapi.com/mcp
      transport: streamable-http
      authentication: bearer
      manifest: https://safetydatasheetapi.com/mcp-manifest.yml
    skill: https://safetydatasheetapi.com/.well-known/agent-skills/safetydatasheetapi/SKILL.md
    clients: https://safetydatasheetapi.com/sdks/
  side_effects:
    extractSds: Consumes API quota. Creates no durable customer-visible resource.
    extractSdsBulk: Consumes API quota. Creates a job record retrievable through getBulkJobStatus.
    getBulkJobStatus: None. Read-only and safe to repeat.
  idempotency:
    operations:
    - extractSdsBulk
    header: Idempotency-Key
    behavior: A repeat with the same key returns the existing job with idempotent_replay true.
  retry_guidance:
    retry_after_header: Retry-After
    retry_on_status:
    - 429
    - 500
    - 502
    do_not_retry_on_status:
    - 400
    - 401
    - 403
    - 404
    - 413
    stable_error_field: error.code
  human_review:
    recommended_when: The response has non-empty warnings or a low confidence_score.
    note: Extracted data is machine-generated. Do not treat it as verified compliance data without review against the source document.
  rate_limits: https://safetydatasheetapi.com/api-docs/#rate-limits
  data_handling: https://safetydatasheetapi.com/data-residency/