Screening Return Requests Against Your Return Policy
Put a policy check in front of your returns queue: compute eligibility from real order data, auto-approve the clear cases, and route exceptions to people. Code in curl, TypeScript and Python.
It's Monday morning and the returns queue has 300 requests in it. Most are boring: a sweater that didn't fit, returned on day six of a thirty-day window. A few are not. One customer wants a refund on headphones bought 45 days ago because the left speaker died. Your policy says 30 days, but it also says defective products may qualify for review. A tired agent on request 212 either approves it on autopilot or rejects it on autopilot. Neither is the decision you wrote the policy for.
This guide shows how to screen return requests before a person sees them. The clear cases move straight through, the impossible ones get a polite and accurate answer, and the interesting ones reach someone with the context already gathered. The screening step uses the Kovca Returns Intelligence API. As with every guide here, most of the work is in the steps around that call.
Decide what the screen is allowed to do
Write this down before you write code. A good screen does three things:
- Understands the request. Is this a defect, damage in transit, the wrong item, or a change of mind? Did the customer ask for a replacement instead of a refund?
- Applies your policy to your facts. The return window is arithmetic. It should never be a guess.
- Suggests a next step. It suggests and never executes. Issuing a refund or a label is your system's job.
Kovca follows the same split. Eligibility is computed from the numbers you send. Kovca never assumes a policy, never invents warranty terms, and returns not_evaluated if you send no policy. Reading the customer's message is the only part that needs interpretation.
The pipeline
- 1Returns portal formcustomer message, order number
- 2Order lookupdelivery date → whole days since delivery
- 3Policywindow_days and exceptions for this product category
- 4Kovca Returns Intelligencereason, eligibility, suggested_actionReturns teamrequires_human_review: true
- 5Portal responselabel, troubleshooting steps, or a request for details
- 6Order systemstore request_id and the suggested action
1Gather the facts
The request body is small, so assemble it from systems you already have:
customer_message: the customer's own words, up to 4,000 characters. Leave out your form labels and any auto-filled text.order.days_since_delivery: compute this in your code from the carrier's delivery scan, not the order date. Use whole days.return_policy.window_daysandreturn_policy.exceptions: up to 10 exceptions in plain language, exactly as your policy states them. If electronics get 15 days and apparel gets 60, send the window for this product's category.product.nameandproduct.category: optional, but they help with reasons like "missing parts".
If the carrier never confirmed delivery, leave days_since_delivery out rather than guessing. When the customer says the item never arrived, Kovca returns delivery_not_confirmed and sends the request to review. It doesn't apply a window it can't measure.
2Screen the request
curl
curl https://kovca.io/api/v1/returns/triage \
-H "Authorization: Bearer $KOVCA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_message": "I bought these headphones 45 days ago and the left speaker stopped working. I'\''d like a refund.",
"product": { "name": "Wireless Headphones", "category": "electronics" },
"order": { "days_since_delivery": 45 },
"return_policy": {
"window_days": 30,
"exceptions": ["Defective products may qualify for additional review"]
}
}'The response for that Monday-morning headphone request:
{
"request_id": "req_3nWq8ZtLs2KfP0aV",
"reason": "defective",
"priority": "medium",
"suggested_action": "manual_review",
"requires_human_review": true,
"eligibility": { "status": "requires_review" },
"signals": ["outside_return_window", "policy_exception_may_apply", "reported_defect"],
"missing_information": []
}The request is outside the window, so a naive rule would reject it. Your own exception might cover it, so it goes to a person, and signals tells that person why.
TypeScript (Node 18+)
type ReturnScreen = {
request_id: string;
reason: "defective" | "damaged" | "wrong_item" | "not_as_described" | "changed_mind" | "missing_parts" | "delivery_issue" | "other";
priority: "low" | "medium" | "high";
suggested_action: "approve_for_processing" | "request_information" | "troubleshoot" | "offer_replacement" | "manual_review" | "reject_request";
requires_human_review: boolean;
eligibility: { status: "eligible" | "ineligible" | "requires_review" | "insufficient_information" | "not_evaluated" };
signals: string[];
missing_information: ("days_since_delivery" | "return_reason")[];
};
type ReturnCase = {
message: string;
deliveredAt?: Date;
product: { name: string; category: string };
policy: { windowDays: number; exceptions: string[] };
};
export async function screenReturn(c: ReturnCase): Promise<ReturnScreen | null> {
const body = {
customer_message: c.message.trim().slice(0, 4000),
product: c.product,
order: c.deliveredAt ? { days_since_delivery: Math.floor((Date.now() - c.deliveredAt.getTime()) / 86_400_000) } : undefined,
return_policy: { window_days: c.policy.windowDays, exceptions: c.policy.exceptions.slice(0, 10) },
};
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch("https://kovca.io/api/v1/returns/triage", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.KOVCA_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (res.ok) return res.json();
const code = (await res.json().catch(() => ({})))?.error?.code;
if (![500, 503, 504].includes(res.status) && code !== "RATE_LIMITED") break;
await new Promise((r) => setTimeout(r, (Number(res.headers.get("Retry-After")) || 2 ** attempt) * 1000));
}
return null; // fall back to the manual queue
}Python
import os, time, requests
from datetime import datetime, timezone
def screen_return(message, delivered_at, product, window_days, exceptions):
body = {
"customer_message": message.strip()[:4000],
"product": product,
"return_policy": {"window_days": window_days, "exceptions": exceptions[:10]},
}
if delivered_at:
body["order"] = {"days_since_delivery": (datetime.now(timezone.utc) - delivered_at).days}
for attempt in range(4):
r = requests.post(
"https://kovca.io/api/v1/returns/triage",
headers={"Authorization": f"Bearer {os.environ['KOVCA_API_KEY']}"},
json=body, timeout=30,
)
if r.ok:
return r.json()
code = r.json().get("error", {}).get("code")
if r.status_code in (500, 503, 504) or code == "RATE_LIMITED":
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
continue
break
return None3Turn the suggestion into a portal response
Kovca checks the actions in a fixed order and returns the first that matches, so each one maps cleanly onto something your portal can do:
| suggested_action | When | What the portal does |
|---|---|---|
approve_for_processing | Inside your window, nothing unusual | Issue the return label. Your refund rules still apply when the item arrives. |
troubleshoot | A fault that a reset or setup often fixes | Show the product's troubleshooting steps, with a "still broken" button that reopens the request. |
offer_replacement | The customer asked for one on a faulty, damaged, wrong, or incomplete item | Offer the exchange flow first. |
request_information | Reason unclear, or the window needs days_since_delivery | Ask for what's in missing_information, then screen again. |
reject_request | Outside the window and no exception applies | Explain the policy clearly and keep a "talk to a person" link. |
manual_review | Conflicting details, possible exception, or no policy | Queue for the returns team with signals attached. |
Two rules keep this safe. Only act automatically when requires_human_review is false. It's true for every manual_review, every high-priority request, and whenever urgency is unclear. And never treat signals as accusations. conflicting_information means the message disagrees with the order data. It doesn't mean fraud, so the reviewer should start with the order, not the customer.
4Close the loop on missing information
When the reason is unclear, missing_information contains return_reason, the action is request_information, and the reason field is only a closest guess, so don't use it. Ask one specific follow-up question in the portal ("What's wrong with the item?") and screen the request again with the answer appended to the message. Customers answer a single question much more often than they fill in a second form.
Rolling it out
- Shadow week. Screen every request and log the suggestion beside what your agents actually did. Look closely at the disagreements on
reject_request. - Automate the boring half. Turn on
approve_for_processingandtroubleshoot. They are the highest-volume actions and the lowest-risk. - Then the polite no. Enable automatic
reject_requestresponses only after you've read a week of them. Keep the escape hatch to a person.
Volume and cost
One request per return request, plus one more if you ask a follow-up. The Free plan covers 1,000 successful requests a month at 10 per minute. Pay As You Go is $0.005 per successful request at 60 per minute, with a spending cap you set (pricing). Customer messages aren't stored by Kovca.
Try it on your own returns
Take ten requests your team argued about last month and run them through with your real policy text. Sign up free (no card required), create a key, and paste one into the playground. The full contract, including every signal, is in the Returns Intelligence docs.