SafetyDataSheetAPI Bulk API
Asynchronous multi-document extraction jobs with optional signed webhooks.
Asynchronous multi-document extraction jobs with optional signed webhooks.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
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.curl "https://apis.io/api/v1/apis/safetydatasheetapi:safetydatasheetapi-bulk-api"
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.
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: 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/