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

# Create certificate

> Create a new certificate. Defaults to ACME issuance; set
`source=uploaded` to store customer-supplied PEM material
directly.

For ACME source: returns 202 with status pending and an empty
`challenges` list — the challenges are planned by the issuance
workflow just after the row is created, not by this call. Poll
GET /v1/certificates/{certificate_id} to pick them up: the row moves
to pending_dns and grows one `challenges` entry per domain. The CNAME
is created automatically when our_dns=true, otherwise the customer
must add `_acme-challenge.<domain> CNAME <expected_cname>` at their
registrar. The cert flips to active once every challenge verifies and
issuance completes.

For uploaded source: returns 201 with status active.




## OpenAPI

````yaml /api-reference/specs/certificate.yaml post /v1/certificates
openapi: 3.0.3
info:
  title: Basaltic Certificate API
  version: 1.0.0
  description: |
    TLS certificates for load balancer listeners and platform endpoints —
    issue, inspect, retrieve material and revoke.
  contact:
    name: Basaltic Support
    email: ping@basaltic.sh
  license:
    name: Proprietary
    url: https://basaltic.sh/terms
servers:
  - url: https://certificate.{region}.basaltic.sh
    description: Regional API endpoint
    variables:
      region:
        default: sa-saopaulo-1
        description: Region code
security:
  - SignatureAuth: []
