๐ Format Conversion ยท 7 min read
CSV to vCard: Turn a CSV Contact List into a .vcf File
Google Contacts will read your CSV directly; iPhone and Android will not. Those importers want vCard, a text format where every contact is a BEGIN:VCARD block with named properties instead of bare columns. So the job is not really a file conversion, it is a column mapping: decide which CSV header becomes FN, which becomes TEL, which becomes EMAIL, then write one block per row.
That distinction matters because it explains every failure people hit. The conversion itself takes minutes. What takes an afternoon is a file that imports 4,000 blank contacts, or one where every number starts with 44 instead of +44, and nothing in the error message tells you which column did it.
Why a CSV Cannot Be Read as Contacts
RFC 4180 gives CSV one job: represent a table as text, with a header row and comma-separated values. It says nothing about what the columns mean. A file with headers Name,Phone,Email is a table. A file with headers a,b,c holding the same data is the same table, and the format is equally happy with both.
vCard is the opposite. It is a format built for one kind of object, and it names its fields. vCard 4.0 is RFC 6350, published in August 2011; vCard 3.0 is RFC 2426. In a vCard file the field names are part of the payload:
BEGIN:VCARD
VERSION:3.0
N:Okafor;Adaeze;;;
FN:Adaeze Okafor
ORG:Northwind Traders
TEL;TYPE=CELL:+442079460018
EMAIL;TYPE=INTERNET:adaeze@northwind.example
END:VCARDTwo properties are not optional. FN carries the display name and must be present, and VERSION must appear immediately after BEGIN:VCARD. Everything else can be left out without breaking the file. That is the whole specification you need to get an import working.
The Column Mapping
Before writing any code, sit with your header row and decide the mapping. This table covers what most contact exports from a CRM or a spreadsheet actually contain.
| CSV column | vCard property | Notes |
|---|---|---|
| First name + Last name | N:Last;First;;; | Structured name. Semicolons separate the parts, so a comma inside a surname needs escaping. |
| Full name | FN: | Required. If you have no separate first and last columns, put the whole name here. |
| Company | ORG: | One value, no semicolons unless you mean a department. |
| Job title | TITLE: | Free text. |
| Mobile | TEL;TYPE=CELL: | Keep the plus sign and country code. Drop spaces and brackets. |
| Work phone | TEL;TYPE=WORK,VOICE: | Two type values, comma separated, no spaces. |
EMAIL;TYPE=INTERNET: | One property per address if a row has several. | |
| Website | URL: | Include the scheme, so https:// and not a bare domain. |
| Notes | NOTE: | Escape commas, semicolons and backslashes. Newlines become \n. |
Columns you have no property for should be dropped, not crammed into NOTE. Importers show NOTE as a wall of text on the contact card, and it is the first thing a user deletes.
Method 1: Google Contacts, Which Reads CSV
If the destination is a Google account, skip vCard entirely. Google Contacts imports CSV and asks you to match its fields to your columns during the import, so Mobile can be pointed at "Phone 1 - Mobile" without renaming anything.
Two things make that import go wrong. The first is a duplicate: if the same address already exists, Google may create a second card rather than merge, which doubles your list. The second is group labels: a "Labels" column has to match an existing label or the import creates new ones with typos included. Clean the file first with the free CSV cleaner so duplicates and stray whitespace are gone before anything reaches your account.
Method 2: A Script, One Block Per Row
This is the version worth keeping, because contact lists get re-exported every quarter. It reads a CSV, escapes the values the spec requires, and writes CRLF line endings.
import csv, re
def esc(v):
return (v.replace("\\", "\\\\").replace(";", "\\;")
.replace(",", "\\,").replace("\n", "\\n"))
with open("contacts.csv", newline="", encoding="utf-8-sig") as src, \
open("contacts.vcf", "w", encoding="utf-8", newline="") as out:
for row in csv.DictReader(src):
first = row.get("First", "").strip()
last = row.get("Last", "").strip()
full = f"{first} {last}".strip()
if not full:
continue
tel = re.sub(r"[^\d+]", "", row.get("Phone", ""))
out.write("BEGIN:VCARD\r\nVERSION:3.0\r\n")
out.write(f"N:{esc(last)};{esc(first)};;;\r\n")
out.write(f"FN:{esc(full)}\r\n")
if tel:
out.write(f"TEL;TYPE=CELL:{tel}\r\n")
email = row.get("Email", "").strip()
if email:
out.write(f"EMAIL;TYPE=INTERNET:{email}\r\n")
out.write("END:VCARD\r\n")Three details in that script are doing real work. encoding="utf-8-sig" strips the byte-order mark Excel adds, which otherwise becomes part of your first header and makes row["First"] fail. newline="" stops Python from rewriting the CRLF pairs. And the regex keeps only digits and the plus sign, which is the difference between a number that dials and one that does not.
Run it, then open the .vcf in a text editor and read the first block. If FN is there and the phone number starts with a plus, the file is probably fine.
Method 3: A Converter, When You Have One File
For a single list on a deadline, an online CSV-to-vCard converter is the right call. Upload, map the columns, download. Check three things in the output before you hand it over: that the number of BEGIN:VCARD blocks matches your row count, that every block has an FN line, and that accented characters survived as UTF-8 rather than question marks.
Excel can fake it too, with a formula-built column that concatenates the vCard lines and a save as plain text with a .vcf extension. It works, but only if the file lands as UTF-8 with CRLF endings, and Excel's "save as" dialog is not always helpful about either. If you go this route, open the result in a text editor and confirm the encoding first.
The Four Ways an Import Fails
- The phone number was a number. Spreadsheets drop the leading zero and the plus sign, so the vCard inherits a number that cannot be dialled from abroad. Keep the column as text in the source. The leading zero guide covers the same trap in CSV terms.
- No FN property. A block with only N, TEL and EMAIL is valid-looking and shows as an unnamed contact, or gets skipped by importers that require FN. Write it on every block.
- Unescaped commas and semicolons. A company called "Smith, Jones & Co" breaks ORG unless the comma is backslash-escaped. Same for a semicolon in a surname, which N reads as a field divider.
- Long values on one line. RFC 6350 says a content line should be folded to a maximum width of 75 octets, with the remainder on the next line starting with a single space. A 400-character NOTE on one line is the usual reason one contact fails while the rest import fine.
Before You Import, Check the Source File
Every one of those failures starts in the CSV, not the vCard. Open the source and look at the delimiter, the encoding and the column types: a file exported from a European Excel may be semicolon-delimited, and a phone column may already have lost its leading zero before you touched it. The CSV analyzer reports delimiter, encoding and inferred column types in one pass, and the email extraction guide covers cleaning an address column when the list came out of a CRM.
If the list is going somewhere other than a phone, vCard is the wrong target. For a spreadsheet, the CSV vs Excel comparison explains what you gain and lose by staying in plain text. For an app or an API, JSON is the usual destination and the CSV to JSON guide covers that direction.
Frequently Asked Questions
Can I import a CSV file directly as contacts?
Sometimes. Google Contacts reads CSV and lets you map columns to contact fields. iPhone and Android system importers expect vCard (.vcf), so for those you convert first. A plain CSV carries no field names beyond its header, so nothing tells the phone that one column is a phone number.
How many contacts can one .vcf file hold?
The format sets no limit; it is one BEGIN:VCARD block repeated per contact. The importer is the real limit, and large single files fail slowly with a generic error on a phone. Splitting into batches of a few thousand makes a failure easy to isolate.
Why does my .vcf file import with empty names?
Usually a missing FN property. FN is mandatory in RFC 6350 and most importers display the contact by it, so a block with only N and TEL shows as a blank row. Write FN with the display name on every block.
Should I write vCard 3.0 or 4.0?
3.0 for anything going to phones, 4.0 when both ends are modern software you control. 3.0 is what older iOS and Android importers handle without argument; 4.0 is cleaner for phone numbers as tel: URIs but some importers drop properties they do not recognise.
Why do phone numbers lose the plus sign or leading zero?
Because the spreadsheet stored the cell as a number. Numbers have no leading zero, so 0044 20 7946 0018 becomes 442079460018 and the dialling prefix is gone. Store the column as text, keep the plus, and strip only spaces, brackets and dashes on the way out.
Do line endings matter in a .vcf file?
Yes. The vCard grammar ends every content line with CRLF, so a Unix-linefeed file can import as one unreadable contact on strict parsers. Write CRLF and UTF-8, and fold lines longer than 75 octets onto a continuation line starting with a space.
Check the Contact CSV First
See the delimiter, the encoding and the column types before you convert, so a lost leading zero or a semicolon-delimited export does not turn into a failed import.
Related reading
Format Conversion โ other guides that pair well with this one.
- CSV to KML: Plot a CSV of Coordinates on Google Earth
- Convert CSV to PDF
- Convert CSV to Excel Without Excel
- Convert Excel to CSV Free Online
Browse all guides in the NoCodeCSV blog.