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

# Submit Certificate

> Submit a certificate document for asynchronous processing

Submit an exemption certificate document for asynchronous processing. You
provide an `https` URL to the document (typically a presigned URL to your own
storage); Numeral downloads it, stores it, and runs the classification
pipeline. The certificate type and jurisdiction are determined from the
document itself — there is no `certificate_type_id` input.

The response is a `tax.certificate_upload`, a processing job. Poll
[`GET /tax/certificate-uploads/{upload_id}`](/api-reference/v2026-03-01/endpoint/get_certificate_upload)
until its `status` leaves `processing`, then read `certificate_ids` to find
the resulting certificates.

<Note>
  Remember to include the `X-API-Version: 2026-03-01` header — older versions
  return `INVALID_REQUEST` (400). Certificate endpoints are live-only — using a
  `sk_test_*` key returns `TESTMODE_NOT_SUPPORTED` (400).
</Note>

## The `document_url` field

`document_url` is fetched server-side, so it must be reachable by Numeral's
servers without any session or cookie:

* **`https` only.** Redirects are followed up to 3 hops, and every hop must
  also be `https`.
* **PDF, PNG, or JPEG.** The type is detected from the file content, not the
  extension or `Content-Type` header.
* **10 MB max**, checked against both `Content-Length` and the downloaded
  bytes.

Share or viewer links (Google Drive, Dropbox, and similar) return an HTML
page rather than the file and fail with `UNSUPPORTED_FILE_TYPE`. Use a
direct download link or a presigned URL instead.

## Pre-mapping to a customer

Pass `customer` to attribute the certificate to a buyer up front. Either
identifier alone is sufficient:

* `customer.id` — an existing Numeral customer id. An unknown id returns
  `CUSTOMER_NOT_FOUND` (404).
* `customer.reference_customer_id` — your own customer identifier. An
  unknown value creates the customer, so you can pre-map a certificate
  before the customer exists in Numeral.

If both are present and resolve to different customers, the request fails
with `BUYER_IDENTITY_CONFLICT` (409) and nothing is written.

<Tip>
  **Pre-mapping is the headline feature.** Submit with a
  `reference_customer_id`, and once the resulting certificate is `active`, any
  `POST /tax/calculations` carrying the same `reference_customer_id` applies
  the exemption automatically. No further wiring is needed.
</Tip>

Omitting `customer` is valid. The upload processes unattributed, Numeral
matches it to a buyer during processing, and the response `customer` is
`null`.

## Duplicate submissions

Resubmitting a byte-identical document with the same `file_name` returns
`201` with `status: "duplicate"` rather than reprocessing the document. The
duplicate upload's `certificate_ids` stays empty — use your original upload
id to retrieve its certificates. When the original upload had failed,
`error_message` on the duplicate carries the same reason so you can see why
resubmitting the same bytes will not help.

## Response

`201 Created` with the `tax.certificate_upload` object. `status` is
`processing` or `duplicate` on create, and `certificate_ids` is always empty
at this point.

## Errors

| HTTP | Type                        | When                                                                                                         |
| ---- | --------------------------- | ------------------------------------------------------------------------------------------------------------ |
| 400  | `INVALID_DOCUMENT_URL`      | Not a valid URL, not `https`, blocked or unresolvable host, or private address.                              |
| 400  | `DOCUMENT_FETCH_FAILED`     | Download failed — network error, too many redirects, HTTP error from the document host, or empty file.       |
| 422  | `DOCUMENT_URL_UNAUTHORIZED` | The document host returned 401 or 403, most commonly an expired presigned URL. Mint a fresh one and retry.   |
| 400  | `UNSUPPORTED_FILE_TYPE`     | Content is not PDF, PNG, or JPEG. HTML gets a targeted message about share/viewer links.                     |
| 400  | `FILE_TOO_LARGE`            | Over 10 MB.                                                                                                  |
| 404  | `CUSTOMER_NOT_FOUND`        | `customer.id` does not exist for this account.                                                               |
| 409  | `BUYER_IDENTITY_CONFLICT`   | `customer.id` and `customer.reference_customer_id` resolve to different customers. Nothing was written.      |
| 400  | `TESTMODE_NOT_SUPPORTED`    | `sk_test_*` key.                                                                                             |
| 400  | `INVALID_REQUEST`           | API version below 2026-03-01, or a schema violation (missing `document_url`, empty `customer` object, etc.). |
| 500  | `NUMERAL_ERROR`             | The upload was stored but processing could not start. Retry the submission.                                  |