paths:
  /v1/certificates:
    post:
      tags:
        - Certificates
      summary: Create certificate
      description: |
        Create a new certificate. Defaults to ACME issuance; set
        `source=uploaded` to store customer-supplied PEM material
        directly.

        For ACME source: returns 202 with status pending and an empty
        `challenges` list — the challenges are planned by the issuance
        workflow just after the row is created, not by this call. Poll
        GET /v1/certificates/{certificate_id} to pick them up: the row moves
        to pending_dns and grows one `challenges` entry per domain. The CNAME
        is created automatically when our_dns=true, otherwise the customer
        must add `_acme-challenge.<domain> CNAME <expected_cname>` at their
        registrar. The cert flips to active once every challenge verifies and
        issuance completes.

        For uploaded source: returns 201 with status active.
      operationId: createCertificate
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CertificateIssueRequest'
      responses:
        '201':
          description: Uploaded certificate stored (source=uploaded)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CertificateResponse'
        '202':
          description: >-
            Issuance started (source=acme). status is pending and challenges is
            empty; poll GET for the CNAME targets.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CertificateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Certificate name already in use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - SignatureAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: >-
        Optional client-generated key that makes a create replay-safe. Retrying
        a request with the same key returns the original outcome verbatim
        instead of creating a duplicate resource. Reusing a key with a different
        request body is rejected (422); a request whose key is still being
        processed returns 409. Records are honored for 24 hours. Use a UUID or
        similarly unique token.
      required: false
      schema:
        type: string
        maxLength: 255
      example: 550e8400-e29b-41d4-a716-446655440000
  schemas:
    CertificateIssueRequest:
      type: object
      required:
        - name
        - domains
      properties:
        name:
          type: string
          pattern: ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,253}$
          description: |
            Unique per account. Surfaces in the CRN
            (`crn:certificate::<account>:certificate/<name>`), so it must be
            URL-safe — letters, digits, dot, dash, underscore.
          example: prod-frontend
        domains:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 100
          description: |
            Capped at 100 to stay inside the certificate authority's
            per-order limits.
          example:
            - example.com
            - www.example.com
            - api.example.com
        key_algorithm:
          $ref: '#/components/schemas/CertificateKeyAlgorithm'
        source:
          $ref: '#/components/schemas/CertificateSource'
          description: |
            Defaults to "acme" — issued by the platform CA. Set to
            "uploaded" to store customer-supplied PEM material instead;
            certificate_pem + private_key_pem must then be provided.
        certificate_pem:
          type: string
          description: PEM-encoded leaf certificate. Required when source=uploaded.
          example: |
            -----BEGIN CERTIFICATE-----
            MIIDdzCCAl+gAwIBAgIEAaH...
            -----END CERTIFICATE-----
        chain_pem:
          type: string
          description: PEM-encoded intermediate chain (optional when source=uploaded).
          example: |
            -----BEGIN CERTIFICATE-----
            MIIEFzCCAv+gAwIBAgIEBb2...
            -----END CERTIFICATE-----
        private_key_pem:
          type: string
          description: PEM-encoded private key. Required when source=uploaded.
          example: |
            -----BEGIN PRIVATE KEY-----
            MIGHAgEAMBMGByqGSM49AgEG...
            -----END PRIVATE KEY-----
        tags:
          $ref: '#/components/schemas/Tags'
    CertificateResponse:
      type: object
      properties:
        certificate:
          $ref: '#/components/schemas/Certificate'
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              description: Error code identifying the type of error
              example: INVALID_INPUT
            message:
              type: string
              description: Human-readable error message
              example: Invalid request parameters
            request_id:
              type: string
              format: uuid
              description: Request ID for debugging
              example: 550e8400-e29b-41d4-a716-446655440000
    CertificateKeyAlgorithm:
      type: string
      enum:
        - ecdsa-p256
        - ecdsa-p384
        - rsa-2048
        - rsa-4096
      default: ecdsa-p256
    CertificateSource:
      type: string
      enum:
        - acme
        - uploaded
      example: acme
    Tags:
      type: object
      additionalProperties:
        type: string
      example:
        environment: production
        team: backend
    Certificate:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
          example: 7c9e6679-7425-40de-944b-e07fc1f90ae7
        crn:
          type: string
          readOnly: true
          description: >-
            Name-based, so an IAM policy can wildcard a naming convention
            (`crn:certificate::my-account:certificate/prod-*`). The region slot
            is empty — certs are not region-bound.
          example: crn:certificate::my-account:certificate/prod-frontend
        name:
          type: string
          example: prod-frontend
        domains:
          type: array
          items:
            type: string
          example:
            - example.com
            - www.example.com
            - api.example.com
        status:
          $ref: '#/components/schemas/CertificateStatus'
        source:
          $ref: '#/components/schemas/CertificateSource'
        key_algorithm:
          $ref: '#/components/schemas/CertificateKeyAlgorithm'
        challenges:
          type: array
          items:
            $ref: '#/components/schemas/CertificateChallenge'
          description: |
            Per-domain CNAME delegation state — one entry per domain on
            the cert. While status=pending_dns issuance waits for every
            challenge's `verified` to flip true; use `expected_cname` and
            `our_dns` to tell which records need to be added at the
            registrar.
        certificate_pem:
          type: string
          description: PEM-encoded leaf certificate. Empty until active.
          example: |
            -----BEGIN CERTIFICATE-----
            MIIDdzCCAl+gAwIBAgIEAaH...
            -----END CERTIFICATE-----
        chain_pem:
          type: string
          description: PEM-encoded intermediate chain.
          example: |
            -----BEGIN CERTIFICATE-----
            MIIEFzCCAv+gAwIBAgIEBb2...
            -----END CERTIFICATE-----
        fingerprint:
          type: string
          readOnly: true
          description: >
            Hex SHA-256 of the leaf's DER — the certificate's material version.
            Changes on every rotation; consumers use it to know when to re-fetch
            material and to verify they fetched the intended generation. Empty
            until the cert has a leaf.
          example: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
        issued_at:
          type: string
          format: date-time
          readOnly: true
          example: '2026-01-15T09:30:00Z'
        expires_at:
          type: string
          format: date-time
          readOnly: true
          example: '2026-04-15T09:30:00Z'
        error_message:
          type: string
          description: Last failure reason (set when status=error).
          example: DNS challenge did not propagate before the deadline
        tags:
          $ref: '#/components/schemas/Tags'
    CertificateStatus:
      type: string
      enum:
        - pending_dns
        - pending
        - active
        - error
        - expired
        - revoked
      example: active
    CertificateChallenge:
      type: object
      properties:
        domain:
          type: string
          description: The cert SAN this challenge belongs to (as the customer wrote it).
          example: '*.example.com'
        cname_record_name:
          type: string
          description: |
            Full LHS of the CNAME record the customer needs to add. For
            wildcard SANs this is the parent name
            (`_acme-challenge.example.com.`), not the literal SAN —
            wildcards validate at their parent under RFC 8555 §8.4.
          example: _acme-challenge.example.com.
        expected_cname:
          type: string
          description: |
            Target FQDN (RHS of the CNAME). Hosted in the platform's
            validation zone, where the per-order TXT is published during
            issuance.
          example: a1b2c3d4e5f6g7h8.acme.cloud.bycoded.dev.
        our_dns:
          type: boolean
          description: |
            True when the domain is hosted on the platform DNS service and
            the CNAME was created automatically. False means the customer
            owns the zone and must add the CNAME at their registrar.
          example: true
        verified:
          type: boolean
          description: |
            True once the CNAME has resolved to expected_cname; cert
            issuance only proceeds when every challenge is verified.
          example: true
        verified_at:
          type: string
          format: date-time
          readOnly: true
          example: '2026-01-15T09:28:00Z'
        error_message:
          type: string
          description: |
            Last verification failure (e.g. "CNAME = foo, want bar").
            Cleared when the challenge eventually verifies.
  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INVALID_INPUT
              message: Invalid request parameters
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Unauthorized:
      description: Authentication required or token invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHORIZED
              message: Authentication required
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: ACCESS_DENIED
              message: You don't have permission to perform this action
              request_id: 550e8400-e29b-41d4-a716-446655440000
    UnprocessableEntity:
      description: |
        The request is well-formed but cannot be processed as sent. On the
        operations that accept `Idempotency-Key` this is the key-reuse case: the
        key was first seen with a different request payload, so replaying the
        stored outcome would answer a question the caller did not ask.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: IDEMPOTENCY_KEY_REUSED
              message: >-
                This Idempotency-Key was already used with a different request
                payload
              request_id: 550e8400-e29b-41d4-a716-446655440000
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INTERNAL_ERROR
              message: An internal error occurred
              request_id: 550e8400-e29b-41d4-a716-446655440000
  securitySchemes:
    SignatureAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >
        Request signing with an access key issued to a service account. An

        HMAC-SHA256 over a canonical form of the request, close to AWS SigV4.

        The `basaltic` CLI signs for you.


        Send `Authorization`, `X-Date` (UTC, `YYYYMMDDTHHMMSSZ`) and `X-Nonce`

        (random per request); add `X-Content-Sha256` to bind a body, and

        `X-Amz-Security-Token` when using temporary credentials.


        ```

        Authorization: BASALTIC-HMAC-SHA256
        Credential=<access_key_id>/<date>/<region>/basaltic/basaltic_request,
        SignedHeaders=host;x-date;x-nonce, Signature=<hex>

        ```


        `<region>` is the region code you are calling, or `global` for the
        global

        services. A signature is valid for 5 minutes from `X-Date`, and mutating

        requests are replay-guarded on the nonce.


        **Full signing procedure, including a working implementation:**

        https://docs.basaltic.sh/authentication


        ## Rate limits

        There is no global request budget. A limit applies only where an

        operation documents a `429`, and that operation says what it counts.

        Those responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`,

        `X-RateLimit-Reset` and, on a `429`, `Retry-After` — read them rather

        than hard-coding a number. Retrying before `Retry-After` is refused and

        extends the window. Everything else is bounded by quota, not by request

        rate.

````