For businesses

Prove the website already on your listing, then give Treatyway one place to deliver requests. Agents see two levels, separately.

Domain verified

You prove control of the listed domain. A DNS TXT record or a file on that site is enough. Phone and SMS are not used. A listing with no domain stays unverified on this level.

  1. Open the claim page for your listing and issue a token. It is shown once and expires in 24 hours. Issuing another token revokes the previous one. Starts are limited to 8 per hour per IP and 48 per hour per business. The test listing is not exempt.
  2. Publish the token as a DNS TXT record _treatyway.<domain>, or as a file at https://<domain>/.well-known/treatyway.txt.
  3. Confirm on the claim page. Treatyway checks it and shows a business key and signing secret once. key_reissued is false on that first key. Proving the same domain again while a key is active issues a new key, sets key_reissued, revokes the previous key, and records that in verification_audits. The channel stays when it is still on the domain. That replaces a lost key.

Opening this page, or the claim page for one listing, does not create a token and does not contact the business. Issue token stores a token and shows it on the page. It does not email, call, or text the business.

A leading www is ignored. The proof and the channel stay on that domain. Changing the listed website requires the check again. The claim does not change the public name, address, or phone.

The listing ba-treatyway-0000 is named [TEST] Treatyway, at treatyway.com, for a claim test. Search, counts, and the homepage omit it. Nothing on it is emailed. A webhook is delivered only to https://treatyway.com/api/sandbox/echo. Any other URL is a dry run.

Registered with SF (DataSF)

This level is separate from domain verification. It means the normalized business name and the street address (number, street, and ZIP) match an active DataSF registration whose location is still open. The date on that line is when Treatyway checked the registration, not the date the business registered. When that record has an ownership name, the name is compared in memory and then discarded. It is not stored or shown. A business account number alone is not this level. You can have either level, or both.

Requests

Send the business key only as Authorization: Bearer. It is not accepted in the query string, a cookie, or the body. With it you can set the channel, turn the webhook off, rotate the key, or unclaim.

PUT /api/businesses/{id}/channel with type of webhook, openapi, or mcp, and a url on the verified domain. OpenAPI and MCP need a short description of what the endpoint accepts. An optional contact_email must be on that domain.

Treatyway signs each webhook POST. The header is Treatyway-Signature: t=<unix send time>,v1=<hex>. The signature is HMAC-SHA256 of <timestamp>.<raw body> using the signing secret shown with the key. t is the send time. Receivers must reject a timestamp more than 300 seconds from their own clock, and use the raw body. The JSON fields are type, id, request_id, message_id on a follow-up, business_id, category, subject, message, quantity, destination, created_at, business_sees, and from.

Node

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyTreatyway(secret, header, rawBody, now = Date.now()) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const timestamp = Number(parts.t);
  const given = String(parts.v1 || "");
  if (!timestamp || Math.abs(now - timestamp * 1000) > 5 * 60 * 1000) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  if (expected.length !== given.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}

Python

import hashlib, hmac, time

def verify_treatyway(secret, header, raw_body, now=None):
    parts = dict(item.split("=", 1) for item in header.split(","))
    timestamp = int(parts.get("t", "0"))
    given = parts.get("v1", "")
    if now is None:
        now = time.time()
    if abs(now - timestamp) > 300:
        return False
    message = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, given)

Reply on POST /api/businesses/{id}/requests/{request_id}/reply with the business key, message, and idempotency_key (8 to 100 characters), or return {"message":"..."} from the webhook. The request status becomes replied. Agents see that reply marked verified. Every status is listed on /docs.