Skip to content
A1 QuoteQuote
PricingDocsFor suppliers
Sign inRequest my next purchase quote

A1 Quote. Quotations on your behalf, form replies in one place.

PrivacyTermsDocsSupplier portal

API documentation

The API uses the same service layer and product rules.

Your key: Settings → API keys. Header Authorization: Bearer.

On this page
  1. Getting started
  2. 1. Base and authentication
  3. 2. Resources and endpoints
  4. 3. Examples
  5. 3.1 POST /quotations (create_quotation)
  6. 3.2 POST /quotations/{id}/send (send_rfq)
  7. 3.3 Comparison (abridged)
  8. 4. Webhooks
  9. 5. Errors
  10. 6. MCP server
  11. Supplier communication language

Getting started

  1. Create a key in Settings → API keys. Each key belongs to one business.
  2. Create a project: POST /api/v1/projects
  3. Create a quotation with its items: POST /api/v1/projects/{id}/quotations
  4. Attach and approve suppliers before sending: POST /api/v1/quotations/{id}/invitations
  5. Send and track: POST /api/v1/quotations/{id}/send / GET /api/v1/quotations/{id}/compare
bash
curl -X POST https://cotacaoia-a1.netlify.app/api/v1/projects \
  -H "Authorization: Bearer cot_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Example purchase" }'
Create an API key

Reference under review. Examples and identifiers retain the original contract data; verify availability and format before integrating.

1. Base and authentication#

Item Value
Base URL https://cotacaoia-a1.netlify.app/api/v1
Auth header Authorization: Bearer cot_live_<chave> — API key per business
Format JSON, UTF-8, timestamps ISO-8601 UTC; commercial amounts in currency units (for example, 12.50) with currency: "BRL", "USD" or "EUR". Stripe billing has its own contract.
IDs opaque strings (cuid2), without a prefix; historical examples preserve their identifiers
Idempotency Each operation has its own rules; MCP has no generic Idempotency-Key contract.
Pagination ?limit=50&cursor=<opaco> → { data: [...], next_cursor: "..." | null }
Rate limit No per-key limit is documented in this version.
Versioning /v1 in the path; new fields are additive; removal = /v2

Keys are created in Settings → API keys, shown once and stored as hashes. A key grants full access to the business, without per-tool scopes or automatic expiration; it can be revoked. Keys with the cot_test_ prefix are rejected in production, including key creation. Outside production, the prefix does not create a sandbox or block real sends.

2. Resources and endpoints#

Historical reference for a proposed contract: not all endpoints, fields and examples in sections 2–5 were implemented. For MCP, use the names, schemas and limitations in section 6. Do not assume outgoing webhooks or header-based idempotency are available.

Resource Endpoints Notes
projects GET /projects · POST /projects · GET /projects/{id} · PATCH /projects/{id} Groups requests for quotation
quotations GET /quotations?project_id= · POST /quotations · GET /quotations/{id} · PATCH /quotations/{id} · POST /quotations/{id}/cancel status: draft | sent | collecting | closed | ordered | cancelled; content in blocks
suppliers GET /suppliers · POST /suppliers · GET /suppliers/{id} · PATCH /suppliers/{id} · POST /suppliers/import (CSV) · POST /suppliers/search Search returns unsaved candidates (Google ToS: only google_place_id is saved when added)
quotation recipients POST /quotations/{id}/suppliers (attaches, pending) · POST /quotations/{id}/suppliers/approve · DELETE /quotations/{id}/suppliers/{supplier_id} Nothing is sent without approval (approved_by, approved_at)
send POST /quotations/{id}/send = send_rfq; creates N invitations, queues a job; idempotent
invitations GET /quotations/{id}/invitations · GET /invitations/{id} · POST /invitations/{id}/resend (1x, manual) channel: email | whatsapp; status: queued | sent | delivered | opened | responded | declined | bounced | failed | opted_out
quotes GET /quotations/{id}/quotes · GET /quotes/{id} · POST /quotes/{id}/confirm · PATCH /quotes/{id} (correct extracted fields) source: form | extracted; is_confirmed; each extracted field has evidence
compare GET /quotations/{id}/compare Item × supplier matrix + summary (AI, with citations)
orders POST /orders · GET /orders/{id} · GET /quotations/{id}/orders status: sent | confirmed | rejected; can split items between suppliers
templates GET /templates · POST /quotations/from-template Industry templates (food service, clinic)
usage GET /usage?from=&to= Credits used, sends per channel, AI cost — mirrors usage_events
webhooks GET /webhooks · POST /webhooks · DELETE /webhooks/{id} Customer endpoints for the events in section 4
opt-out (public) GET /sair/{token} · POST /sair/{token} No auth; records opted_out_at global
public GET /q/{token} · POST /q/{token}/quote · GET /o/{token} · POST /o/{token}/confirm Supplier pages/actions, no account required

