# Quotations API

The Quotations API lets a tenant's authorized staff prepare and send a price proposal to a customer. A quotation is owned by one seller `account_id`; any listing lines must belong to that same account and tenant. Customers can retrieve, accept, or reject only quotations assigned to their authenticated `user_id`.

All examples use Bearer authentication. Tenant and user identity are resolved from the authenticated request by the gateway; do not send separate identity headers.

```bash
export API="https://api.tradly.app"
export TOKEN="<bearer-token>"
```

## Important: quotation vs. buyer RFQ

The API supports both buyer-submitted quote requests (RFQ/RFP) and seller-issued quotations. A buyer submits a request to an active seller account; the authenticated buyer owns it and can read or cancel it. A seller quotation is a separate object and may be linked to the request it answers. The request endpoints below document the buyer-facing flow; staff-only review and quotation-management actions remain in the tenant operations flow.

## Buyer quote requests (RFQ/RFP)

Use these endpoints for a storefront “Request a quote” action on an account or listing. Every request belongs to one active `account_id` in the authenticated tenant. The buyer identity comes from the Bearer token; clients cannot submit a request on behalf of another `user_id`.

### Submit a quote request

`POST /v1/quotations/requests`

Send a `quote_request` object. Include at least one of `subject`, `message`, `items`, or `attributes`. If `items` are supplied, every item needs a non-empty `title` and positive `quantity`; `listing_id` is optional, but when supplied the listing must belong to the selected account and tenant. Item quantity supports up to four decimal places.

```bash
curl -X POST "$API/v1/quotations/requests" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-raw '{
    "quote_request": {
      "account_id": 742,
      "subject": "Bulk laundry service quote",
      "message": "Please quote weekly pickup and delivery for our hotel.",
      "currency_id": 13,
      "items": [
        {
          "listing_id": 812,
          "title": "Commercial laundry service",
          "quantity": 250,
          "notes": "Kilograms per week",
          "metadata": {"service_frequency": "weekly"}
        }
      ],
      "attributes": [
        {"id": 901, "values": ["Weekly"]},
        {"id": 902, "values": ["Hotel"]}
      ]
    }
  }'
```

The API generates `unique_id` (a UUID) and `request_number` when omitted. The response is under `data.quote_request`; it includes the database `id`, public `unique_id`, `request_number`, `status`, `account`, `items`, and configured `attributes`. To discover request attribute definitions, call `GET /v1/attributes?type=quote_requests`; attribute values use the shared `{ "id", "values" }` format.

### List your quote requests

`GET /v1/quotations/requests`

An authenticated buyer receives only their own requests. Optional query parameters are `page` (default `1`), `per_page` (default `30`, maximum `100`), and `status`.

```bash
curl "$API/v1/quotations/requests?page=1&per_page=30&status=submitted" \
  -H "Authorization: Bearer $TOKEN"
```

The response contains `data.quote_requests` and `data.pagination` (`page`, `per_page`, `total`). Each request may include seller quotations linked as responses.

### Retrieve one of your quote requests

`GET /v1/quotations/requests/{id}`

Use the request's generated `unique_id` or `request_number`. (`id` is the database integer and is not accepted by this endpoint.) A buyer can retrieve only their own request; another customer's request is returned as not found.

```bash
curl "$API/v1/quotations/requests/RFQ-AB12CD34" \
  -H "Authorization: Bearer $TOKEN"
```

### Cancel your quote request

`POST /v1/quotations/requests/{id}/cancel`

The owner can cancel a request while its status is `submitted` or `in_review`. For this action, `{id}` must be the request's `unique_id` UUID returned by the API (not the database integer `id` or human-readable `request_number`). No request body is required.

```bash
curl -X POST "$API/v1/quotations/requests/UNIQUE-REQUEST-UUID/cancel" \
  -H "Authorization: Bearer $TOKEN"
```

## Buyer endpoint summary

| Method | Endpoint | Buyer access | Purpose |
|---|---|---|---|
| `POST` | `/v1/quotations/requests` | Authenticated buyer | Submit a request to an account |
| `GET` | `/v1/quotations/requests` | Authenticated buyer | List the buyer's own requests |
| `GET` | `/v1/quotations/requests/{id}` | Request owner | Retrieve the buyer's request and linked replies |
| `POST` | `/v1/quotations/requests/{id}/cancel` | Request owner | Cancel a submitted or in-review request |

## Quotation lifecycle

`draft` → `sent` → `accepted` or `rejected`; staff may cancel a draft, sent, or accepted quotation. An accepted quotation can be converted to an order. Expired sent quotations can no longer be accepted or rejected. There is no delete endpoint.

## Manage quotations (tenant staff)

Quotation management endpoints require an authenticated tenant administrator or staff member authorized by the tenant's gateway permissions. Customer access is described below.

### Create a draft

`POST /v1/quotations`

Send a `quotation` object with an active seller `account_id`, `currency_id`, and at least one line in `items`. `user_id` may be supplied to associate the quotation with a customer account; alternatively provide a customer email and name for email delivery. When listing IDs are supplied, each listing must belong to the same tenant and seller account.

```bash
curl -X POST "$API/v1/quotations" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-raw '{
    "quotation": {
      "account_id": 42,
      "user_id": 123,
      "customer_name": "Alex Customer",
      "sent_to_email": "alex@example.com",
      "customer_phone": "+15551234567",
      "currency_id": 13,
      "valid_until": "2026-12-31T23:59:59Z",
      "subtotal": 500,
      "discount_total": 0,
      "tax_total": 0,
      "shipping_total": 0,
      "grand_total": 500,
      "notes": "Prepared for the requested items.",
      "terms": "Valid until the date shown.",
      "items": [
        {
          "listing_id": 729341,
          "variant_id": 0,
          "title": "Custom service",
          "sku": "CUSTOM-01",
          "quantity": 1,
          "unit_price": 500,
          "discount_rate": 0,
          "discount_amount": 0,
          "tax_rate": 0,
          "tax_amount": 0,
          "line_total": 500
        }
      ]
    }
  }'
```

