procurement.txt

Agent Guide

This guide is for developers building AI purchasing agents, procurement bots, or automated buying systems that consume procurement.txt files.


Discovery

An agent should follow this discovery sequence:

  1. Request https://{domain}/procurement.txt via HTTPS.
  2. If the response is 404, try https://{domain}/.well-known/procurement.txt.
  3. If neither location returns a 200 response, assume no procurement.txt exists. Do not infer procurement capabilities.
async function discover(domain) {
  const primary = await fetch(`https://${domain}/procurement.txt`)
  if (primary.ok) return primary.text()
 
  if (primary.status === 404) {
    const fallback = await fetch(`https://${domain}/.well-known/procurement.txt`)
    if (fallback.ok) return fallback.text()
  }
 
  return null // No procurement.txt found
}

Parsing

Parse the file line by line:

  1. Strip any UTF-8 BOM at the start of the file.
  2. Ignore blank lines (empty or whitespace-only).
  3. Ignore comment lines (starting with #).
  4. Split each directive on the first occurrence of : (colon + space).
  5. The text before : is the field name; the text after is the value.
  6. Normalize field names to lowercase for matching.
function parse(text) {
  const fields = {}
  const errors = []
 
  // Strip BOM
  if (text.startsWith('\uFEFF')) text = text.slice(1)
 
  for (const [i, line] of text.split(/\r?\n/).entries()) {
    if (!line.trim() || line.trimStart().startsWith('#')) continue
 
    const sep = line.indexOf(': ')
    if (sep === -1) {
      errors.push({ line: i + 1, message: `Missing ': ' separator` })
      continue
    }
 
    const key = line.slice(0, sep).toLowerCase()
    const value = line.slice(sep + 2)
 
    // Repeatable fields
    if (key === 'contact' || key === 'escalation' || key === 'commerce-protocol') {
      fields[key] = [...(fields[key] || []), value]
    } else {
      fields[key] = value // last value wins for duplicates
    }
  }
 
  return { fields, errors }
}

Handling API URIs

When a file declares Ordering: api <URI>, Pricing: api <URI>, or any capability field with api <URI> (e.g., Quote: api <URI>, Tracking: api <URI>), the URI points to a machine-readable API specification or structured documentation — not a raw endpoint.

What to expect at the URI:

  • An OpenAPI 3.x specification (JSON or YAML) — the preferred format
  • A structured API documentation page
  • Another machine-readable description (e.g., Postman collection)

How to handle it:

  1. Fetch the URI.
  2. Detect the format (check Content-Type, or look for OpenAPI signature fields like openapi, info, paths).
  3. Parse the specification to determine available endpoints, request schemas, and response formats.
  4. Read the spec's securitySchemes and security fields to understand authentication requirements. Credentials are pre-established out-of-band — the spec tells you what method is required, not how to acquire credentials on-the-fly.

Merchants will often use the same OpenAPI spec URI across multiple fields (Ordering, Pricing, Quote, etc.). This is expected — fetch the spec once and use the relevant paths for each operation.

If the URI does not return a recognizable API specification, treat the field as if it were on-request and fall back to Contact or Escalation.


Unknown Fields

Agents MUST ignore fields they do not recognize. This includes:

  • Fields beginning with X- (extension fields)
  • Any field name not in the v1 field list
  • Fields from future versions

Do not treat unknown fields as errors. This ensures forward compatibility.


Missing Fields

If an optional field is absent, do not assume a default behavior.

  • No Negotiation field does not mean negotiation is available or unavailable — it is unknown.
  • No individual capability field (e.g. no Quote field) means that capability has not been declared — it may or may not be available.
  • No Rate-Limit field: default to conservative rates (≤1 request/second).

Caching

  • Respect standard HTTP caching headers (Cache-Control, ETag, Last-Modified).
  • If an Expires field is present in the file body, re-fetch after that date regardless of HTTP cache headers.
  • If a file is expired (Expires is in the past), treat the data as stale and re-fetch if possible.

Human Escalation

When handing off a transaction to a human:

  1. Prefer Escalation URIs over Contact URIs — they are intended for active transaction hand-offs.
  2. If no Escalation field exists, fall back to Contact.
  3. Support multiple contact/escalation entries — try them in order or present the best match.

Rate Limits

  • Respect the advisory Rate-Limit field.
  • If absent, default to no more than 1 request per second to the merchant's purchasing endpoints.
  • The field is advisory, not enforced. Being a good citizen protects you from being blocked.

Security Considerations

Fetch over HTTPS only

Agents MUST only fetch procurement.txt over HTTPS. Do not follow redirects from the https:// URL to http://.

Don't treat the file as proof of identity

procurement.txt is unsigned and unauthenticated. Verify:

  • The file is served over HTTPS.
  • The domain matches the merchant you intend to interact with.
  • The Location header of any redirect stays on the same domain.

Do not follow redirects from procurement.txt URLs to a different domain without flagging the redirect to the user.

Cross-domain URIs

The Contact, Escalation, Catalog, and API specification URIs may point to different domains (e.g., a CDN-hosted OpenAPI spec, or a third-party documentation platform). Flag cross-domain API URIs for human review before executing transactions, unless your agent has been explicitly configured to trust cross-domain references for that merchant.

Don't auto-execute purchases

Agents MUST NOT auto-execute financial transactions — purchases, payments, or commitments — based solely on procurement.txt without human authorization or a pre-configured trust policy.

This file describes capabilities and declares intent. All transactions remain subject to the merchant's terms of service, applicable law, and any required human authorization.

Canonical-Hash verification

If the Canonical-Hash field is present, verify it:

  1. Remove the Canonical-Hash line from the fetched content.
  2. Normalize line endings to LF.
  3. Compute SHA-256 of the remaining content.
  4. Compare the hex digest with the declared value (the part after sha256:).

Treat a mismatch as a warning, not a hard failure. The merchant may have updated the file without yet recomputing the hash. A match gives additional confidence that the file has not been altered since publication.

async function verifyCanonicalHash(rawText, declaredHash) {
  if (!declaredHash.startsWith('sha256:')) return 'unsupported-algorithm'
  const expected = declaredHash.slice(7)
 
  // Remove the canonical-hash line and normalize line endings
  const stripped = rawText
    .split(/\r?\n/)
    .filter(line => !line.toLowerCase().startsWith('canonical-hash:'))
    .join('\n')
 
  const encoded = new TextEncoder().encode(stripped)
  const hashBuffer = await crypto.subtle.digest('SHA-256', encoded)
  const computed = Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
 
  return computed === expected ? 'match' : 'mismatch'
}

Handling Commerce-Protocol Fields

When a Commerce-Protocol field is present:

  1. Prefer the protocol for supported transaction types. UCP and ACP handle one-shot purchases with integrated payment. For these transactions, use the declared endpoint directly rather than the Ordering: api path.
  2. Ignore unrecognized protocol names. If you encounter Commerce-Protocol: xyz https://… and don't support xyz, ignore that line and fall back to other declared ordering methods.
  3. Both declared is fine. If a merchant declares both Commerce-Protocol and Ordering: api, use whichever suits the transaction: the protocol for fast consumer-style purchases, the API for complex B2B orders.
  4. Ordering: protocol without a Commerce-Protocol field means the ordering method is unknown — fall back to Contact.

Using Interaction-Model

The Interaction-Model field tells you upfront how much human involvement to expect:

ValueWhat it means for your agent
automatedYou can complete the transaction end-to-end. No approval step needed.
approval-requiredPrepare and submit the order, then wait for human authorization before it is fulfilled.
human-ledUse the file for discovery only. Surface findings to a human who will initiate the transaction.
hybridCheck X-Auto-Approve-Under (if present) to determine whether this specific order qualifies for automated approval.

If Interaction-Model is absent, treat the interaction model as unknown — do not assume automation is permitted.


Agent Readiness Scores

Use the following heuristic to score how ready a merchant's setup is for automated procurement:

ScoreCriteria
HIGH+HIGH + at least two of: Service-Region, Payment-Terms, Auth, Min-Order, Interaction-Model
HIGHValid file + (Ordering + Pricing) or Commerce-Protocol
MEDIUMValid file + Ordering and/or Pricing
LOWValid file with only Version and Contact
NONEFile is invalid or missing

A HIGH score means the merchant has declared enough for an agent to take action — either Ordering + Pricing together, or a Commerce-Protocol endpoint (which subsumes both by providing a complete transactional flow including payment). Individual capability fields (Quote, Invoice, Tracking, etc.) provide additional detail but are not required for HIGH.

A HIGH+ score indicates the merchant has additionally provided key context. Interaction-Model now counts toward HIGH+ alongside Service-Region, Payment-Terms, Auth, and Min-Order.

A LOW score means the file is valid but provides minimal guidance.


Using the Validator API

You can use the procurement.txt validator API to check any domain:

GET https://procurementtxt.org/api/validate?domain=example.com

Response:

{
  "domain": "example.com",
  "url": "https://example.com/procurement.txt",
  "valid": true,
  "fields": {
    "version": "1",
    "contact": ["mailto:sales@example.com"],
    "ordering": "api https://api.example.com/openapi.json",
    "pricing": "public",
    "quote": "yes",
    "invoice": "api https://api.example.com/openapi.json"
  },
  "warnings": [],
  "errors": [],
  "agentReadiness": "HIGH",
  "fetchedAt": "2026-03-23T00:00:00Z"
}

Plain text format:

GET https://procurementtxt.org/api/validate?domain=example.com&format=text