> ## 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.

# Lease messages

> POST rather than GET because it mutates: a receive takes a lease,
increments the delivery count, and hides the messages from every
other consumer. Modelling that as a GET would invite a proxy or a
retrying client to consume the queue by replaying what looks like
a safe read.

Returns an empty list, not an error, when nothing is available.




## OpenAPI

````yaml /api-reference/specs/queue.yaml post /v1/queues/{queue_id}/messages/receive
openapi: 3.0.3
info:
  title: Basaltic Queue API
  version: 1.0.0
  description: |
    Durable message queues with at-least-once delivery.

    **A receive is a lease, not a read.** `ReceiveMessage` hides the
    messages it returns for the queue's visibility timeout and hands back
    a *receipt handle*. The message comes back — to you or to another
    consumer — unless you delete it with that handle before the lease
    lapses. Everything else follows from this: a consumer that crashes
    mid-work loses nothing, and a message no consumer can process is
    caught by `receive_count` crossing the redrive threshold.

    **Receipt handles are per-lease, not per-message.** A new one is
    minted on every receive. Presenting a handle from a lease that has
    already lapsed fails with `QUEUE_RECEIPT_HANDLE_EXPIRED` rather than
    deleting a message that has since been handed to someone else — so a
    slow consumer finds out it was slow instead of silently destroying
    another's work.

    **Two kinds of queue, fixed at creation.** A *standard* queue
    delivers at least once with best-effort ordering and races
    consumers freely. A *FIFO* queue — whose name must end in `.fifo` —
    delivers strictly in order within a `message_group_id` and refuses
    to hand out a group's next message while an earlier one is still in
    flight; sends inside a five-minute window that repeat a
    `message_deduplication_id` collapse to one message. Ordering across
    groups is undefined, which is what lets groups run concurrently. The
    kind cannot be changed: a queue holding messages written under one
    set of rules cannot start honouring the other.

    **Long polling.** `wait_time_seconds` (up to 20) blocks the receive
    until a message arrives rather than returning empty immediately. It
    costs one request instead of the many an empty polling loop would
    make, and it returns as soon as a producer sends.

    **Message bodies are encrypted at rest.** Each queue has a data key
    that seals its messages; that key is itself wrapped by a platform key
    or, if you set `kms_key_id`, by your own. The unwrapped data key is
    cached for `kms_data_key_reuse_period_seconds` — so for up to that
    long after you disable your key, sends and receives on the queue keep
    working. Lower it to tighten that window at the cost of more KMS
    round-trips.
  contact:
    name: Basaltic Support
    email: ping@basaltic.sh
  license:
    name: Proprietary
    url: https://basaltic.sh/terms
servers:
  - url: https://queue.{region}.basaltic.sh
    description: Regional API endpoint
    variables:
      region:
        default: sa-saopaulo-1
        description: Region code
security:
  - SignatureAuth: []
tags:
  - name: Queues
    description: Queue lifecycle and attributes
  - name: Messages
    description: Sending, receiving, and acknowledging messages
paths:
  /v1/queues/{queue_id}/messages/receive:
    parameters:
      - $ref: '#/components/parameters/QueueId'
    post:
      tags:
        - Messages
      summary: Lease messages
      description: |
        POST rather than GET because it mutates: a receive takes a lease,
        increments the delivery count, and hides the messages from every
        other consumer. Modelling that as a GET would invite a proxy or a
        retrying client to consume the queue by replaying what looks like
        a safe read.

        Returns an empty list, not an error, when nothing is available.
      operationId: receiveMessages
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReceiveRequest'
      responses:
        '200':
          description: Leased messages, possibly none.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceiveResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The queue's encryption key is unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  parameters:
    QueueId:
      name: queue_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
        example: a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
  schemas:
    ReceiveRequest:
      type: object
      properties:
        max_messages:
          type: integer
          minimum: 1
          maximum: 10
          description: >-
            How many messages to lease. Fewer may be returned even when more are
            available.
          example: 10
        visibility_timeout_seconds:
          type: integer
          minimum: 0
          maximum: 43200
          description: Overrides the queue's default for this lease only.
          example: 60
        wait_time_seconds:
          type: integer
          minimum: 0
          maximum: 20
          description: >-
            Block up to this long for a message rather than returning empty.
            Defaults to the queue's receive_wait_time_seconds.
          example: 20
    ReceiveResponse:
      type: object
      required:
        - messages
      properties:
        messages:
          type: array
          items:
            $ref: '#/components/schemas/ReceivedMessage'
    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
    ReceivedMessage:
      type: object
      required:
        - id
        - receipt_handle
        - body
        - receive_count
        - enqueued_at
        - visible_at
      properties:
        id:
          type: string
          format: uuid
          example: 019400aa-bbbb-7ccc-8ddd-eeeeffff0000
        receipt_handle:
          type: string
          description: |
            The credential for this lease. Required to delete the message
            or extend its lease, and valid only until the lease lapses.
          example: >-
            019400aabbbb7ccc8dddeeeeffff00003f2a91c4d5e60718293a4b5c6d7e8f9015a6b7c8d9e0f1a2b3c4d5e6f708192a3
        body:
          type: string
          example: '{"order_id":"A-1001"}'
        attributes:
          $ref: '#/components/schemas/MessageAttributes'
        receive_count:
          type: integer
          description: >-
            How many times this message has been leased, including now. Compare
            against the redrive policy to spot a poison message.
          example: 1
        message_group_id:
          type: string
          example: customer-42
        enqueued_at:
          type: string
          format: date-time
          example: '2026-01-18T11:45:00Z'
        visible_at:
          type: string
          format: date-time
          description: When this lease lapses and the message is redelivered.
          example: '2026-01-18T11:45:30Z'
    MessageAttributes:
      type: object
      description: |
        Typed attributes carried alongside the body. At most 20; values at
        most 1024 characters. `Binary` values are standard base64. The
        queue never coerces a value — it only checks the value is
        representable as the declared type.
      additionalProperties:
        type: object
        required:
          - type
          - value
        properties:
          type:
            type: string
            enum:
              - String
              - Number
              - Binary
            example: String
          value:
            type: string
            example: critical
      example:
        severity:
          type: String
          value: critical
        attempt:
          type: Number
          value: '2'
  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
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: NOT_FOUND
              message: Resource not found
              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.

````