Gumroad ConvertKit Integration: What Exists, and How to Move Buyers to Kit
Search "Gumroad ConvertKit integration" and the first page is almost entirely automation vendors telling you they can connect the two. None of them start with the fact that decides your whole approach: Gumroad ships no native email-marketing integration at all, and Kit is not a special case of that — nothing is.
That changes the question from "where's the toggle?" to "which of four mechanisms am I building?" This guide answers that first, then covers the path the automation vendors never document — moving your existing buyer history into Kit in bulk, field by field, without importing rows you shouldn't.
The starting fact: Gumroad has four integrations, and Kit isn't one
Gumroad is open source, so this is checkable rather than arguable. The list of product integrations lives in one constant:
CIRCLE = "circle"
DISCORD = "discord"
ZOOM = "zoom"
GOOGLE_CALENDAR = "google_calendar"
ALL_NAMES = [CIRCLE, DISCORD, ZOOM, GOOGLE_CALENDAR]
That's app/models/integration.rb, and it's the complete set — the four community and scheduling services Gumroad can auto-add a customer to after purchase. The help center is narrower still: Gumroad's article on integrating products with external services walks through Circle and Discord only, and doesn't document the other two at all. There is no Mailchimp, no ActiveCampaign, no Kit, no ConvertKit.
Gumroad's own help center is consistent about this. The article that mentions ConvertKit by name files it under Zapier, alongside Mailchimp and AWeber, as something Zapier does rather than something Gumroad does (common Zapier integrations).
The reverse direction is closed too. The help article covering your Audience page has an "Importing" section that consists of one sentence: "Unfortunately, we don't support the importing of customers or subscribers" (Starting an email newsletter). What Gumroad does expose is raw material:
- Gumroad Ping — an HTTP POST to a URL you set in account settings, fired on a sale. Per Gumroad's developer page, "The payload is x-www-form-urlencoded. If your endpoint does not return a 200 HTTP status code, the POST is retried once an hour for up to 3 hours" (Gumroad Ping).
- Resource subscriptions — the richer webhook layer, for registered OAuth applications. Eight event names exist:
sale,cancellation,subscription_ended,subscription_restarted,subscription_updated,refund,dispute,dispute_won(resource_subscription.rb). - The REST API —
GET /v2/sales, what any bulk move reads from.
One detail in that file shows how Zapier-shaped the ecosystem is: assign_content_type_to_json_for_zapier switches the outgoing content type to JSON when the post URL host ends with zapier.com, and leaves everything else form-encoded.
Four routes, and only one of them is a "sync"
| Native Gumroad integration | Automation SaaS (Zapier, n8n, Make) | Manual CSV export → Kit import | GumKit CSV export → Kit import | |
|---|---|---|---|---|
| Exists for Kit? | No — four integrations, none email | Yes | Yes | Yes |
| Trigger | n/a | New sale (Zapier lists Sale, Refund, Cancellation, New Product as triggers) | You, manually | You, manually |
| Latency | n/a | Near real-time | Whenever you run it | Whenever you run it |
| Backfills existing buyers | n/a | No — fires forward only | Yes | Yes |
| Handles refunds after the fact | n/a | Yes, if you build the refund branch |
Only if you filter before importing | Only if you filter before importing |
| Ongoing dependency | n/a | A third-party account, plus its task pricing | None | None |
| Where your Gumroad token sits | n/a | On the vendor's servers (or OAuth grant) | Not needed for the dashboard export | chrome.storage.local, no backend |
| What arrives in Kit | n/a | One subscriber per event, as it happens | A whole file, deduplicated on arrival | A whole file, deduplicated on arrival |
The row that matters is "backfills existing buyers." Automation tools fire forward and never backwards. Connect Zapier today and your 900 existing buyers stay invisible to Kit forever; only sale number 901 lands. A bulk import does the opposite: it moves everyone you already have and then stops caring.
So the honest recommendation is not "pick one":
- A buyer tagged in Kit within seconds of checkout needs a webhook. Gumroad Ping into your own endpoint, or an automation platform — n8n, for instance, ships a Gumroad Trigger node and needs either an access token or OAuth (n8n Gumroad credentials). Zapier's Gumroad page likewise lists triggers, so Gumroad is the source side of the Zap and Kit is the action side. Nothing here replaces that.
- The buyers you already have, tagged by product, is a bulk export and import. A job you do twice a year, not a system you run.
Most sellers need both, in that order. The rest of this guide is the bulk half, which nobody documents — Gumroad doesn't publish it and Kit's import guide has never heard of Gumroad.
What Gumroad actually gives you per sale
GET /v2/sales returns one object per purchase. The mechanics of pulling that file — pagination, date boundaries, the byte-order mark — are in export Gumroad customers to CSV, and the token in the Gumroad API access token guide. What matters here is the shape of what lands.
Two structural facts govern everything below.
Null keys are deleted before the JSON is built. The serializer ends with .delete_if { |_, v| v.nil? } (purchase.rb). A sale object has no fixed shape; a membership renewal carries keys a one-off download never will.
Gumroad's published field list is incomplete. The API documentation page enumerates 93 field rows for a sale — 77 top-level, plus 16 nested inside buyer_presentment, card, affiliate, and offer_code (responseFieldDefinitions.ts). The serializer emits several more that never made it into that list:
| Field emitted but not in Gumroad's published field list | Value | Why it matters for a Kit import |
|---|---|---|
full_name |
full_name.try(:strip).presence || purchaser&.name |
The only name you get. Absent entirely for guest checkouts with no name entered |
country |
Buyer country, falling back to IP geolocation | Segmentation, and the reason your VAT rows look odd |
country_iso2 |
Two-letter code | The version you actually want in a Kit custom field |
city, street_address |
Shipping address parts | Do not import these into an email tool |
refunded |
stripe_refunded |
The single most important exclusion field — see below |
giftee_email, gifter_email |
Gift purchase pair | The buyer and the recipient are different people |
Undocumented fields carry no compatibility promise, so verify against your own account before building on them. But they are in the serializer today, and full_name in particular has no substitute.
The refunded trap
refunded maps to the stripe_refunded column, and in db/schema.rb that column is declared t.boolean "stripe_refunded" — with no default. Its partial-refund counterpart, declared some forty columns further down the same table, is t.boolean "stripe_partially_refunded", default: false.
That difference matters once you combine it with delete_if. stripe_refunded is written at charge time — save_charge_data sets it from the processor's own refunded flag, which is false on a fresh charge — so on an ordinary paid sale the key is present, as false. But nothing writes it on a purchase that never reached a processor charge — free ($0) purchases, free-trial rows sitting in not_charged, and rows old enough to predate the column. Those keep NULL, and delete_if strips the key from the JSON entirely. partially_refunded, having a default, is present as false on every row.
Gumroad's own code assumes exactly this. Its paid scope reads where("stripe_refunded is null OR stripe_refunded = 0") — the platform treats "missing" and "false" as the same state, because both occur.
Two consequences for your filter. Test truthiness (if (sale.refunded)), never sale.refunded === false, which silently skips every row where the key was dropped. And in a CSV built from the union of keys across rows, the refunded column is blank on those rows — or absent from the header altogether if your account happens to contain none of them and you've never issued a refund. Check that the column exists before you filter on it.
There is no top-level ISO currency code
At the top level the sale object carries currency_symbol ("$") and amount_refundable_in_currency (a string), and that's it — there is no top-level three-letter currency field. The documented ISO code sits on the product object, and on the sale it appears only as buyer_presentment.currency, inside an object Gumroad's own docs say is "omitted when the buyer was charged in Gumroad's canonical currency (most sales)." Kit's purchases endpoint requires a currency on every record, so for the common case you supply it yourself. Canonical price stays USD cents whatever the buyer paid in.
The field mapping table
Kit's subscriber record is deliberately thin. Per Kit's help center, profiles "contain only first name and email address fields by default"; everything else is a custom field, capped at 140 per account (custom fields). Keys are derived from the label — Kit's API docs describe the key as an "ASCII-only, lowercased, underscored representation of your label," so a label of Interests produces key: "interests" and name: "ck_field_6_interests" (create a custom field). The CSV importer doesn't match on header text — step 4 has you map each column to a Kit field by hand, and "columns that are not mapped to fields will not be imported" — but naming your headers with the key saves you guessing at that screen, and it's the identifier the API wants if you later script this.
| Gumroad sale field | Shape | Kit import target | Notes |
|---|---|---|---|
purchase_email |
string | Email address (required) | Use this, not email. email prefers the buyer's Gumroad account address; purchase_email is always the checkout address |
full_name |
string, often absent | First name | Kit has no last-name field. A full name lands whole in {{ subscriber.first_name }}, so "Hi Maria Sanchez," is what your welcome email says. Split it before importing or accept it |
product_name |
string | Tags column | Product titles change when you rename; the tag stays as imported |
product_permalink |
string, e.g. XCBbJ |
custom field | The stable identifier. Worth carrying alongside the tag |
created_at |
ISO 8601, UTC | custom field | Kit has no native purchase-date field on a subscriber |
price |
integer cents | custom field, or subtotal on a Kit purchase |
Divide by 100. 1000 is $10.00 |
gumroad_fee |
integer cents | not imported | Your cost, not the customer's. See what Gumroad's fees really cost |
tax_cents |
integer cents | not imported | Never your revenue — why |
variants |
object, e.g. {"Tier": "Premium"} |
Tags column | Flatten to Tier: Premium or emit one tag per variant |
custom_fields |
object | custom fields | Header must be the Kit key, one column each |
country_iso2 |
string | custom field | Two letters beats the full country name |
subscription_id |
string, membership sales only | custom field | Its presence is how you tell a membership from a one-off |
recurring_charge |
boolean, memberships only | do not import | true marks a renewal row, not a new customer |
can_contact |
boolean | filter, not a field | See the consent section |
refunded / partially_refunded / chargedback |
boolean; refunded can be absent on uncharged rows |
filter, not a field | Drop the row rather than storing the flag |
card, zip_code, street_address, city |
mixed | do not import | Payment and address data has no business in an email tool |
Kit's importer accepts a Tags column with comma-separated tag names inside a single cell, and creates tags that don't exist yet (how to import a subscriber list). The file must literally be .csv — Kit's pre-import troubleshooting is explicit that "Your subscriber import's filetype must be .csv" and that .xlsx won't parse (pre-import troubleshooting).
What Kit does with duplicates, and what it quietly overwrites
Your export is one row per purchase. A buyer who bought three products is three rows; a monthly subscriber is one row per charge. Kit's post-import guide is precise about what happens to that:
- Duplicates inside one file are collapsed: "if there are still some are duplicates within the CSV, those duplicates will also be counted and imported only once."
- Duplicates against your existing list are skipped: "If there's a subscriber contained in your import file that's already on your list, we won't add them to your list again."
- Unsubscribes are never resurrected: "If someone previously unsubscribed from your list and then are subsequently included in an import, we will not re-activate them on your list." (post-import troubleshooting)
That's the good news: you don't have to deduplicate before uploading. The bad news is the other half, and it comes from a different page — Kit's import walkthrough:
"when we import your data into Kit, we will replace any existing data we have on your subscribers with the data you provide in your import file"
Including blanks. The same page spells it out: "this includes replacing existing subscriber data for certain fields with blank values if your import file contains blank values for those fields." If your CSV has a first_name column that's empty for 200 rows because those buyers checked out without a name, those 200 subscribers lose the first names Kit already had. The documented workaround is to split the import, one file with the column and one without. In practice, delete any column that isn't fully populated with data you want to win.
Tags are the exception and behave additively: "any existing Tags your subscribers have will not be replaced. If your CSV contains new Tags to be added to subscribers, these new Tags will be added to the subscribers during the import." (All three quotes: how to import a subscriber list.)
That's what makes per-product tagging easy. Rather than one wide file with a comma-joined tag list per buyer, run one import per product — split the export by product_id, set the Tags column to that product's tag, upload. Buyers who own three products appear in three imports, are skipped as duplicates each time, and accumulate three tags. Slower to click, far harder to get wrong.
Splitting is a spreadsheet filter on the product_id column, or a second pass through the API, which accepts product_id as a query parameter on /v2/sales. Don't expect GumKit to do it for you — its CSV tab takes a From and a To date and nothing else.
One warning from the same import guide, and it's the mistake that generates support tickets: "Adding subscribers to Tags, Forms, or Email Sequences may activate Visual Automations or Rules that have these Tags or Forms as triggers." A 900-row import into a tag that starts a welcome sequence sends 900 welcome emails at once. Kit's own instruction is to "pause or turn them off first."
The rows to exclude before you upload
Nothing in either export removes refunds, chargebacks, or dead subscriptions. Both the API sale scope and the dashboard export filter on purchase state only. You do the filtering.
| Situation | Field test on the sale object | Why exclude it |
|---|---|---|
| Full refund | refunded present and truthy |
They asked for their money back. Tagging them as an owner is wrong and invites a spam complaint |
| Partial refund | partially_refunded === true |
Judgment call — usually keep, since they still own the product |
| Chargeback | chargedback === true or disputed === true |
Adversarial relationship; do not email |
| Chargeback you won | dispute_won === true |
Reinstate if you want, but flag it |
| Subscription renewal row | recurring_charge === true |
A duplicate person, not a new one. Kit collapses these anyway, but they distort your row counts |
| Cancelled membership | cancelled === true or ended === true |
Should not get "thanks for subscribing" content |
| Buyer opted out | can_contact === false |
See below. This one is not optional |
| Gift recipient | is_gift_receiver_purchase === true |
The payer and the recipient are different people; gifter_email is the one who transacted |
A worked example, because the arithmetic surprises people. Say a two-year-old account selling one $9/month membership and four one-off products exports 1,200 rows:
- 1,200 rows total
- minus 310 renewal rows (
recurring_charge === true) → 890 - minus 34 refunds → 856
- minus 6 chargebacks → 850
- minus 11 rows where
can_contact === false→ 839 - collapse to unique
purchase_email→ around 610 people
You uploaded 1,200 lines and gained roughly 610 subscribers. That gap isn't a failure — it's Kit's three deduplication rules plus your own filters, working correctly.
Consent: the field that is not what it looks like
can_contact is the field everyone assumes means "this buyer opted in." It doesn't, and the source settles it.
The column was created with t.boolean :can_contact, default: true, and the migration then backfilled every existing purchase to true (the migration). The only thing that flips it to false is an unsubscribe: unsubscribe_buyer sets can_contact: false across every purchase sharing that email and seller, and a before_create hook named toggle_off_can_contact_if_buyer_has_unsubscribed propagates that to future purchases by the same person.
So can_contact: true means "has not unsubscribed yet." It's an opt-out flag wearing an opt-in name, and every buyer who has never interacted with your emails carries it.
That matters because Kit's Acceptable Use Policy sets the bar you actually have to clear:
"We require that all Subscribers have given direct permission to receive marketing emails from the sender, or have purchased an item from the sender within the past 12 months and consented to receive emails at the time of purchase." (Acceptable Use Policy)
Two conditions, both load-bearing: within 12 months, and consented at the time of purchase. The same policy carries the enforcement mechanism — "Elevated bounce, complaint or unsubscribe rates may result in manual review, suspension, or termination of the account" — and Kit restates the permission test in plainer words in its guidance on adding contacts: subscribers must have "given explicit permission to receive marketing emails from you," or have "consented to receive emails when they made a purchase from you within the last twelve months" (adding personal contacts).
A four-year-old Gumroad export does not clear that bar just because can_contact is true. Three practical consequences:
- Drop every row where
can_contact === false. These people unsubscribed from you already, on a different platform. - Slice by
created_at. Recent buyers into your main flow; older cohorts get a re-permission email rather than a sequence. - Every marketing email needs a working unsubscribe. Under 15 U.S.C. § 7704 the opt-out mechanism must remain "capable of receiving such messages or communications for no less than 30 days after the transmission of the original message," and a sender may not keep mailing "more than 10 business days after the receipt of such request" (15 U.S.C. § 7704). Kit handles the mechanism; the 10-day clock covers whatever else you do with the same list.
Note too that Gumroad's unsubscribe_buyer also calls Follower.unsubscribe on the same email. A buyer who opted out is gone from your customer emails and your follower list at once — treat that as one deliberate signal.
Where those subscribers land, and what they cost
Kit's Subscribers page filters by seven status labels, and only Confirmed — plus the Cold subset inside it — reaches your invoice.
| Kit UI status | Meaning | Counts toward your bill |
|---|---|---|
| Confirmed | Confirmed via the double-opt-in email, or auto-confirmed at signup when double opt-in is off. "These are also the only subscribers you can email via Kit" | Yes |
| Unconfirmed | Signed up through a double-opt-in form, never clicked confirm | No |
| Unsubscribed | Unsubscribed from your list | No |
| Bounced | Hard bounce; "we automatically unsubscribe bounced subscribers from your list." Soft bounces do not produce this status | No |
| Complained | Reported one of your emails as spam | No |
| Blocked | You blocked them | No |
| Cold | Not a separate status — "a subset of your confirmed subscribers" with no open or click in 90 days, and not shown on the profile sidebar | Yes |
That's the subscriber profile page and status, which is also where the billing rule lives: "Only subscribers with the 'Confirmed' status count toward your billing total," and "cold subscribers are still confirmed subscribers (and therefore they do count toward your billing total)."
The API is a separate vocabulary and Kit does not publish a mapping between the two. Create a subscriber accepts state values active, cancelled, bounced, complained, and inactive, defaulting to active, and defines none of them. Read across from a UI label to an API value at your own risk — in particular, don't assume inactive is how Kit represents "Cold."
The billing line is the one to sit with. Cold subscribers cost the same as engaged ones, so importing 400 buyers from 2023 who never open anything is a recurring bill for the privilege of not emailing them.
If you'd rather script it than click it
Kit's API v4 is cleaner than the CSV importer for anything repeatable.
POST /v4/bulk/subscribers takes a subscribers array of { first_name, email_address, state }. It "Creates or updates the subscribers synchronously when 100 or less subscribers are requested" and "asynchronously when more than 100 subscribers are requested," in which case the response is a 202 with an empty body — pass the optional callback_url in your request if you need to know when it finished. The synchronous response carries both a subscribers array and a failures array, each failure pairing the offending record with its errors (bulk create subscribers).
POST /v4/purchases is the more interesting one, because it records the transaction rather than just the person. Purchases are matched on transaction_id, so re-submitting one updates rather than duplicates — feed it Gumroad's sale id and your import becomes idempotent. Per the docs, "If no subscriber with that email address exists, one is created (in the active state) as part of the request" (create a purchase). One caveat from the same page: the products you send are added as line items rather than replacing what's recorded, so a retry loop that re-posts the whole payload stacks duplicates on the same purchase.
Rate limits are published and worth planning against: "no more than 600 requests over a rolling 60 second period for given access token" on OAuth, and "no more than 120 requests over a rolling 60 second period for a given API Key," with a 429 past either (response codes).
Run the numbers on that 1,200-row account:
- Reading from Gumroad:
/v2/salesreturns 10 rows per request, so 1,200 sales is 120 requests. At GumKit's 350 ms pacing that's about 42 seconds of deliberate sleeping, plus network time. - Writing subscribers to Kit: 610 unique people at 100 per synchronous bulk call is 7 requests. Trivial under either limit.
- Writing purchases to Kit: one call per transaction. The 839 rows left after filtering, on an API key at 120/minute, is a floor of about 7 minutes; on OAuth at 600/minute, under 90 seconds.
Where GumKit fits, and where it doesn't
GumKit is an independent Chrome extension for Gumroad sellers — not affiliated with or endorsed by Gumroad — and it's worth being blunt about which half of this article it addresses.
It does not sync anything. No webhook, no Ping endpoint, no background polling, no Kit connection of any kind. If you need a buyer in Kit within seconds of checkout, GumKit is the wrong tool and an automation platform is the right one.
What it does is the export half: walk GET /v2/sales to the end with your own token and hand you a file, which is the input to a Kit import and nothing more. The full mechanics — where the token lives, the exclusive before boundary, the byte-order mark, the 5,000-sale ceiling, and what a mid-run 429 does to your file — are written up once in export Gumroad customers to CSV and not repeated here.
Two of those details change what you have to do before Kit sees the file:
- Columns are the union of keys across rows. A field absent from every row never becomes a column, which is the
refundedbehaviour described above showing up in your spreadsheet. - Nested objects are JSON-stringified into a single cell, so
variantsandcustom_fieldsarrive as{"Tier":"Premium"}and need flattening into their own columns before the Kit importer can map them.
Everything runs through Gumroad's official API v2 with a token you generate, and it's free. It can't make Gumroad talk to Kit, and it doesn't claim to.
FAQ
Does Gumroad have a ConvertKit integration?
Not natively. Gumroad's source lists exactly four product integrations — Circle, Discord, Zoom, and Google Calendar — and no email platform is among them. Gumroad's own help center files ConvertKit under Zapier integrations, meaning a third party bridges the two. There is no first-party toggle to find.
What's the fastest way to get Gumroad buyers into Kit?
For buyers you already have: export sales to CSV, filter out refunds and opt-outs, then use Kit's Add Subscribers → Import CSV with a Tags column. For buyers arriving from now on: a webhook, either Gumroad Ping to your own endpoint or an automation platform's Gumroad trigger. Different problems, and most sellers eventually do both.
Will importing create duplicate subscribers in Kit?
No. Kit deduplicates within the file and against your existing list, and it will not reactivate anyone who previously unsubscribed. That's also why the imported count lands well below your row count — every renewal row and repeat buyer collapses into one person.
How do I tag Gumroad buyers by which product they bought?
Run one import per product. Split the export by product_id — a spreadsheet filter, or a product_id query parameter if you're calling /v2/sales yourself — set the Tags column to that product's tag, and upload. Because Kit adds tags rather than replacing them, a buyer who owns three products accumulates three tags across three imports without you building a joined tag string.
Can I email everyone in my Gumroad export?
Only those who meet Kit's permission bar: direct permission, or a purchase within the past twelve months with consent given at the time of purchase. Gumroad's can_contact doesn't establish that — it defaults to true for everyone and turns false only when someone unsubscribes.
Does a bulk import raise my Kit bill?
Yes. Confirmed subscribers count toward billing, and Cold subscribers are a subset of Confirmed, so they count too. Unconfirmed, unsubscribed, bounced, complained, and blocked subscribers don't. Importing an old cohort that never opens anything is a recurring monthly cost — a decent argument for slicing by purchase date rather than importing everything.
Bottom line
"Gumroad ConvertKit integration" returns a wall of automation vendors because there's nothing else to return: Gumroad ships four integrations and none of them handle email. Everything is a bridge, and bridges come in two shapes that don't substitute for each other. A webhook moves the next buyer; a bulk import moves the last thousand. Build only the webhook and your existing customers stay invisible to Kit forever, which is usually the larger list.
The bulk move is a handful of checkable decisions: pull from /v2/sales, drop refunds and chargebacks and every row where can_contact is false, map purchase_email rather than email, delete any column you don't want overwriting Kit's existing data, tag by product with one file per product, and pause your automations first. Do that and the numbers will look wrong in the reassuring way — 1,200 rows in, 610 subscribers out, exactly as documented.
Get GumKit for Chrome
Bulk discount codes, PPP regional pricing, bulk price changes and customer CSV export for Gumroad — all using your own API token. Free, runs in your browser, no server.
More Gumroad guides
- Best Gumroad Chrome Extensions (2026): Every One I Could Actually Verify
- Export Gumroad Customers to CSV: Every Column, Field, and Gotcha (2026)
- Gumroad API Access Token: How to Create, Use, and Revoke It (2026)
- Gumroad API Rate Limit: What Actually Triggers a 429
- Gumroad Bulk Discount Codes: One Code, Every Product
- How to Change Prices on All Gumroad Products at Once (2026)
- Gumroad Sales CSV Export Wrong Data? A Symptom-by-Symptom Diagnosis
- Gumroad Discount Code Not Working? Every Error String, Decoded
- Gumroad Fees Explained (2026): What You Actually Keep
- Gumroad PPP Pricing: How the Native Toggle Works vs Region Codes
- Gumroad Sales Tax & VAT: What Sellers Actually Owe (2026)
- Gumroad SEO: What Your Product Page Actually Sends to Google
- Gumroad vs Lemon Squeezy (2026): Which Should Digital Sellers Use?
- ParityDeals Alternative for Gumroad: 4 Options Compared