How Doozer sends physical mail via Thanks.io and feeds the results back into intelligence — claim postcards, radius audience buying that reverse-engineers new sites, and delivery/scan webhooks.
Thanks.io is a direct-mail API (postcards, letters, notecards) with radius audience buying and QR/delivery webhooks. Doozer uses it both ways so every send and every response compounds into the intelligence layer. Grounded in the real API: base https://api.thanks.io/api/v2, auth Authorization: Bearer <key>. Engine: includes/thanksio.php. Config-free, self-creating tables, portable.
thanksIoMailClaim($businessId)mints a claim code + doozer.link QR (claim-codes) and sends a postcard to the business address carrying that QR and a warm message. Receiving it proves the address; scanning/entering claims the listing. Doozer+ "pay first, snail mail, claim" verification uses the same path (plus_verify).
thanksIoBuyRadiusSearch(address, postal, count, {record_types, append_data}) buys 1–10,000 real addresses around a point ($0.05/record, +$0.20 with phone/email append), filtered by record_types: all, onlybusinesses, newbusiness, newhomeowner, renters, absenteeowner, likelytomove, highnetworth, retired, and more. thanksIoIngestRadiusTargets(listId) pulls the records into thanksio_targets; thanksIoBuildFromTargets() resolves each business record to a Google Place (placesSearchBest) and materializes a Doozer site (resolvePlaceToEcosystem). The mailing list becomes site inventory — which then earns its own claim postcard. Full circle.
?e=thanksio_webhook (gated by a sharedsecret in the URL — Thanks.io sends no signature) ingests order.status_update / order_item.delivered / scans.scan_update into thanksio_events, mirrors status onto the matching thanksio_sends row, and pings Comms: delivered = the address is reachable (the proof-of-address precondition), scan = the owner engaged before claiming (a demand signal for the Demand Ledger).
thanksio_sends — every send (preview or live): business, claim_code, order id,recipients, authorization_total, status, is_preview, qr url, address snapshot.
thanksio_events — webhook events (unique on event_id), resolved to a business.thanksio_audiences — radius searches bought: center, record_types, count, cost.thanksio_targets — records pulled from a radius audience, staged for building(resolve_status new/resolving/built/existing/skipped/failed + business_id/place_id).
Spend is real and irreversible, so: every send DEFAULTS TO PREVIEW (Thanks.io returns an estimate, no charge) unless live=1; radius buys return a cost estimate unless confirm=1; build-from-targets is bounded per call. Operator- triggered only. Cost logged on every row ([[feedback_api_costs]]).
Cost-units bug FIXED (v2.2.409): Thanks.io returns authorization_total / leads_fee ALREADY in cents (114 = $1.14 for a 4x6, 1 recipient); the send path (thanksIoSendPostcard) was doing round(value * 100) → stored 100× too high (11400 for a $1.14 card). Dropped the *100; dz postcard do=fixcosts re-derives historical thanksio_sends rows from raw meta.response.authorization_total. Previews (proof images) live at response.data.previews = [front, handwriting back] S3 URLs; persisted to postcard_designs.proof_urls for the admin lightbox.
Delivery/scan/claim per send are read LIVE by postcardDesignLiveStats() ([[ai-postcards]]) from thanksio_sends + thanksio_events + shortlinks + claim_codes (never cached JSON). The webhook now also recomputes the linked postcard_designs.tracking on each delivered/scan event, so the admin "Sent & performance" view stays fresh without a manual Refresh.
mailclaim | sends | events`.
thanksio,or platform_settings thanksio.api_key); (2) sets the return address (thanksio.return_name/address/city/state/postal_code); (3) sets a claim postcard front (thanksio.claim_image_template_id designed in Thanks.io, or thanksio.claim_front_image_url); (4) runs dz thanksio do=webhook to register the receiver. Then preview a mailclaim, and add live=1 when happy.
The front is the Factory image; the BACK is where direct-mail response is won. thanksIoClaimMessageVariants($bizName,$code,$opts) returns warm / leads / curious variants, personalized with the town, no em-dashes. The message never spells out the claim code or the doozer.link URL (v2.2.405): cursive handwriting makes codes/URLs hard to read, and a code is already a friction point, so the back POINTS to them instead: "Scan the QR code, or use the claim code printed on the front of this card." Code + URL live only where they're legible: the front. Greeting frames the business as the OBJECT, not a salutation: "Hi, we built Powell Flooring a real website..." (warm "you / yours"), not "Hi Powell Flooring, we built you...". thanksIoDefaultClaimMessage picks one (opts.variant). Operator levers (all platform_settings, flowed through thanksIoMailClaim): thanksio.handwriting_style_id (a handwritten back lifts response), thanksio.size (4x6 default; 6x9/6x11 stand out but need a matching front aspect), thanksio.message_variant. Multi-campaign: postcardCampaignMessage resolves the back copy per campaign (claim → these variants; other campaigns → their message_template with tokens).
Related: claim codes, shortlinks + QR, AI Postcards (campaigns + Factory + Theme Studio), the observed world (mail closes the CLAIM loop; radius feeds OBSERVE), Prospector / place-resolver.