## OpenAPI

````yaml POST /tax/certificates
openapi: 3.0.1
info:
  title: Numeral API
  description: >-
    API for sales tax calculations - Version 2026-03-01. This version adds
    IP-based tax resolution for calculations and platform calculations, in
    addition to Merchant management and Platform Calculations.
  license:
    name: MIT
  version: '2026-03-01'
servers:
  - url: https://api.numeralhq.com/
security:
  - bearerAuth: []
paths:
  /tax/certificates:
    post:
      summary: Submit Certificate
      description: >-
        Submit a certificate document for asynchronous processing. Numeral
        downloads the document from the `https` URL you provide, stores it, and
        classifies the certificate type and jurisdiction from the document
        itself. Returns a `tax.certificate_upload` you can poll. Live-only —
        `sk_test_*` keys are rejected with `TESTMODE_NOT_SUPPORTED`.
      operationId: submitCertificate_v20260301
      parameters:
        - name: X-API-Version
          in: header
          required: true
          schema:
            type: string
            enum:
              - '2026-03-01'
          description: >-
            Must be `2026-03-01` or later. Older versions return
            `INVALID_REQUEST` (400).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CertificateUploadRequest'
            example:
              document_url: >-
                https://your-bucket.s3.amazonaws.com/certs/resale.pdf?X-Amz-Signature=...
              file_name: resale.pdf
              customer:
                reference_customer_id: '20506'
      responses:
        '201':
          description: >-
            Upload accepted. `status` is `processing` or `duplicate`;
            `certificate_ids` is always empty at this point — poll `GET
            /tax/certificate-uploads/{upload_id}` for results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CertificateUploadResponse'
              example:
                id: upl_4471
                object: tax.certificate_upload
                status: processing
                customer:
                  id: cust_6126acaf-7379-411a-8ada-00005bac0715
                  reference_customer_id: '20506'
                certificate_ids: []
                error_message: null
                created_at: '2026-09-15T17:04:11.000Z'
                livemode: true
        '400':
          description: >-
            `INVALID_DOCUMENT_URL`, `DOCUMENT_FETCH_FAILED`,
            `UNSUPPORTED_FILE_TYPE`, `FILE_TOO_LARGE`, `TESTMODE_NOT_SUPPORTED`,
            or `INVALID_REQUEST` (schema violation or API version below
            2026-03-01)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`CUSTOMER_NOT_FOUND` — `customer.id` does not exist for this account'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerNotFoundError'
        '409':
          description: >-
            `BUYER_IDENTITY_CONFLICT` — `customer.id` and
            `customer.reference_customer_id` resolve to different customers;
            nothing was written
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: >-
            `DOCUMENT_URL_UNAUTHORIZED` — the document host returned 401/403,
            most commonly an expired presigned URL. Mint a fresh URL and retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: >-
            `NUMERAL_ERROR` — the upload was stored but processing could not
            start. Retry the submission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    CertificateUploadRequest:
      type: object
      properties:
        document_url:
          type: string
          format: uri
          description: >-
            An `https` URL that Numeral's servers can `GET` — typically a
            presigned URL to your own storage. Fetched server-side. The file
            must be a PDF, PNG, or JPEG (detected from content, not extension or
            `Content-Type`), at most 10 MB, reachable within 3 redirects (each
            hop must also be `https`).
          example: >-
            https://your-bucket.s3.amazonaws.com/certs/resale.pdf?X-Amz-Signature=...
        file_name:
          type: string
          minLength: 1
          maxLength: 255
          description: >-
            Display name for the document (1–255 characters). Defaults to the
            URL's basename when it has an extension, otherwise to a generic name
            derived from the detected file type.
          example: resale.pdf
        customer:
          $ref: '#/components/schemas/CertificateUploadCustomerInput'
      required:
        - document_url
    CertificateUploadResponse:
      type: object
      description: >-
        A certificate upload — the asynchronous processing job created by `POST
        /tax/certificates`.
      properties:
        id:
          type: string
          description: >-
            Upload id, `upl_<number>`. Endpoints that accept an upload id also
            accept the bare numeric form.
          example: upl_4471
        object:
          type: string
          description: 'The type of object: `tax.certificate_upload`'
          example: tax.certificate_upload
        status:
          $ref: '#/components/schemas/CertificateUploadStatus'
        customer:
          allOf:
            - $ref: '#/components/schemas/CertificateUploadCustomer'
          nullable: true
          description: >-
            The customer supplied at submission. `null` when the upload was
            submitted without a customer — attribution then happens during
            processing.
        certificate_ids:
          type: array
          items:
            type: string
            example: cert_a8f3d2c1-1b9a-4c5e-8d7e-6f4a3b2c1d0e
          description: >-
            Certificates produced by this upload, usable with the certificate
            endpoints. Empty while `status` is `processing`; populated at
            `completed`. One document can yield multiple certificates.
        error_message:
          type: string
          nullable: true
          description: >-
            Human-readable reason processing failed. Also set on `duplicate`
            uploads when the original upload had failed, so you can see why
            re-submitting the same bytes will not help.
        created_at:
          type: string
          format: date-time
          nullable: true
        livemode:
          type: boolean
          enum:
            - true
          description: Always `true` — these endpoints are live-only.
      required:
        - id
        - object
        - status
        - customer
        - certificate_ids
        - error_message
        - created_at
        - livemode
    Error:
      type: object
      description: Standard error response format for API version 2026-01-01
      properties:
        code:
          type: integer
          description: HTTP status code
          example: 400
        type:
          type: string
          description: Machine-readable error type
          example: MISSING_FIELD
        message:
          type: string
          description: Human-readable error message
          example: Required field 'address_country' is missing
      required:
        - code
        - type
        - message
    CustomerNotFoundError:
      type: object
      properties:
        code:
          type: integer
          example: 404
        type:
          type: string
          example: CUSTOMER_NOT_FOUND
        message:
          type: string
          example: Customer not found
    CertificateUploadCustomerInput:
      type: object
      description: >-
        Pre-maps the resulting certificate to a customer. Supply at least one of
        `id` or `reference_customer_id`. Either alone is sufficient; if both are
        present and resolve to different customers the request fails with
        `BUYER_IDENTITY_CONFLICT` (409) and nothing is written.
      properties:
        id:
          type: string
          description: >-
            Existing Numeral customer id. An unknown id returns
            `CUSTOMER_NOT_FOUND` (404).
          example: cust_6126acaf-7379-411a-8ada-00005bac0715
        reference_customer_id:
          type: string
          description: >-
            Your own identifier for the customer. An unknown value creates the
            customer, so pre-mapping works before the customer exists in
            Numeral.
          example: '20506'
      minProperties: 1
    CertificateUploadStatus:
      type: string
      enum:
        - processing
        - completed
        - failed
        - duplicate
      description: >-
        Processing state of a certificate upload. This is a separate vocabulary
        from certificate `status` — an upload is a processing job, a certificate
        is its output. `processing` is the only non-terminal state; `completed`
        (certificates produced), `failed` (see `error_message`), and `duplicate`
        (same document already submitted) are terminal.
    CertificateUploadCustomer:
      type: object
      description: The customer the upload was attributed to at submission.
      properties:
        id:
          type: string
          example: cust_6126acaf-7379-411a-8ada-00005bac0715
        reference_customer_id:
          type: string
          nullable: true
          example: '20506'
      required:
        - id
        - reference_customer_id
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer

````