3. Examples#

3.1 POST /quotations (create_quotation)#

http
POST /api/v1/quotations
Authorization: Bearer cot_live_xxx
Content-Type: application/json
json
{
  "project_id": "prj_01j8x",
  "title": "Embalagens e descartáveis — setembro",
  "category": "embalagens e descartáveis",
  "deadline_at": "2026-09-12T21:00:00Z",
  "delivery_city": "Campinas/SP",
  "content": [
    {
      "type": "items",
      "columns": ["qty", "unit", "description", "brand"],
      "rows": [
        { "qty": 500, "unit": "un", "description": "Embalagem kraft para viagem M", "brand": "" },
        { "qty": 2000, "unit": "un", "description": "Copo 300 ml PP transparente", "brand": "Copobras ou similar" },
        { "qty": 20, "unit": "pct", "description": "Guardanapo 33x33 c/ 50", "brand": "" }
      ]
    },
    { "type": "text", "body": "Entrega na loja, de segunda a sexta, até 11h. Frete incluso no preço, por favor." }
  ]
}

Response 201:

json
{
  "id": "qtn_01j8y",
  "number": "0042",
  "project_id": "prj_01j8x",
  "workspace_id": "wrk_01j8a",
  "status": "draft",
  "title": "Embalagens e descartáveis — setembro",
  "category": "embalagens e descartáveis",
  "deadline_at": "2026-09-12T21:00:00Z",
  "delivery_city": "Campinas/SP",
  "items": [
    { "id": "qti_1", "qty": 500, "unit": "un", "description": "Embalagem kraft para viagem M", "brand": "" },
    { "id": "qti_2", "qty": 2000, "unit": "un", "description": "Copo 300 ml PP transparente", "brand": "Copobras ou similar" },
    { "id": "qti_3", "qty": 20, "unit": "pct", "description": "Guardanapo 33x33 c/ 50", "brand": "" }
  ],
  "content": [ "...(como enviado)..." ],
  "suppliers": [],
  "created_at": "2026-09-05T14:02:11Z",
  "updated_at": "2026-09-05T14:02:11Z"
}

3.2 POST /quotations/{id}/send (send_rfq)#

Preconditions: status = draft, ≥ 1 approved supplier, business with credits, tenant not paused (bounce/complaint), no supplier with opted_out_at.

http
POST /api/v1/quotations/qtn_01j8y/send
Authorization: Bearer cot_live_xxx
Idempotency-Key: 7f3c1a2e-send-0042
Content-Type: application/json
json
{
  "channels": ["email", "whatsapp"],
  "followup_after_hours": 24,
  "message_note": "Se puder, indique frete separado."
}

Response 202 (sending is asynchronous; check invitations or subscribe to webhooks):

json
{
  "quotation_id": "qtn_01j8y",
  "status": "sent",
  "job_id": "run_01j8z",
  "invitations": [
    { "id": "inv_a1", "supplier_id": "sup_9k", "channel": "email", "status": "queued" },
    { "id": "inv_a2", "supplier_id": "sup_9k", "channel": "whatsapp", "status": "queued" },
    { "id": "inv_b1", "supplier_id": "sup_7m", "channel": "email", "status": "queued" },
    { "id": "inv_b2", "supplier_id": "sup_7m", "channel": "whatsapp", "status": "skipped", "skip_reason": "no_whatsapp_opt_in" }
  ],
  "credits_consumed": 1,
  "credits_remaining": 11
}

WhatsApp requires permission for the channel; an email response does not open the WhatsApp window. Outside the window started by a supplier message on WhatsApp, Meta requirements and approved templates apply. Repeating with the same Idempotency-Key returns the same body without new sends.

