Integration
How to send Streta a list and receive the rows. Generated from the code that runs; schema 2026-10.
- Send a list
- Send rows one at a time
- Your key
- What comes back
- The columns
- The webhook
- A recipe per tool
- What changes
Send a list
We enrich your list and never build one. What you send is used for your job and nothing else, and it isn't sold or shared. We find each business's own published line and ask it two or three plain questions. We never ring the person on your row.
The columns
A CSV with a header row. It needs a company_name column and, for each business, a city or domain (a website) so we can find its own line; a phone is optional and is kept as the contact's number, never rung. A state (two letters) on each row decides which state's rules apply to the call and the recording; a row with no state is closed as such rather than guessed.
Of everything else on your row, only ref (and the other keys of rows merged into it) and domain are returned; the rest is used for your job and kept for you. A phone on a row with a city or website is the contact's number, kept as storage; we ring the listing's line and the file says your_number_differed: yes.
Your own key for each row comes back on the row as ref: send it as row_id or ref, or as any column that starts ref_. If a file sends several, row_id wins, then ref, then the first ref_ column.
Headers we read as ours
Headers are read in any case and spelling ("Company Name" is company_name). These are read as the column on the left unless the file already has that column:
| We read | Your header may be |
|---|---|
company_name | account_name, business, business_name, company, name |
phone | main_phone, mobile, phone_number, tel, telephone |
domain | site, url, web, website, website_url |
state | st, state_code, state_province |
The reader takes a comma, semicolon, tab or pipe as the separator, UTF-8 or Windows-1252, and a short row (its last cells blank). A file with the wrong columns is refused with the headers we found, and the response lists the names we read yours as (mapped_columns). The most one upload carries is 20 MB.
Rows we don't ring
A mobile line, a business in a state closed to an artificial voice, a row with no state, and a business whose own line we can't find aren't rung. Each comes back as a row, with the reason in words and as a value (not_called_reason), so every business you send that we could name comes back as a row, and the ones we didn't call are yours to call. A row with nothing to name (a duplicate of another row, no company name, no city or website, a number on our stop list) is on the list of rows we could not take, below.
Look before it dials
Add dry_run=true to the same request. It resolves the list, counts what would happen (found, not found with the reason, mobile, closed state, no state, how many numbers we would ring from) and writes nothing and rings no one. Read the counts with us before the first job.
curl -X POST "https://streta.ai/jobs?mission_id=$MISSION_ID&dry_run=true" \
-H "X-API-Key: $STRETA_KEY" \
-F "file=@list.csv"
import requests
r = requests.post(
"https://streta.ai/jobs",
params={"mission_id": MISSION_ID, "dry_run": "true"}, # drop dry_run to create the job
headers={"X-API-Key": STRETA_KEY},
files={"file": open("list.csv", "rb")},
)
print(r.status_code, r.json())Send rows one at a time
A CRM that posts each new lead, a Clay column or a script can send rows into a mission that already exists, without a file. The mission is in the path; the rows are one object, a list of objects, or {"rows": [...]}, with the same column names a file takes (the header aliases above are read here too). Your own id for each row goes in ref.
curl -X POST "https://streta.ai/missions/$MISSION_ID/rows" \
-H "X-API-Key: $STRETA_CLIENT_KEY" -H "Idempotency-Key: lead-2041" -H "Content-Type: application/json" \
-d '{"company_name": "Heights Tire", "city": "Houston", "state": "TX", "ref": "crm-2041"}'
The answer is 202 and one entry per row, at once: {ref, streta_id, status}. streta_id is the id the row will carry in your file and in the webhook's sortie.id; ref is yours. The result of each row reaches your webhook per row, as it closes, and the file has it by wave. (A row that is the same business as another row you sent is merged into the first when the list is planned; the webhook's merged_refs says so.)
| status | Meaning |
|---|---|
queued | Taken. It is being prepared and booked; its result comes by your webhook. |
already_accepted | You sent this ref (or this Idempotency-Key) before; this is the id it has. Nothing was booked again. |
refused | Not taken, with a reason (for example no_company_name). It has no streta_id. |
Idempotency-Key is one per request (up to 200 characters): the same key again is answered with the rows it already made and books nothing. A ref this mission already has is answered already_accepted with the id it has, so a retry with a new key does not book a business twice. Send a ref on every row.
Rows go to the mission's job for the UTC day, made on the first row of the day. It is an ordinary job: the same pace, the same file, and job.finished when the day is over and its rows are closed. A client's first job is created held for a day so its counts are read before anybody is rung; the answer says held_until. The checks the file meets apply here (a number to ring from, the approved lines, the carrier). To look before you send, send a file with dry_run=true.
Your key
An account key is Streta's to give an agency; a client of its own has a key of its own. A person at your file makes it: /file, Settings, "A key for your tools" (Make a key). It is shown once and only its last four characters are kept; make one for each tool and revoke one without touching the rest. Send it in an X-API-Key header.
The key is one client's. It reads that client's rows and posts rows into that client's missions, and every other route answers 403:
| Route | What it is for |
|---|---|
GET /jobs | Your jobs, newest first. |
GET /jobs/{job_id} | One job: its counts, its planning, the rows we could not take. |
GET /jobs/{job_id}/not_accepted | The rows we could not make into a call, each with your ref and why. |
GET /jobs/{job_id}/progress | Where every row stands: queued, calling, retry_scheduled or finished, with your ref and the result. |
GET /jobs/{job_id}/results | The job's file as a CSV; closed_since for what is new; headers=keys for the machine header. |
GET /jobs/{job_id}/sorties | The same rows, optionally one status. |
GET /missions/{mission_id}/results | A mission's file across its jobs, by day or by closed_since. |
POST /missions/{mission_id}/rows | Send rows into one of your missions (above). |
GET /stops | Every business that asked us to stop, with the day and their words. |
To pull what is new, ask for closed_since with the instant of your last pull; the response header X-Streta-Next-Cursor is the instant to ask from next time, and a pull that finds nothing new returns the one it was asked from.
What comes back
Every business we complete comes back as one of three results, in result_key: matches (the answer you were looking for), no_match (a definitive answer that isn't), or no_answer (no definitive answer, with the reason). A business that asked us to stop comes back as exactly that; honor it in your own outreach. A call that failed on our side is retried and is never shown to you or charged.
Each column is one of two kinds. Observable is what happened or is on the record: whether a person answered, the answer, the quote. Inferred is our reading, kept in separate columns whose keys end _inferred, and is never stored as a fact. Weight the two as you like.
Rows we could not take, and rows we could not complete
Every row you send is one of: a row of the file (a result, or a business we named and did not call, with the reason), a row we could not take, or a row we could not complete. The first three counts of job.finished are the file's, and the other two make rows sent reconcile with rows back: counts carries matches, no_match, no_answer, not_accepted, not_completed.
Could not take are rows with nothing to make into a call. They are not results, not a count on your page and not billed. They are a list, GET /jobs/{id}/not_accepted (JSON, or format=csv), and on /file a quiet download on the job page, each with your ref, the company, one reason from this closed set and the reason in words:
| reason | In words |
|---|---|
no_listing | Google has no listing for this business, so we had no line of its own to call |
listing_not_confirmed | we found a listing but could not confirm it is this business (its name and place did not agree with your row) |
permanently_closed | Google lists this business as permanently closed |
lookup_failed | our lookup for this row did not complete; it says nothing about the business |
no_phone | there was no phone number to call: your row had none and the business's listing has none we can call |
duplicate_of | another row of yours is the same business and is the one we worked |
on_our_stop_list | this number is on our stop list, so we did not call it |
no_company_name | the row has no company name |
no_city_or_domain | the row has a phone number but no city or website to find the business's own line by |
on_your_do_not_contact_list | this business is on the do-not-contact list you sent with this list, so we did not call it; we keep that list for this job only and use it for no one else |
not_worked_from_a_list | we could not work this number from your list, so it was not called |
answered_before | we already asked this business for this mission and have its answer, which is on your earlier file under the id given; not asked again so soon (send reask=true to ask again) |
A business we could name and did not call (no listing, a closed one, no phone, on our stop list) is also a row of the file, no_answer, "Not called by us: …". Could not complete are rows our own call failed on: never shown as a result, never charged, counted as not_completed and said on your page as "N rows we could not complete; not charged".
Where a row stands
GET /jobs/{id}/progress lists every row with your ref and one of these statuses, and result once finished:
| status | Meaning |
|---|---|
queued | Nothing placed yet. A row you posted and we have not planned yet is here, under the id it was answered with. |
calling | A call is on the line now. |
retry_scheduled | We tried and the row is still open: another try is booked, or we wait on a callback or a reply. |
finished | Closed. result is matches, no_match or no_answer, or null for a row we could not complete. |
A list over 2,000 rows to look up is answered 409 with its estimate until the request carries confirm_spend=true. A second file, suppression (a CSV with a phone and/or domain column), is your own do-not-contact list: a row on it is not called, comes back no_answer with that reason, and is read for that job only. GET /stops is every business that asked us to stop, with the day and their words, for your sequencer. POST /rows/{streta_id}/outcome says what came of a row.
The columns
The same keys, in this order, on every row, whatever its result; a key that doesn't apply is blank. Keys never change and are only added, at the end. A question's sentence is published once, in questions and in the column's label, never in a key (q1_answer is question 1 whichever words it was asked in). The labels below are for a mission that asks the three example questions.
| Key | Kind | Label | Meaning |
|---|---|---|---|
ref | observable | ref | Your own key for the row, as you sent it (a row_id, ref or ref_<name> column). Blank when you sent none. |
merged_refs | observable | merged_refs | Your keys for other rows of your file that were the same business as this one, joined with ;. They were worked once, as this row. |
streta_id | observable | streta_id | Streta's id for this business on this job. Stable; use it to match a later update of the same row. |
job | observable | job | The name of the job (one wave of a mission over one list) the row belongs to. |
company | observable | company | The business as you named it. |
phone | observable | phone | The business's own published line that we rang or would have rung, not the contact's number on your row. |
city | observable | city | The city you sent, or the one the business's listing gave. |
domain | observable | domain | The website you sent for this row, as you sent it. Blank when you sent none; the listing's own website is in the context columns. |
result_key | observable | result_key | The result as a value to route on: matches, no_match or no_answer. |
result | observable | result | The result in words: Matches your objective, Does not match your objective, or No definitive answer. |
answered_on | observable | answered_on | The UTC day (YYYY-MM-DD) the answer arrived: the day of our settling call, or of the callback, text or email reply that settled it; for a business we never called, the day we filed the row; for an earlier answer given again (by prior), the day of that earlier answer. |
what_we_found | observable | what we found | One sentence of what happened, in plain words, for a person reading the row. |
answer | observable | answer | The answer to the qualifying question as it files (yes, no or one of the mission's choices). Blank when there is none. |
quote | observable | quote | The business's own words that carry the answer. |
asked_not_to_be_contacted | observable | asked_not_to_be_contacted | yes when they asked us not to contact them again. Honor it in your own outreach. |
instrument | observable | instrument | Who placed the call: ai or human. |
by | observable | by | How the answer arrived, a fact: call (our call's), callback (a call they made to us), text, email (a reply to our email), prior (an earlier answer of ours given again), research (the business's own published words, found before the call, when nobody picked up to confirm or contradict them; see research_words and not_tested), or blank for a business we never called. |
your_number_differed | observable | your_number_differed | yes when the number on your row was not the business's own line. We ring the listing's line, never a contact's cell. |
q1_answer | observable | 1. do y'all have a waiting area? | The answer to question 1 as it files (yes, no, a choice, or the quote for an open question). The question's sentence is in `questions` and in this column's label. |
q1_words | observable | 1. their words | The business's own words for question 1. |
q1_reading_inferred | inferred | (the question's sentence, once in `questions`) | Our reading of question 1, only where the mission asks for one. Inferred: a reading, not a fact. |
q2_answer | observable | 2. are there snacks for customers? | The answer to question 2 as it files (yes, no, a choice, or the quote for an open question). The question's sentence is in `questions` and in this column's label. |
q2_words | observable | 2. their words | The business's own words for question 2. |
q2_reading_inferred | inferred | 2. our reading (inferred) | Our reading of question 2, only where the mission asks for one. Inferred: a reading, not a fact. |
q3_answer | observable | 3. when are y'all least busy? | The answer to question 3 as it files (yes, no, a choice, or the quote for an open question). The question's sentence is in `questions` and in this column's label. |
q3_words | observable | 3. their words | The business's own words for question 3. |
q3_reading_inferred | inferred | (the question's sentence, once in `questions`) | Our reading of question 3, only where the mission asks for one. Inferred: a reading, not a fact. |
ended_before_qualifier | observable | ended before the qualifier | yes when the call ended before the qualifying question was asked. |
ended_early_words | observable | ended early: their words | Their words at the moment the call ended early. |
ended_early_reading_inferred | inferred | ended early: our reading (inferred) | Our reading of why it ended early. Inferred: a reading, not a fact. |
manner_reading_inferred | inferred | their manner: our reading (inferred) | Our reading of how they sounded. Inferred: a reading, not a fact. |
manner_heard | observable | their manner: what we heard | What we heard of their manner, as it happened (rushed, friendly), without a verdict. |
called_back | observable | called us back | yes when the business rang us back after the call. |
called_back_from | observable | called back from | The number the callback came from. |
called_back_from_line_kind | observable | called back from: line kind | What kind of line the callback came from (business line, mobile, withheld). |
called_back_words | observable | called back: their words | What they said when they called back. |
context_status | observable | context: status | Whether we read context about the business: found, partial or not read. |
context_listing | observable | context: their listing | What the business's own listing says (observable). |
context_website | observable | context: their website | What the business's own website says (observable). |
context_web | observable | context: elsewhere on the web | What the web says about the business elsewhere (observable). |
context_reviews | observable | context: customer reviews | What customer reviews say (observable). |
context_reading_inferred | inferred | context: our reading (inferred) | Our reading of the context. Inferred: a reading, not a fact. |
context_could_not_read | observable | context: could not read | What we could not read, and why. |
stop_requested_on | observable | stop requested on | The date they asked us to stop. |
stop_request_words | observable | stop request: their words | Their words when they asked us to stop. |
no_answer_reason | observable | no answer reason | For a No definitive answer row, the group of why. One of: answered_not_firm, wrong_person, declined, gated, machine, nobody, stop, questioned, language, nothing_usable, every_channel, not_called. |
not_called_reason | observable | not called reason | For a row we did not call, the code of why. One of: mobile_not_called, state_law_not_called, no_state_not_called, no_city_or_domain_not_called, business_not_found, answered_before, client_do_not_contact, skipped, capped. |
answered_before_streta_id | observable | answered before: streta_id | For a row we did not ask again because this mission already has the business's answer (by prior), the streta_id of the earlier row that answered. Blank otherwise. |
answered_by_said | observable | answered by: what they said | What the person who answered said about who they are or why they would know, in their own words. Blank when they said nothing of the kind. A fact: it is what they said, not whether it is true. |
would_know_inferred | inferred | would they know: our reading (inferred) | Our reading of whether the person who answered would know the answer: yes, no or unknown. Inferred: a reading, not a fact; it rests on what they said and how they spoke. |
research_words | observable | research: their words | The business's own published sentence that says yes to the qualifying question, word for word, found before any call. Blank when its own words did not say it (silence is not a no). A fact: it is what they published, not whether it is true. |
research_where | observable | research: where it was said | Whose words these are and where: their listing, or their website with the address of the page. |
research_call | observable | research: what the call found | What our call found beside the published words, a fact about the call: confirmed, contradicted, not reached, not called, did not settle it. Blank when there are no published words. |
not_tested | observable | not tested | On a row that stands on the business's published words alone (by research), what the phone did not test and why. Blank on every other row. |
called_back_caller_id_check | observable | called back from: caller ID check | Whether the carrier verified the number they called back from (STIR/SHAKEN), in plain words, or "not known". A stop from a number that failed it still stands. |
context_job_posts | observable | context: their job posts | What the business's own job posts say, with the board, the address, the date posted and the posting's hiring contact (observable). |
context_dns | observable | context: their DNS | The MX and SPF records the business's own domain publishes, quoted, with the mail provider and sending services they name (observable). |
Where a key takes a closed set of values, here they are:
| Key | Takes one of |
|---|---|
result_key | matches, no_match, no_answer |
by | call, callback, text, email, prior, research |
research_call | confirmed, contradicted, not reached, not called, did not settle it |
instrument | ai, human |
no_answer_reason | answered_not_firm, wrong_person, declined, gated, machine, nobody, stop, questioned, language, nothing_usable, every_channel, not_called |
not_called_reason | mobile_not_called, state_law_not_called, no_state_not_called, no_city_or_domain_not_called, business_not_found, answered_before, client_do_not_contact, skipped, capped |
Of what you sent, a row comes back with ref and merged_refs (your own keys) and domain (the website you sent). Nothing else on your row is returned: join on ref. A row we did not ask because this mission already has the business's answer is by prior, with the earlier answer and its date, and answered_before_streta_id names the earlier row. A row whose answer is the business's own published words, with nobody reached to confirm them, is by research: research_words is their sentence word for word, research_where says where it was said, and not_tested says what the phone could not test. When a call did reach someone, research_call says whether it confirmed or contradicted the published words, and the answer is the call's.
The webhook
Set the URL on your file's settings page; the signing secret is shown once. Each finished row is a POST of JSON. Answer with a 2xx at once and do your work after.
Events
| event | When |
|---|---|
row.finished | A business is finished. One row, with its result. |
row.updated | The same row again because something on it changed after it went out (the context we read about the business). Same sortie.id; replace the earlier row. |
row.withdrawn | A row we sent is no longer a result. Remove it. |
job.finished | A job is over: its file has completed. No row; job (id, name, finished_on) and counts (matches, no_match, no_answer, not_accepted, not_completed). Sent once per address, with the same sequence and version numbering. A workflow with a waiting-for-recon state leaves it on this. |
row.sample | The Send a sample row button: a whole row of sample text, "test": true, never billed and never in your file. |
Order and repeats
Deliveries are sent in order per row. sequence is your deliveries in order; version is this row's. Apply a delivery only if its version is greater than the one you hold for that sortie.id. A resend carries the same id, sequence and version.
Retries
If your receiver is down we try again after 1 minute, 5 minutes, 1 hour, 6 hours; a 4xx other than 429 is taken as your bug and isn't retried. A failed delivery is on your settings page and can be resent from there.
Signature
Each request carries Streta-Signature: t=<seconds>,v1=<hex>, where v1 is the HMAC-SHA256, with your secret, of "<t>." + the raw body. Check it over the body exactly as received, and refuse a timestamp more than a few minutes old.
import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = int(parts["t"]), parts["v1"]
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), str(t).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
# in your handler: verify(SECRET, request.body, request.headers["Streta-Signature"])
const crypto = require("crypto");
function verify(secret, rawBody, header, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split(/=(.*)/s).slice(0, 2)));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// rawBody is the request body exactly as received (a Buffer), not re-serialized JSON
# the header: Streta-Signature: t=<seconds>,v1=<hex>
printf '%s.%s' "$T" "$(cat body.json)" | openssl dgst -sha256 -hmac "$SECRET" -hex # compare to v1
A sample row
This is the whole payload of a row, as Send a sample row sends it to your receiver (built from your own mission once you have one).
{
"id": "5d9c2a40-1111-4222-8333-444455556666",
"event": "row.sample",
"test": true,
"schema": "2026-10",
"sent_at": "2026-10-20T15:04:05+00:00",
"client": {
"id": "c0ffee00-1111-4222-8333-444455556666",
"name": "Your company"
},
"sequence": null,
"version": null,
"mission": {
"id": "a1b2c3d4-1111-4222-8333-444455556666",
"name": "Sample mission",
"objective": "Do they have a snack machine for customers?"
},
"job": {
"id": "e5f6a7b8-1111-4222-8333-444455556666",
"name": "DFW tire shops"
},
"sortie": {
"id": "7c1f2a90-0000-4000-8000-000000000001"
},
"questions": [
{
"key": "q1",
"line": "do y'all have a waiting area?",
"qualifier": false
},
{
"key": "q2",
"line": "are there snacks for customers?",
"qualifier": true
},
{
"key": "q3",
"line": "when are y'all least busy?",
"qualifier": false
}
],
"columns": [
{
"key": "ref",
"label": "ref",
"kind": "observable"
},
{
"key": "merged_refs",
"label": "merged_refs",
"kind": "observable"
},
{
"key": "streta_id",
"label": "streta_id",
"kind": "observable"
},
{
"key": "job",
"label": "job",
"kind": "observable"
},
{
"key": "company",
"label": "company",
"kind": "observable"
},
{
"key": "phone",
"label": "phone",
"kind": "observable"
},
{
"key": "city",
"label": "city",
"kind": "observable"
},
{
"key": "domain",
"label": "domain",
"kind": "observable"
},
{
"key": "result_key",
"label": "result_key",
"kind": "observable"
},
{
"key": "result",
"label": "result",
"kind": "observable"
},
{
"key": "answered_on",
"label": "answered_on",
"kind": "observable"
},
{
"key": "what_we_found",
"label": "what we found",
"kind": "observable"
},
{
"key": "answer",
"label": "answer",
"kind": "observable"
},
{
"key": "quote",
"label": "quote",
"kind": "observable"
},
{
"key": "asked_not_to_be_contacted",
"label": "asked_not_to_be_contacted",
"kind": "observable"
},
{
"key": "instrument",
"label": "instrument",
"kind": "observable"
},
{
"key": "by",
"label": "by",
"kind": "observable"
},
{
"key": "your_number_differed",
"label": "your_number_differed",
"kind": "observable"
},
{
"key": "q1_answer",
"label": "1. do y'all have a waiting area?",
"kind": "observable"
},
{
"key": "q1_words",
"label": "1. their words",
"kind": "observable"
},
{
"key": "q1_reading_inferred",
"label": null,
"kind": "inferred"
},
{
"key": "q2_answer",
"label": "2. are there snacks for customers?",
"kind": "observable"
},
{
"key": "q2_words",
"label": "2. their words",
"kind": "observable"
},
{
"key": "q2_reading_inferred",
"label": "2. our reading (inferred)",
"kind": "inferred"
},
{
"key": "q3_answer",
"label": "3. when are y'all least busy?",
"kind": "observable"
},
{
"key": "q3_words",
"label": "3. their words",
"kind": "observable"
},
{
"key": "q3_reading_inferred",
"label": null,
"kind": "inferred"
},
{
"key": "ended_before_qualifier",
"label": "ended before the qualifier",
"kind": "observable"
},
{
"key": "ended_early_words",
"label": "ended early: their words",
"kind": "observable"
},
{
"key": "ended_early_reading_inferred",
"label": "ended early: our reading (inferred)",
"kind": "inferred"
},
{
"key": "manner_reading_inferred",
"label": "their manner: our reading (inferred)",
"kind": "inferred"
},
{
"key": "manner_heard",
"label": "their manner: what we heard",
"kind": "observable"
},
{
"key": "called_back",
"label": "called us back",
"kind": "observable"
},
{
"key": "called_back_from",
"label": "called back from",
"kind": "observable"
},
{
"key": "called_back_from_line_kind",
"label": "called back from: line kind",
"kind": "observable"
},
{
"key": "called_back_words",
"label": "called back: their words",
"kind": "observable"
},
{
"key": "context_status",
"label": "context: status",
"kind": "observable"
},
{
"key": "context_listing",
"label": "context: their listing",
"kind": "observable"
},
{
"key": "context_website",
"label": "context: their website",
"kind": "observable"
},
{
"key": "context_web",
"label": "context: elsewhere on the web",
"kind": "observable"
},
{
"key": "context_reviews",
"label": "context: customer reviews",
"kind": "observable"
},
{
"key": "context_reading_inferred",
"label": "context: our reading (inferred)",
"kind": "inferred"
},
{
"key": "context_could_not_read",
"label": "context: could not read",
"kind": "observable"
},
{
"key": "stop_requested_on",
"label": "stop requested on",
"kind": "observable"
},
{
"key": "stop_request_words",
"label": "stop request: their words",
"kind": "observable"
},
{
"key": "no_answer_reason",
"label": "no answer reason",
"kind": "observable"
},
{
"key": "not_called_reason",
"label": "not called reason",
"kind": "observable"
},
{
"key": "answered_before_streta_id",
"label": "answered before: streta_id",
"kind": "observable"
},
{
"key": "answered_by_said",
"label": "answered by: what they said",
"kind": "observable"
},
{
"key": "would_know_inferred",
"label": "would they know: our reading (inferred)",
"kind": "inferred"
},
{
"key": "research_words",
"label": "research: their words",
"kind": "observable"
},
{
"key": "research_where",
"label": "research: where it was said",
"kind": "observable"
},
{
"key": "research_call",
"label": "research: what the call found",
"kind": "observable"
},
{
"key": "not_tested",
"label": "not tested",
"kind": "observable"
},
{
"key": "called_back_caller_id_check",
"label": "called back from: caller ID check",
"kind": "observable"
},
{
"key": "context_job_posts",
"label": "context: their job posts",
"kind": "observable"
},
{
"key": "context_dns",
"label": "context: their DNS",
"kind": "observable"
}
],
"row": {
"ref": "crm-1042",
"merged_refs": "crm-1077;crm-1090",
"streta_id": "7c1f2a90-0000-4000-8000-000000000001",
"job": "DFW tire shops",
"company": "Cowtown Tire",
"phone": "+18175550142",
"city": "Fort Worth",
"domain": "cowtowntire.example",
"result_key": "matches",
"result": "Matches your objective",
"answered_on": "2026-10-20",
"what_we_found": "Spoke with someone at the shop: no snack machine.",
"answer": "no",
"quote": "No, we just have a coffee pot.",
"asked_not_to_be_contacted": "",
"instrument": "ai",
"by": "call",
"your_number_differed": "yes",
"q1_answer": "yes",
"q1_words": "Sample: the business's words for q1.",
"q1_reading_inferred": "Sample: our reading for q1. Inferred.",
"q2_answer": "no",
"q2_words": "Sample: the business's words for q2.",
"q2_reading_inferred": "Sample: our reading for q2. Inferred.",
"q3_answer": "no",
"q3_words": "Sample: the business's words for q3.",
"q3_reading_inferred": "Sample: our reading for q3. Inferred.",
"ended_before_qualifier": "no",
"ended_early_words": "Sample: words when a call ended early.",
"ended_early_reading_inferred": "Sample: our reading of why it ended early.",
"manner_reading_inferred": "Sample: our reading, relaxed and helpful.",
"manner_heard": "Sample: unhurried, answered at once.",
"called_back": "no",
"called_back_from": "+18175550199",
"called_back_from_line_kind": "business line",
"called_back_words": "Sample: returning your call, we don't have one.",
"context_status": "found",
"context_listing": "Sample: independent tire shop, open 8 to 6, waiting area.",
"context_website": "Sample: family owned since 1987.",
"context_web": "Sample: mentioned in a local list of tire shops.",
"context_reviews": "Sample: customers mention a clean waiting room.",
"context_reading_inferred": "Sample: our reading of the context.",
"context_could_not_read": "",
"stop_requested_on": "",
"stop_request_words": "",
"no_answer_reason": "",
"not_called_reason": "",
"answered_before_streta_id": "Sample: our reading for an. Inferred.",
"answered_by_said": "Sample: our reading for an. Inferred.",
"would_know_inferred": "Sample: our reading for wo. Inferred.",
"research_words": "Sample: the business's words for re.",
"research_where": "Sample: our reading for re. Inferred.",
"research_call": "Sample: our reading for re. Inferred.",
"not_tested": "Sample: our reading for no. Inferred.",
"called_back_caller_id_check": "Sample: our reading for ca. Inferred.",
"context_job_posts": "Sample: our reading for co. Inferred.",
"context_dns": "Sample: our reading for co. Inferred."
},
"inferred_columns": [
"q1_reading_inferred",
"q2_reading_inferred",
"q3_reading_inferred",
"ended_early_reading_inferred",
"manner_reading_inferred",
"context_reading_inferred",
"would_know_inferred"
],
"message": "This is a sample row from Streta. It isn't a business, it isn't billed and it isn't in your file."
}A recipe per tool
Clay
Export the table as a CSV and send it as above, or send it from a script on a schedule. To receive rows, add a table with a Webhook source in Clay and put its URL in your Streta file settings: each row.finished arrives as one record and the fields are under row. Clay can't check the signature, so keep the URL secret.
Zapier
Trigger: Webhooks by Zapier, Catch Hook; paste its URL into your Streta file settings and press Send a sample row to teach Zapier the fields. Zapier can't verify the signature; keep the URL secret. Use row.result_key to filter (matches, no_match, no_answer) and row.ref to find your own record.
Make
Scenario with a Custom webhook module; paste its address into your file settings and press Send a sample row so Make learns the structure. Filter on row.result_key; route row.updated and row.withdrawn events to update or delete the record that carries the same sortie.id.
n8n
A Webhook node (POST, respond immediately with 200). To verify the signature, add a Code node that runs the Node.js function above over the raw body (set the Webhook node's Raw Body option).
A script or your own server
Receive POST requests, verify the signature with the function above, answer 2xx at once, and do your work after you have answered. Key your records on sortie.id and apply a delivery only if its version is greater than the one you hold.
What changes
The schema is 2026-10. A key is never renamed or removed and a new one is added at the end of row, so a receiver that reads the keys it knows keeps working. A change that cannot be made that way gets a new schema value, announced here before it is sent.
Questions: hello@streta.ai.