The response contains `data.quotation`, including the generated `id` (a unique ID used by action endpoints), `quotation_number`, `status`, seller account, and items. The API accepts `quotation_number` if supplied; otherwise it generates one.

### List drafts and quotations

`GET /v1/quotations`

Staff see quotations for their tenant. Optional query parameters: `page` (default `1`), `per_page` (default `30`, maximum `100`), `status`, `customer` (matches customer name or email), `search` (matches quotation number, name, or email), and `from` / `to` (creation-date bounds).

```bash
curl "$API/v1/quotations?page=1&per_page=20&status=draft" \
  -H "Authorization: Bearer $TOKEN"
```

The response contains `data.quotations` and `data.pagination` (`page`, `per_page`, `total`).

### Get a quotation

`GET /v1/quotations/{id}`

Staff may use the quotation's unique `id` or `quotation_number`. A customer may use the unique `id` or `quotation_number` only when the quotation's `user_id` matches their authenticated user. Unauthorized customer lookups return not found.

```bash
curl "$API/v1/quotations/QT-12345678" \
  -H "Authorization: Bearer $TOKEN"
```

### Update a draft

`PATCH /v1/quotations/{id}`

Staff can edit drafts only. The body can contain a `quotation` wrapper or the fields directly. To replace line items, send the full `items` array; the API replaces all existing lines. Changing the account or items revalidates that all referenced listings belong to the selected account and tenant.

```bash
curl -X PATCH "$API/v1/quotations/QUOTE-UNIQUE-ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-raw '{
    "quotation": {
      "valid_until": "2027-01-31T23:59:59Z",
      "grand_total": 550,
      "items": [
        {"listing_id": 729341, "title": "Custom service", "quantity": 1, "unit_price": 550, "line_total": 550}
      ]
    }
  }'
```

### Send the quotation

`POST /v1/quotations/{id}/send`

Staff can send a draft only. A recipient email is required. Sending changes the status to `sent` and attempts to email the quotation using the tenant's email configuration. The send action succeeds independently of whether the email provider later delivers the message.

```bash
curl -X POST "$API/v1/quotations/QUOTE-UNIQUE-ID/send" \
  -H "Authorization: Bearer $TOKEN"
```

### Cancel a quotation

`POST /v1/quotations/{id}/cancel`

Staff can cancel a draft, sent, or accepted quotation.

```bash
curl -X POST "$API/v1/quotations/QUOTE-UNIQUE-ID/cancel" \
  -H "Authorization: Bearer $TOKEN"
```

### Duplicate a quotation

`POST /v1/quotations/{id}/duplicate`

Staff can duplicate a quotation into a new draft with a new unique ID and quotation number. Its account, customer, and listing references are revalidated.

```bash
curl -X POST "$API/v1/quotations/QUOTE-UNIQUE-ID/duplicate" \
  -H "Authorization: Bearer $TOKEN"
```

### Convert an accepted quotation to an order

`POST /v1/quotations/{id}/convert`

Staff can convert an accepted, unexpired quotation when every line references a listing. `payment_method_id` and `shipping_method_id` are required; `shipping_address` and `billing_address` may also be supplied. Conversion creates an order in the existing incomplete/unpaid state; it does not capture payment. Repeating a successful conversion returns the previously created order ID.

```bash
curl -X POST "$API/v1/quotations/QUOTE-UNIQUE-ID/convert" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-raw '{
    "payment_method_id": 7175,
    "shipping_method_id": 1,
    "shipping_address": {},
    "billing_address": {}
  }'
```

## Customer actions

The customer must authenticate as the same Tradly user referenced by the quotation's `user_id`. These endpoints do not expose quotations addressed only to an email address, and there is no guest response link/token flow.

### Accept or reject a sent quotation

`POST /v1/quotations/{id}/accept`

`POST /v1/quotations/{id}/reject`

Only the assigned customer can accept or reject a sent, unexpired quotation. No request body is required.

```bash
curl -X POST "$API/v1/quotations/QUOTE-UNIQUE-ID/accept" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN"
```

```bash
curl -X POST "$API/v1/quotations/QUOTE-UNIQUE-ID/reject" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN"
```

## Endpoint summary

| Method | Endpoint | Access | Purpose |
|---|---|---|---|
| `POST` | `/v1/quotations` | Tenant staff | Create a draft |
| `GET` | `/v1/quotations` | Tenant staff | List tenant quotations |
| `GET` | `/v1/quotations/{id}` | Staff or assigned customer | Retrieve a quotation |
| `PATCH` | `/v1/quotations/{id}` | Tenant staff | Update a draft |
| `POST` | `/v1/quotations/{id}/send` | Tenant staff | Send a draft to its customer |
| `POST` | `/v1/quotations/{id}/accept` | Assigned customer | Accept a sent quotation |
| `POST` | `/v1/quotations/{id}/reject` | Assigned customer | Reject a sent quotation |
| `POST` | `/v1/quotations/{id}/cancel` | Tenant staff | Cancel a quotation |
| `POST` | `/v1/quotations/{id}/duplicate` | Tenant staff | Copy as a new draft |
| `POST` | `/v1/quotations/{id}/convert` | Tenant staff | Convert accepted quotation to an unpaid order |

## Related APIs

- [Accounts](accounts) — seller accounts that own quotations.
- [Listings](listings) — catalog listings referenced by quotation lines.
- [Orders](orders) — order lifecycle after a quotation is converted.
