Conversions and adjustments
How a call converts on its own, how to mark or post a conversion, how to tag a call after the fact, and how to correct its books without losing the record of why.
After this page you can say why any call did or did not convert, change that when your buyer tells you something the phone system could not know, and correct a figure in a way that survives everything the platform does afterwards. A call's money is settled when the call ends, and most of the time that needs nothing from you.
None of this moves any money. A tracking call's revenue and payout are books-only — the record of what your buyer owes you and what you owe your publisher, both settled off the platform. Correcting them changes your reports and your statements, and no wallet anywhere.
How a call converts on its own
When a call ends, the platform writes its verdict from values frozen on the call when it was routed — never from what the target says now. A buyer repriced at three o'clock has not repriced the call that started at 2:58.
| Side | Converts when | The terms come from |
|---|---|---|
| Revenue — what the buyer owes you | A buyer answered and talked for at least the frozen duration threshold. Or a conversion was posted for the call. | The target that took the call, or the routing plan's override for it, frozen on the dial attempt. |
| Payout — what you owe the publisher | The publisher's own terms are met: their own buffer if their lane sets one, otherwise the buyer's threshold. | The publisher's lane, frozen when the call started. |
The two sides are different deals, and they can disagree. A lane that pays on 60 seconds owes nothing for a 45-second call even though the buyer's 30-second threshold converted it — and owes its fixed payout for a 70-second call the buyer's 90-second threshold did not. That is the whole business, working.
The books say reason | Meaning |
|---|---|
duration | Talk time met the frozen threshold. The usual conversion. |
postback | The buyer reported it. |
manual | Somebody said so by hand. |
not_answered | No buyer ever answered. |
no_buyer | The call reached no target at all. An unrouted call that the fallback number answered lands here, however long the conversation was. |
under_threshold:41s_of_90s | A buyer answered and hung up short. The two figures are the talk time and the threshold it had to beat. |
call.completed and call.converted — see Events — and in the settled step of the call's waterfall.Three things you can do afterwards
| Conversion | Adjustment | Tags | |
|---|---|---|---|
| Answers | Did this call earn its price? | What were the figures, exactly? | What do we know about it now? |
| Changes | converted, and the revenue with it. | revenueCents, payoutCents, or both. | The call's tags. |
| Needs | A call that has ended. | A call that has ended. | Nothing — any call. |
| Written to the activity log | Yes, medium severity. | Yes, medium severity. | Yes, low severity. |
| Fires an event | call.converted, when it newly converts. | call.converted, when it newly converts. | None. |
Conversions
Some buyers only pay for a call that became a sale, and only they know which did. A conversion records that against the call, and it can arrive three ways.
| Who | How | Notes |
|---|---|---|
| You, in the console | The call drawer's Mark this call converted. | Shows Booked now, Payout now and Talk time against the threshold before you decide. |
| Your system | POST /api/v1/calls/:id/conversion, with a key that has calls:write. | Takes the same fields as the dialog. |
| The buyer | The per-call postback URL and key they were handed with the call. | Within 72 hours of the call ending, and once — see below. |
curl -X POST "https://api.buy3.io/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11/conversion" \
-H "Authorization: Bearer $BUY3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"converted": true,
"revenueCents": 5000,
"note": "Buyer confirmed the transfer by email"
}'{
"call": {
"ref": "CA-482137",
"converted": true,
"revenueCents": 5000,
"payoutCents": 2750,
"profitCents": 2250,
"convertedAt": "2026-09-20T16:22:04.118Z",
"conversionSource": "manual",
"conversionNote": "Buyer confirmed the transfer by email",
"frozen": {
"revenueCents": 4200,
"payoutCents": 0,
"durationThresholdSeconds": 90
},
"corrections": { "converted": true, "revenueCents": 5000 },
"adjustments": []
},
"before": { "converted": false, "revenueCents": 0, "payoutCents": 0, "convertedAt": null },
"after": { "converted": true, "revenueCents": 5000, "payoutCents": 2750, "convertedAt": "2026-09-20T16:22:04.118Z" }
}convertedis required, andfalseis how a converted call stops being one. The engine never revokes on its own; only a person, saying so, on an audited route.revenueCentsis optional. Leave it out and the call books the price frozen on it; send it to override that for this call only.converted: falsewith a revenue above zero is refused — a call that did not convert has no revenue.noteis kept on the call and repeated in the activity log. Write the reason, not the action: "Buyer confirmed the transfer by email" outlives "marked converted".- The payout is recomputed from the publisher's frozen terms against the new revenue, so a revenue-share lane follows the correction without a second request.
beforeandaftercome back in the answer, which is what you log on your side.
When the buyer posts it themselves
| Rule | Detail |
|---|---|
| Window | 72 hours after the call ends. Later answers 410 window_closed. |
| Once | A second postback answers 409 already_converted with the time of the first. A correction goes through the console or the API instead of silently overwriting it. |
| A negative postback | A buyer saying the call did not convert records their disposition and leaves the conversion stamp alone. "It did not sell" must not be the write that makes a call converted. |
| A wrong key | 404, not 403. A 403 would confirm the call id is real, which turns the endpoint into an oracle for anyone with a list of guesses. |
{{postback.url}} and {{postback.key}} in Macros. See Ping and post for the protocol.Adjustments
An adjustment corrects a figure. Use it when a buyer short-pays and you agree, when a publisher is owed a make-good, or when a rate was wrong on the day. It changes the record and nothing else.
# Correct one side and leave the other alone
curl -X POST "https://api.buy3.io/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11/adjust" \
-H "Authorization: Bearer $BUY3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "revenueCents": 3500, "reason": "Acme short-paid — agreed make-good" }'
# And later, hand that side back to the frozen terms
curl -X POST "https://api.buy3.io/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11/adjust" \
-H "Authorization: Bearer $BUY3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "revenueCents": null, "reason": "Correction withdrawn — buyer paid in full" }'{
"adjustment": {
"at": "2026-09-21T10:04:55.310Z",
"by": { "kind": "api_key", "id": "7f6e5d4c-3b2a-4190-8f7e-6d5c4b3a2190", "name": "API key b3_live_Zk3q" },
"reason": "Acme short-paid — agreed make-good",
"before": { "revenueCents": 4200, "payoutCents": 2750 },
"after": { "revenueCents": 3500, "payoutCents": 2750 }
}
}| You send | It means |
|---|---|
revenueCents: 3500 | Set that side to 35.00 and keep it there. |
revenueCents: null | Clear the correction. That side goes back to whatever the frozen terms compute — the buyer's price if the call converted, nothing if it did not. |
| Nothing for that side | Leave it alone, correction and all. |
reasonis required, between 3 and 500 characters. An adjustment with no reason is a figure nobody can defend three months later.- Send
revenueCents,payoutCentsor both. Sending neither, and clearing neither, is refused. - A call may be adjusted 50 times. Each is appended to the call's history with who made it, when, why, and the figures before and after — visible under Adjustments in the call drawer.
- A figure above $1,000,000 on one call is refused. That is a typo, not a correction.
A correction sticks
- The engine only ever converts forwards. Left to itself it moves a call from not-converted to converted and never back. Only an explicit
converted: falsetakes it back, and because these are books that is a correction rather than a clawback. nulland "not sent" are deliberately different. Not sent leaves the kept figure alone, which is what every caller that is not correcting it wants;nullremoves it.- A cleared side is recorded as cleared in the history, so it reads "cleared" rather than showing a figure nobody typed.
- The current corrections on a call are
correctionson the call detail, and the console shows a note on each side of the adjust dialog when one is standing.
What moves with a correction
- Your reports change. A report covering that date is computed from the calls as they stand now, so it follows the correction. Export what you invoice from — see Exports.
- Cap counters move by the difference, and only when the call settled today in UTC. A counter's window has rolled by tomorrow, and subtracting yesterday's money from today's count would be a second error rather than a fix.
call.convertedfires when the call newly converts, so a conversion pixel reports it. It does not fire again for a call that was already converted, and un-converting fires nothing at all.- The waterfall gains a
settledstep recording the recompute, with the previous figures beside the new ones.
Tags after the fact
You can add tags to a finished call — a disposition from your CRM, a quality score, the agent who took it. They are available to filters, reports and exports straight away.
curl -X POST "https://api.buy3.io/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11/tags" \
-H "Authorization: Bearer $BUY3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tags": {
"crm_disposition": "policy_written",
"crm_owner": "dallas-3",
"qa_flagged": null
}
}'{
"id": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"tags": {
"gclid": "Cj0KCQjw",
"utm_source": "google",
"crm_disposition": "policy_written",
"crm_owner": "dallas-3"
},
"tagSources": {
"gclid": "dni",
"utm_source": "dni",
"crm_disposition": "api",
"crm_owner": "api"
}
}- A tag set this way is stamped
apiwhen a key sets it anduserwhen a person does — the console shows By hand. It can never pass for something the publisher sent or the router observed, which is what makes tag provenance worth anything. - Tags are merged, not replaced. A key with a
nullvalue removes that tag; keys you do not mention are untouched. - 40 keys per request, 80 on a call in total. Keys are up to 64 letters, digits,
.,-and_; values are up to 512 characters and must be a string, a number or a boolean. - Everything is stored as a string, so a filter comparing
"35"with35is a bug that cannot happen. - Tagging works on a call at any time, including one still in progress — it changes no money and needs no settled books.
Worked example: a disputed call, settled
BlueSky Legal says call CA-482137 was a wrong number and should not have been billed. You agree. The call converted on duration for the frozen price, and your publisher has already been credited a revenue share of it.
- Open the call and read the Money section. Booked revenue, booked payout, and the frozen terms beside them — a call that paid nothing on a priced target is explained by those two being different.
- Listen to the recording if there is one. A wrong number is audible in ten seconds, and agreeing without listening is how a buyer learns to dispute everything.
- Mark this call not converted, with the note "Buyer disputed — wrong number, agreed to reverse". Revenue goes to zero.
- Decide the publisher's side separately. If their lane earns on their own buffer, they may be owed regardless of what the buyer pays — that is your deal with them, not the buyer's. If you have agreed a make-good, Adjust the payout with the reason.
- Check the day's report. If the call settled today, the buyer's and the target's counters have already moved by the difference; if it settled last week, they have not, and that is deliberate.
- Tell the publisher, and use a tag —
crm_disposition: wrong_number— so the same question next month is a filter rather than a conversation.
Everything you did is in the activity log with your name on it, the values before and after, and the reasons you typed. That is what makes the next dispute short.
