ProductsDocsGuidesPricing
Sign inGet Started

Intelligence APIs for developers.

Product

  • All APIs
  • Support Triage
  • Pricing

Developers

  • Getting started
  • Guides
  • API reference
  • Errors
  • Dashboard

Company

  • About
  • Security
  • Contact

Legal

  • Privacy Policy
  • Terms of Service
© Kovca. kovca.io
GuidesCommerce

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.

Returns Intelligence7 min readcURL · TypeScript · Python
approvetroubleshootmanual_review

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:

  1. 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?
  2. Applies your policy to your facts. The return window is arithmetic. It should never be a guess.
  3. 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 most common mistake is sending only the customer's message. Without days_since_delivery and return_policy.window_days, nothing can be checked, so everything ends up in manual review. Pull those facts from your order system first.

The pipeline

  1. 1
    Returns portal form
    customer message, order number
  2. 2
    Order lookup
    delivery date → whole days since delivery
  3. 3
    Policy
    window_days and exceptions for this product category
  4. 4
    Kovca Returns Intelligence
    reason, eligibility, suggested_action
    Returns teamrequires_human_review: true
  5. 5
    Portal response
    label, troubleshooting steps, or a request for details
  6. 6
    Order system
    store 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_days and return_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.name and product.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
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:

JSON
{
  "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+)

TypeScript
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

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 None

3Turn 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_actionWhenWhat the portal does
approve_for_processingInside your window, nothing unusualIssue the return label. Your refund rules still apply when the item arrives.
troubleshootA fault that a reset or setup often fixesShow the product's troubleshooting steps, with a "still broken" button that reopens the request.
offer_replacementThe customer asked for one on a faulty, damaged, wrong, or incomplete itemOffer the exchange flow first.
request_informationReason unclear, or the window needs days_since_deliveryAsk for what's in missing_information, then screen again.
reject_requestOutside the window and no exception appliesExplain the policy clearly and keep a "talk to a person" link.
manual_reviewConflicting details, possible exception, or no policyQueue 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

  1. Shadow week. Screen every request and log the suggestion beside what your agents actually did. Look closely at the disagreements on reject_request.
  2. Automate the boring half. Turn on approve_for_processing and troubleshoot. They are the highest-volume actions and the lowest-risk.
  3. Then the polite no. Enable automatic reject_request responses 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.

On this page

  • Decide what the screen is allowed to do
  • The pipeline
  • Rolling it out
  • Volume and cost
  • Try it on your own returns

Returns Intelligence

Classify return requests, check them against your return policy, and get a suggested next step.

Try it in the playground

Keep going

Returns IntelligenceAPI reference
same_variantdifferent_variantuncertain
Catalog Matching9 min read

How to Match Supplier Product Catalogs to Your Own SKUs

A practical guide to matching supplier feeds to your SKUs: blocking candidates, using GTINs safely, handling variants, and routing uncertain matches. Code in curl, TypeScript and Python.

Read guide
billingtechnicalneeds_review
Support Triage7 min read

Automating Support Ticket Triage in Your Helpdesk Pipeline

Build a support ticket triage pipeline: webhook intake, category and priority classification, human-review routing, and safe fallbacks. Code in curl, TypeScript and Python.

Read guide
monitorcontact_carrierinvestigate
Shipment Intelligence6 min read

Catching Delivery Exceptions Before Your Customers Do

Turn raw carrier tracking events into routed delivery exceptions: late and stalled parcels, address problems, losses and disputes. A scheduled pipeline with curl, TypeScript and Python.

Read guide