Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Hands for Agents Quotes API
version: 0.1.0
description: 'Engineering hands and on-site presence in the Czech Republic (EU) for AI agents. A person designs, makes, assembles, measures, tests, verifies or ships a physical thing, documents it with evidence, and the agent''s operator gets an invoice from a registered EU company.
Provider: your engineering s.r.o., company ID (IČO) 09738100, Praha 4, Czech Republic.
Every request is screened and quoted by a human; nothing is accepted automatically. `GET /services` and `POST /quotes` need no authentication. The other operations need the `access_token` returned by `POST /quotes`, sent as a bearer token. The same five operations are MCP tools at ; the MCP tools get_status, create_task and confirm_delivery answer briefly by default (get_status with `full: true` gives the complete record), while this REST API always returns the complete record.'
termsOfService: https://handsforagents.com/terms.html
contact:
name: your engineering s.r.o.
email: tasks@handsforagents.com
url: https://handsforagents.com
servers:
- url: https://api.handsforagents.com/v1
tags:
- name: Quotes
paths:
/quotes:
post:
operationId: request_quote
summary: Send a task to a human for screening and a fixed-price quote
description: 'Not binding. A human answers within 24 hours (every day, including weekends and Czech public holidays). Required: description, email and name (the name of the person or company for the invoice); delivery_address is required too when a physical item is to be shipped, handed over or collected. The fields can be sent flat, at the top level (QuoteRequestFlat, the shape smaller models get right), or nested under task / client / agent (QuoteRequestNested); both are accepted, also mixed, and if a field is sent both ways the nested one wins. A missing required field is a 400 that names the field and shows an example. Put everything the user told you (quantity, material, colour, size, address) into the fields or into description. For name use the company or person from the shipping address (or the company the user gave); do not ask the user for it, ask only if there is neither. If the user gave an address, its country is the country (ISO 3166-1 alpha-2). A VAT id only if the user gave one: never guess or make one up. Never invent a quote_id or a status: a real quote_id starts with q_ and comes only from this tool''s own response. The answer carries quote_id and access_token: Tell the user to save the access_token now: it is shown once and is the only key to the quote and the task. If the access_token is lost: POST https://api.handsforagents.com/v1/quotes/{quote_id}/access-token (REST only, not a tool); a new one is sent to the contact e-mail. get_status cannot recover it.'
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/QuoteRequest'
examples:
shortest_possible:
value:
description: Make 20 rubber gaskets, 60 mm outer diameter, black EPDM.
email: ops@example.com
name: Example Inc.
with_address_and_file:
value:
description: Make 20 rubber gaskets, 60 mm outer diameter, black EPDM.
email: ops@example.com
name: Example Inc.
delivery_address: Example GmbH, Musterstr. 1, 10115 Berlin, DE, +49 30 000000
input_files:
- https://example.com/gasket-drawing.pdf
nested_form:
value:
task:
description: Make 20 rubber gaskets, 60 mm outer diameter, black EPDM.
delivery_address: Example GmbH, Musterstr. 1, 10115 Berlin, DE, +49 30 000000
input_files:
- https://example.com/gasket-drawing.pdf
client:
email: ops@example.com
name: Example Inc.
responses:
'201':
description: Request received
content:
application/json:
schema:
$ref: '#/components/schemas/QuoteReceipt'
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
tags:
- Quotes
/quotes/{quote_id}:
get:
operationId: get_quote_status
summary: get_status for a quote
security:
- taskToken: []
description: 'Always read next_action: one or two sentences on what to do now (wait; review the quote and call create_task; pay checkout_url; call confirm_delivery), with paid, quote_valid_until and expected_ship_date. messages is the thread with the operator, newest first (from: operator or agent). Every status can change at any time: call again whenever the user asks, never answer from memory, and report only what the response says, without conclusions of your own. This REST answer is always the complete record; the MCP tools get_status, create_task and confirm_delivery answer briefly by default (get_status with full=true returns this complete record). updates lists the last 3 status changes, oldest first, each with the task status at that moment; older_updates counts the earlier ones (every status change is also e-mailed to the contact address). To answer an operator question, call get_status with message: send the same request as POST with a JSON body {"message": "..."} (plain text, 1-2000 characters); it is stored in the same thread and e-mailed to the operator, and the answer is the usual status, including your message (message_sent: true). Read the operator''s message first, then reply: while the operator has written something you have not read yet, your message is NOT stored; the answer is the usual status with that message in messages, message_sent: false and a next_action that says so, and you send your answer with the next call. Send it once and only with what the user told you: the same text again within 10 minutes is ignored (duplicate_ignored: true). E-mail to tasks@handsforagents.com with the quote_id in the subject still works. test_mode true marks a test order: nothing is paid or invoiced.'
parameters:
- name: quote_id
in: path
required: true
schema:
type: string
responses:
'200':
description: The quote, or the refusal with its reason
content:
application/json:
schema:
$ref: '#/components/schemas/Quote'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
tags:
- Quotes
post:
operationId: get_quote_status_with_message
summary: get_status for a quote, with a message to the operator
security:
- taskToken: []
description: 'This is get_status with the optional message, as a POST so that the text is in the body, not in the URL (which servers log). Without a message (empty body, or {"message": null}) it is a plain status call, like the GET. With a message it is written to the thread and e-mailed to the operator, and the answer is the same as the GET''s, including your message (from: agent) and message_sent: true. Read the operator''s message first, then reply: while the operator has written something you have not read yet, nothing is stored or e-mailed; the answer is the GET''s, with that message in messages, message_sent: false and a next_action that says so, and you send your answer with the next call (HTTP 200 either way, so check message_sent). Plain text, 1-2000 characters, never interpreted as HTML; an empty or longer message is a 400. Limits: 10 messages per hour per quote (its quote_id and task_id share it) and 30 per hour per client address (429). The same text again within 10 minutes, while it is still the newest message of the thread, is not stored or e-mailed a second time (the answer then has duplicate_ignored: true). It needs the same access_token as the GET; without one, or with another quote''s, nothing is stored or sent (401).'
parameters:
- name: quote_id
in: path
required: true
schema:
type: string
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/StatusMessage'
responses:
'200':
description: The same answer as the GET, now including your message; if the operator had written something you had not read, message_sent is false, nothing was stored and the answer shows that message
content:
application/json:
schema:
$ref: '#/components/schemas/Quote'
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
tags:
- Quotes
/quotes/{quote_id}/access-token:
post:
operationId: request_access_token
summary: Recover a lost access_token
security: []
description: Always the same response, whether or not quote_id exists, so this cannot be used to find out which quote ids exist. If the quote exists and has a contact e-mail on file, a new access_token is generated and sent there; any access_token already issued for this quote keeps working. Rate-limited per quote.
parameters:
- name: quote_id
in: path
required: true
schema:
type: string
responses:
'202':
description: Always the same, generic acknowledgement
content:
application/json:
schema:
$ref: '#/components/schemas/AccessTokenRequestAck'
'429':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
tags:
- Quotes
/quotes/{quote_id}/files:
post:
operationId: upload_files
summary: Upload one or more input files to a quote
security:
- taskToken: []
description: An alternative to a URL in task.input_files or an e-mail attachment. multipart/form-data, one or more parts named `file`. At most 95 MB per request (this hosting's own ceiling), 20 files or 500 MB per quote in total, extensions step, stp, stl, 3mf, obj, iges, igs, dxf, dwg, pdf, png, jpg, jpeg, webp, zip only. Only while the quote, and any task made from it, is open.
parameters:
- name: quote_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
description: One or more files.
oneOf:
- type: string
format: binary
- type: array
items:
type: string
format: binary
responses:
'201':
description: Files stored
content:
application/json:
schema:
$ref: '#/components/schemas/UploadedFileList'
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'413':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'415':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
tags:
- Quotes
get:
operationId: list_files
summary: List files uploaded to a quote
security:
- taskToken: []
parameters:
- name: quote_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Files uploaded so far
content:
application/json:
schema:
$ref: '#/components/schemas/UploadedFileList'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
tags:
- Quotes
components:
schemas:
UploadedFileList:
type: object
required:
- files
properties:
files:
type: array
items:
type: object
required:
- name
- size
- sha256
- created_at
properties:
name:
type: string
description: Sanitized name as stored; may differ from the name you sent.
size:
type: integer
sha256:
type: string
created_at:
type: string
format: date-time
QuoteReceipt:
type: object
required:
- quote_id
- access_token
- status
- response_due_at
- nda_url
properties:
quote_id:
type: string
access_token:
type: string
status:
type: string
enum:
- received
response_due_at:
type: string
format: date-time
nda_url:
type: string
format: uri
const: https://handsforagents.com/nda.html
description: 'One-way non-disclosure agreement, pre-filled with the provider''s details. Optional: every task is confidential under the terms of service without it. The operator fills in its details, signs and e-mails the PDF to legal@handsforagents.com.'
upload_url:
type: string
format: uri
description: POST input files here (multipart, field name `file`) instead of a URL in task.input_files or an e-mail attachment. Same access_token, as a bearer token.
next_action:
type: string
description: 'One or two sentences in English: what to do now (tell the user to save the access_token, when the answer is due, which call to make next).'
QuoteRequest:
description: 'A quote request in either shape; the two can also be mixed, and the nested value wins if a field is sent both ways. Required in the flat shape: description, email, name; in the nested shape: task.description, client.email, client.name.'
anyOf:
- $ref: '#/components/schemas/QuoteRequestFlat'
- $ref: '#/components/schemas/QuoteRequestNested'
AccessTokenRequestAck:
type: object
required:
- message
properties:
message:
type: string
const: If the quote exists and has a contact e-mail, a new access_token was sent there.
QuoteRequestNested:
type: object
properties:
task:
type: object
properties:
description:
type: string
description: 'What is to be done, in plain language: everything the user told you (quantity, material, colour, size, tolerances, what counts as done). A detail that has no field of its own belongs here.'
maxLength: 20000
services:
type: array
description: Service ids from the service list, if you know them.
items:
type: string
enum:
- design
- make
- assemble
- measure
- test
- verify
- ship
deliverables:
type: string
description: What you expect to receive (item, files, report, photos, video). Optional - if you are not sure, leave it out and put it in task.description instead.
maxLength: 5000
deadline:
type: string
description: Latest acceptable delivery or dispatch date (ISO 8601). Optional; omit if the user gave none.
format: date
maxLength: 32
delivery_address:
type: string
description: 'Required whenever a physical item is to be shipped, handed over in person or collected; omit it only for purely digital results (files, a report). One plain string, not an object and not a list: recipient name, street, postcode and city, country; a phone number at the end only if you already have one (phone is optional - never stop to ask the user for it). If you do not have the address, ask the user for it; never invent one.'
maxLength: 1000
input_files:
type: array
description: HTTPS URLs of input files, for example photos, a sketch, a CAD model (STEP, STL), a PDF drawing, or other instructions, or attach the files to the e-mail. At most 20, each at most 2000 characters.
items:
type: string
maxLength: 2000
pattern: ^https://
maxItems: 20
currency:
type: string
description: Currency of the quote. Default EUR.
enum:
- EUR
- USD
required:
- description
client:
type: object
properties:
email:
type: string
description: E-mail of the operator. The quote, the payment link and the invoice go here.
format: email
maxLength: 254
pattern: ^[^\s@]+@[^\s@]+\.[^\s@]+$
name:
type: string
description: 'Required. The name of the person or company for the invoice: the registered name of the company that operates the agent, or a person''s full name. Use a company name or address already in the conversation; if you have none, ask the user.'
minLength: 1
maxLength: 200
pattern: ^[^\r\n]+$
country:
type: string
description: Country of the operator, ISO 3166-1 alpha-2 (upper case). Optional, but include it whenever you can derive it (for example from the operator's address) rather than leaving it out or asking the user.
pattern: ^[A-Z]{2}$
vat_id:
type: string
description: VAT or tax id of the operator, printed on the invoice. Optional. Only if the user gave it; never guess or make one up (a wrong number goes on the invoice). Leave it out otherwise.
maxLength: 32
required:
- email
- name
agent:
type: object
properties:
name:
type: string
description: Name of the agent or platform sending the request. Optional, for our records.
maxLength: 200
allow_anonymised_example:
type: boolean
description: Consent to publish this task as an anonymised example (no names, addresses or personal data), under the confidentiality section of the terms of service. Only set true if the client explicitly agreed to this; otherwise omit the field or leave it false. Default false; can be withdrawn.
default: false
QuoteRequestFlat:
type: object
properties:
description:
type: string
description: 'What is to be done, in plain language: everything the user told you (quantity, material, colour, size, tolerances, what counts as done). A detail that has no field of its own belongs here.'
maxLength: 20000
services:
type: array
description: Service ids from the service list, if you know them.
items:
type: string
enum:
- design
- make
- assemble
- measure
- test
- verify
- ship
deliverables:
type: string
description: What you expect to receive (item, files, report, photos, video). Optional - if you are not sure, leave it out and put it in task.description instead.
maxLength: 5000
deadline:
type: string
description: Latest acceptable delivery or dispatch date (ISO 8601). Optional; omit if the user gave none.
format: date
maxLength: 32
delivery_address:
type: string
description: 'Required whenever a physical item is to be shipped, handed over in person or collected; omit it only for purely digital results (files, a report). One plain string, not an object and not a list: recipient name, street, postcode and city, country; a phone number at the end only if you already have one (phone is optional - never stop to ask the user for it). If you do not have the address, ask the user for it; never invent one.'
maxLength: 1000
input_files:
type: array
description: HTTPS URLs of input files, for example photos, a sketch, a CAD model (STEP, STL), a PDF drawing, or other instructions, or attach the files to the e-mail. At most 20, each at most 2000 characters.
items:
type: string
maxLength: 2000
pattern: ^https://
maxItems: 20
currency:
type: string
description: Currency of the quote. Default EUR.
enum:
- EUR
- USD
email:
type: string
description: E-mail of the operator. The quote, the payment link and the invoice go here.
format: email
maxLength: 254
pattern: ^[^\s@]+@[^\s@]+\.[^\s@]+$
name:
type: string
description: 'Required. The name of the person or company for the invoice: the registered name of the company that operates the agent, or a person''s full name. Use a company name or address already in the conversation; if you have none, ask the user.'
minLength: 1
maxLength: 200
pattern: ^[^\r\n]+$
country:
type: string
description: Country of the operator, ISO 3166-1 alpha-2 (upper case). Optional, but include it whenever you can derive it (for example from the operator's address) rather than leaving it out or asking the user.
pattern: ^[A-Z]{2}$
vat_id:
type: string
description: VAT or tax id of the operator, printed on the invoice. Optional. Only if the user gave it; never guess or make one up (a wrong number goes on the invoice). Leave it out otherwise.
maxLength: 32
agent_name:
type: string
description: Name of the agent or platform sending the request. Optional, for our records.
maxLength: 200
allow_anonymised_example:
type: boolean
description: Consent to publish this task as an anonymised example (no names, addresses or personal data), under the confidentiality section of the terms of service. Only set true if the client explicitly agreed to this; otherwise omit the field or leave it false. Default false; can be withdrawn.
default: false
required:
- description
- email
- name
Error:
type: object
required:
- error
properties:
error:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
Quote:
type: object
required:
- quote_id
- status
properties:
quote_id:
type: string
status:
type: string
enum:
- received
- quoted
- refused
- expired
refusal_reason:
type: string
description: Present when status is refused. Refused requests are not invoiced.
refusal_code:
type: string
enum:
- credentials
- impersonation
- surveillance
- social-manipulation
- verification-bypass
- referral-fraud
- weapons-regulated
- counterfeit
- reshipping-fraud
- illegal
- capability-limit
- sensitive-unverified
- other
description: 'Present when status is refused: the category from the task policy, or capability-limit / sensitive-unverified / other.'
task_id:
type: string
description: Present once create_task was called for this quote.
price:
type: object
required:
- amount
- currency
properties:
amount:
type: number
description: Fixed total. No VAT is added; the provider is not VAT-registered.
currency:
type: string
enum:
- EUR
- USD
payment_schedule:
type: object
required:
- mode
description: 'Up to 150 EUR: full_prepayment or card_hold. Above: deposit_and_balance.'
properties:
mode:
type: string
enum:
- full_prepayment
- card_hold
- deposit_and_balance
deposit:
type: object
required:
- amount
- currency
properties:
amount:
type: number
description: Fixed total. No VAT is added; the provider is not VAT-registered.
currency:
type: string
enum:
- EUR
- USD
balance:
type: object
required:
- amount
- currency
properties:
amount:
type: number
description: Fixed total. No VAT is added; the provider is not VAT-registered.
currency:
type: string
enum:
- EUR
- USD
scope:
type: string
deliverables:
type: string
evidence:
type: array
items:
type: string
description: What the evidence package will contain.
subcontracted:
type: string
description: Processes done by subcontractors, if any.
delivery_date:
type: string
format: date
inbound_address:
type: string
description: Where to send parts or parcels, if the task needs them.
operator_verification_required:
type: boolean
description: True for the sensitive task categories.
valid_until:
type: string
format: date
terms_version:
type: string
next_action:
type: string
description: 'One or two sentences in English: what the agent or the client should do now.'
messages:
$ref: '#/components/schemas/OperatorMessages'
duplicate_ignored:
type: boolean
description: Present and true only in the answer to a POST whose message was ignored because the same text was already the newest message of the thread (sent within the last 10 minutes).
message_sent:
type: boolean
description: 'Present only in the answer to a POST with a message: true, the message was stored and e-mailed to the operator; false, it was NOT stored because the operator had written something you had not read yet (that message is in messages, now marked as read; next_action says so): send your answer again. Absent when the same text was ignored (duplicate_ignored) and in every plain status call.'
test_mode:
type: boolean
description: 'Present and true only for a test quote: an order made from it is a test order.'
StatusMessage:
type: object
description: Optional body of POST /quotes/{quote_id} and POST /tasks/{task_id}. An empty body is a plain status call.
properties:
message:
type:
- string
- 'null'
minLength: 1
maxLength: 2000
description: 'Your reply to the operator: plain text, 1-2000 characters, never HTML. Null or absent: a plain status call.'
OperatorMessages:
type: array
description: 'The thread with the operator, newest first: the operator''s notes and your own replies (from: agent over REST; the MCP tools show your own replies as from: you). An operator message you have not been shown yet is always included, also in the answer to your own reply (a message of yours sent while it is unread is not stored: message_sent is false). Read the operator''s message first, then reply: call get_status with message (POST with a JSON body over REST); e-mailing the tasks address with the quote_id in the subject also works.'
items:
type: object
required:
- at
- from
- text
properties:
at:
type: string
format: date-time
from:
type: string
enum:
- operator
- agent
text:
type: string
description: Plain text, at most 2000 characters. Never HTML.
securitySchemes:
taskToken:
type: http
scheme: bearer
description: access_token returned by POST /quotes. Valid for that quote and the task created from it.
x-status: live