3.3 Comparison (abridged)#

json
{
  "quotation_id": "qtn_01j8y",
  "items": [
    {
      "item_id": "qti_1",
      "description": "Embalagem kraft para viagem M",
      "qty": 500,
      "offers": [
        { "supplier_id": "sup_9k", "quote_id": "quo_1", "unit_price_cents": 89, "delivery_days": 3, "is_confirmed": true, "is_best": true },
        { "supplier_id": "sup_7m", "quote_id": "quo_2", "unit_price_cents": 97, "delivery_days": 2, "is_confirmed": false, "is_best": false }
      ]
    }
  ],
  "totals_by_supplier": [
    { "supplier_id": "sup_9k", "total_cents": 184250, "items_quoted": 3, "items_missing": 0 },
    { "supplier_id": "sup_7m", "total_cents": 191000, "items_quoted": 2, "items_missing": 1 }
  ],
  "not_responded": ["sup_3p"],
  "summary": {
    "text": "Distribuidora Boa Vista tem o menor total (R$ 1.842,50) e cotou os 3 itens; Atacado Central entrega 1 dia antes mas não cotou o guardanapo e o copo ainda precisa de confirmação.",
    "citations": [ { "quote_id": "quo_1", "field": "total" }, { "quote_id": "quo_2", "field": "delivery_days" } ]
  }
}

4. Webhooks#

Proposed contract for outgoing webhooks. Confirm availability in this instance before integrating. The table and payload below describe the planned contract; they do not guarantee active delivery.

Event When
quotation.sent job send_rfq completed
invitation.delivered · invitation.opened · invitation.bounced · invitation.failed normalized Resend / Meta events
invitation.opted_out supplier opted out (email or SAIR)
quote.received Quote created (form or extraction)
quote.needs_confirmation extraction completed with is_confirmed=false
quote.confirmed buyer confirmed/corrected
quotation.deadline_reached deadline passed; status → closed
order.sent · order.confirmed · order.rejected order lifecycle
workspace.paused circuit breaker (bounce > 4%, complaint > 0.08%) or no credits
usage.credit_low remaining credits below the plan threshold

Payload:

json
{
  "id": "evt_01j9a",
  "type": "quote.received",
  "created_at": "2026-09-06T12:40:03Z",
  "workspace_id": "wrk_01j8a",
  "data": { "quote_id": "quo_1", "quotation_id": "qtn_01j8y", "supplier_id": "sup_9k", "source": "form", "is_confirmed": true }
}

5. Errors#

json
{
  "error": {
    "type": "validation_error",
    "code": "recipient_not_approved",
    "message": "Fornecedor sup_7m ainda não foi aprovado para esta cotação.",
    "param": "suppliers[1]",
    "doc_url": "https://cotacaoia-a1.netlify.app/docs/errors#recipient_not_approved",
    "request_id": "req_01j9b"
  }
}
HTTP type Common codes
400 validation_error invalid_field, recipient_not_approved, deadline_in_past, no_recipients
401 authentication_error invalid_api_key, key_revoked
402 billing_error no_credits, subscription_inactive
403 permission_error workspace_paused (circuit breaker), test_key_cannot_send
404 not_found quotation_not_found (includes cross-tenant: never reveal existence)
409 conflict invalid_status_transition, supplier_opted_out, idempotency_key_reused_with_different_body
422 unprocessable search_source_unavailable, template_paused (WhatsApp)
429 rate_limited too_many_requests (+ Retry-After)
500 internal_error always with request_id

Error messages are in pt-BR (shown to users by the UI); code is stable and in English for the agent.


6. MCP server#

Tools, an agent workflow, and client installation are documented at /docs/mcp. Server URL: https://cotacaoia-a1.netlify.app/api/mcp, with the same API key.

Supplier communication language#

POST /api/v1/suppliers, PATCH /api/v1/suppliers/{id} and the add_supplier MCP tool accept the optional communication_locale field. Valid values: pt-BR, en or es. New suppliers default to pt-BR when the field is omitted; an update without this field preserves the existing preference.

The preference selects the language of request and order emails and configured WhatsApp templates. It does not change currency, country, time zone or original item and terms content. WhatsApp still requires recorded permission and an appropriate approved template. A language without a configured template is not silently replaced with Portuguese.