TPV Order API
Submit verification orders directly to the Automatdo TPV platform and receive results back in real time — no CRM sync required.
How the integration works
Orders are pushed to us before the verification call, not fetched during it. Once an order is in our queue, the customer is either dialed by our verification agent or looked up automatically by phone number when they call in. When the call ends, the result comes back to you.
POST per closed sale, at the moment it needs TPV.Because order data is pre-loaded, calls are fast (no live lookups against your systems), verification keeps working even if your systems are briefly down, and there is a complete audit trail of exactly what data each verification used.
Authentication
We issue you an API key at onboarding. Keys look like sk_live_…, are shown once at creation, and are scoped to your organization — every order you submit and every result you read is automatically isolated to your account.
Send the key on every request using either header:
Authorization: Bearer sk_live_your_key_here
# — or —
X-API-Key: sk_live_your_key_here
| Scope | Grants |
|---|---|
| tpv:write | Submitting orders |
| results:read | Pulling verification results (section 08) |
- All requests must use HTTPS.
- Keys are rate limited to 60 requests per minute by default — contact us if you need more.
- Keys can be revoked and rotated at any time; store yours in a secrets manager, never in client-side code.
Submit an order
Accepts a flat JSON object. Four fields are required; address fields are strongly recommended (the verification script confirms the service address with the customer). Any additional fields you include are stored with the order and available to the verification script — this is how job-specific details like square footage and pricing reach the call.
Standard fields
| Field | Notes | |
|---|---|---|
| external_order_id | Required | Your unique ID for this sale. Used for deduplication, versioning, and matching results back to your records. |
| customer_first_name | Required | |
| customer_last_name | Required | |
| customer_phone | Required | The number we call. E.164 preferred (+16125551234); common US formats are normalized automatically. Invalid numbers are rejected. |
| customer_email | Optional | |
| supplier_name | Optional | Business name read in the script, e.g. "Acme Home Services". |
| service_address_line1 | Recommended | Address of the job site. State should be a two-letter code (uppercased automatically) — it also determines which state’s compliance rules apply to the call. |
| service_address_line2 | Optional | |
| service_address_city | Recommended | |
| service_address_state | Recommended | |
| service_address_zip | Recommended |
Custom fields
Everything else in the payload is passed through to the verification call, so the script can confirm the specifics of your product or service with the customer. For example, a home-services order might include:
| Field | Example | Used for |
|---|---|---|
| product_name | "Radiant Barrier Package" | The product or package being verified |
| install_sqft | "1500" | Installation square footage confirmed on the call |
| unit_count | "2" | Number of units included |
| total_price | "4,995.00" | Total contract price confirmed with the customer |
New custom fields don’t require any change on our side to be stored — but if a new field should be spoken or confirmed during the call, tell us so we can update the script to reference it.
Example request
curl -X POST https://app.automatdo.com/voice/api/tpv/orders/ \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "external_order_id": "ORD-2026-08451", "supplier_name": "Acme Home Services", "customer_first_name": "Ryan", "customer_last_name": "Smith", "customer_phone": "+16125551234", "customer_email": "ryan.smith@example.com", "service_address_line1": "123 Main Street", "service_address_city": "Minneapolis", "service_address_state": "MN", "service_address_zip": "55401", "product_name": "Radiant Barrier Package", "install_sqft": "1500", "unit_count": "2", "total_price": "4,995.00" }'
Response — 201 Created
{
"order_id": "3f8b2c1a-9d4e-4f6a-b7c8-1e2d3f4a5b6c",
"external_order_id": "ORD-2026-08451",
"status": "pending",
"created_at": "2026-08-26T14:32:07.123456+00:00"
}
order_id. Results reference both our order_id and your external_order_id, so either works for matching — but the UUID is guaranteed unique across resubmissions.Resubmission & re-verification
Deduplication is keyed on your external_order_id:
- The order is still active (pending or in progress) → the request is rejected with
400and the current status, so you can’t accidentally queue the same sale twice. - The order already finished (completed, failed, expired, or cancelled) → a new version of the order is created and queued for verification. Use this to re-verify after a failed call or a contract change.
Response when a new version is created — 201 Created
{
"order_id": "7a1c9e2b-…", // the new version’s ID
"external_order_id": "ORD-2026-08451",
"status": "pending",
"version": 2,
"previous_order_id": "3f8b2c1a-…",
"created_at": "2026-08-27T09:02:11.000000+00:00"
}
Errors
| Status | Meaning | Body |
|---|---|---|
| 400 | Validation failed — missing required fields, invalid phone number, malformed JSON, or a duplicate active order. | {"error": "Validation failed.", "details": [ … ]} |
| 401 | Missing or invalid API key. | {"error": "Invalid API key."} |
| 403 | Key is valid but lacks the required scope. | {"error": "API key lacks tpv:write scope."} |
| 500 | Unexpected server error — safe to retry with backoff. | {"error": "Failed to create order: …"} |
Example 400 body
{
"error": "Validation failed.",
"details": [
"customer_phone must be a valid phone number.",
"Active order with external_order_id 'ORD-2026-08451' already exists (status: pending)."
]
}
Results webhook
When a verification call finishes, we POST the result to an HTTPS endpoint you provide. This is the primary way results reach you.
| Header | Value |
|---|---|
| X-Watson-Event | tpv.verification.completed |
| X-Watson-Timestamp | Unix timestamp used in the signature |
| X-Watson-Signature | HMAC-SHA256 hex digest (section 07) |
| Content-Type | application/json |
Respond with any 2xx within a few seconds to acknowledge. Failed deliveries are retried automatically up to 5 times with exponential backoff, so brief outages on your side don’t lose results.
Example payload
{
"event": "tpv.verification.completed",
"timestamp": "2026-08-26T15:04:22.518000+00:00",
"data": {
"result_id": "b4d1f8c2-…",
"confirmation_number": "TPV8X2K9M3",
"order_id": "3f8b2c1a-…",
"external_order_id": "ORD-2026-08451",
"outcome": "pass",
"failure_reason": "",
"failure_question_id": "",
"completed_at": "2026-08-26T15:04:20+00:00",
"customer": {
"first_name": "Ryan", "last_name": "Smith",
"phone": "+16125551234", "email": "ryan.smith@example.com"
},
"service_address": {
"line1": "123 Main Street", "line2": "",
"city": "Minneapolis", "state": "MN", "zip": "55401"
},
"answers": [
{ "question_id": "Q1", "normalized": "yes" },
{ "question_id": "Q2", "verbatim": "Ryan Michael Smith" }
// … one entry per script question
],
"compliance": {
"status": "pass", "summary": "…", "flags": [],
"policy_id": "…", "policy_version": 3, "checked_at": "…", "error": null
},
"recording": { "url": "https://…", "duration_seconds": 312 },
"call": { "call_id": 48213, "call_sid": "CA…", "direction": "outbound", "duration_seconds": 318 },
"contract": { /* energy-market fields — null unless applicable to your vertical */ }
}
}
Outcomes
| Outcome | Meaning |
|---|---|
| pass | Customer verified the sale. confirmation_number is your proof of verification. |
| fail | Customer declined or answered a required question negatively — see failure_reason and failure_question_id. |
| incomplete | Call ended before the script finished. |
| no_answer | Customer didn’t pick up. |
| wrong_number | The person reached wasn’t the customer. |
| callback_requested | Customer asked to be called at another time. |
| do_not_call | Customer asked not to be contacted again. |
| language_barrier | Verification couldn’t proceed in the customer’s language. |
incomplete and no_answer. If you only want final pass/fail results delivered, we can configure that.Verifying webhook signatures
Every webhook is signed with a shared secret we give you at onboarding. The signature is an HMAC-SHA256 hex digest of the string {timestamp}.{raw_body} — the timestamp from the X-Watson-Timestamp header, a literal dot, then the raw request body exactly as received.
Python example
import hmac, hashlib def verify_watson_webhook(request_body: bytes, headers: dict, secret: str) -> bool: timestamp = headers["X-Watson-Timestamp"] provided = headers["X-Watson-Signature"] payload = f"{timestamp}.".encode() + request_body expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, provided)
- Compute the HMAC over the raw bytes of the body — don’t re-serialize the JSON first.
- Use a constant-time comparison (as above) and reject requests with missing or invalid signatures.
- Optionally reject timestamps older than a few minutes to guard against replay.
Pulling results on demand
For reconciliation, backfills, or as a fallback if your webhook endpoint was down, results can also be pulled. Requires the results:read scope.
| Query param | Notes |
|---|---|
| since | ISO 8601 datetime — only results created after this time |
| outcome | Filter to one outcome, e.g. pass |
| delivered | true/false — filter by whether the webhook was delivered |
| limit | Default 50, max 200 |
| offset | Pagination offset |
curl "https://app.automatdo.com/integrations/api/v1/tpv/verifications?since=2026-08-26T00:00:00Z&outcome=pass" \ -H "Authorization: Bearer sk_live_your_key_here"
The response contains total, limit, offset, and a results array. A single result can be fetched at GET /integrations/api/v1/tpv/verifications/<result_id>.
Order lifecycle
Every order moves through a small set of states:
- pending — queued and waiting for the verification call.
- in_progress — a call is underway (or between retry attempts).
- completed / failed — the verification finished; the result webhook carries the outcome.
- expired — the order aged out before verification completed. Orders don’t expire unless an expiration window is configured for your account.
- cancelled — cancelled by us at your request (e.g., the sale fell through).
Once an order reaches a dashed (terminal) state, resubmitting the same external_order_id starts a fresh verification as a new version (section 04).
Go-live checklist
We provide
- API key with
tpv:write+results:readscopes - Webhook signing secret
- A test window to submit sandbox orders and receive live webhooks
We need from you
- Your HTTPS webhook endpoint URL
- Confirmation of the custom field list your script should reference
- Whether you want webhooks for non-final outcomes (
no_answer,incomplete)
Suggested test plan
- Submit a test order to a known phone number and complete a pass
- Verify the webhook signature check on your side
- Exercise a resubmission after a terminal outcome
- Confirm duplicate-active submissions return
400