Gumroad ConvertKit Integration: What Exists, and How to Move Buyers to Kit

Updated 2026-07-27 · GumKit guides

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:

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? Nofour 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":

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:

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:

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:

  1. Drop every row where can_contact === false. These people unsubscribed from you already, on a different platform.
  2. Slice by created_at. Recent buyers into your main flow; older cohorts get a re-permission email rather than a sequence.
  3. 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:

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:

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.

Add to Chrome — it's free