Map a CUSIP to ticker and CIK
Convert CUSIPs to tickers and SEC CIKs with the SecFeed CUSIP mapping API, then map a whole 13F portfolio, with a validated name fallback for the CUSIPs that do not match.
13F holdings, 13D and 13G cover pages, N-PORT fund holdings and most bond documents identify securities by CUSIP. Market data and EDGAR use tickers and CIKs. /v1/mapping/cusip bridges the two. SecFeed learns CUSIPs from 13F information tables, where every institutional holding since 2013 is reported by CUSIP with the issuer's name, and matches that name to the SEC's ticker list.
Setup
import os
import re
import requests
BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}
def get(path, **params):
r = requests.get(f"{BASE}{path}", params=params, headers=HEADERS, timeout=60)
r.raise_for_status()
return r.json()
One CUSIP
print(get("/v1/mapping/cusip", cusip="594918104")[0])
Output:
{'cusip': '594918104', 'issuerName': 'MICROSOFT CORP', 'matches': [{'cik': 789019, 'name': 'MICROSOFT CORP', 'ticker': 'MSFT', 'exchange': 'Nasdaq'}]}
The response is a list with one entry per way 13F filers have spelled the issuer's name, most common first. For NVIDIA's CUSIP 67066G104 there are three: NVIDIA CORP, NVIDIA CORP COM and NVIDIA Corp, all matching CIK 1045810. Use the first entry's first match.
A whole 13F portfolio
The realistic job is converting a fund's holdings. Take Berkshire Hathaway's 13F for the quarter ended June 30, 2026, collect the distinct CUSIPs and map each one:
rows = get("/v1/form-13f/holdings", cik=1067983, period="06-30-2026")
cusips = {}
for r in rows:
cusips.setdefault(r["cusip"], r["issuer"])
print(len(rows), "holdings rows,", len(cusips), "distinct CUSIPs")
89 holdings rows, 29 distinct CUSIPs
13F names are abbreviated by the filer ("CAPITAL ONE FINL CORP", "D R HORTON INC"), and sometimes the abbreviation does not match the SEC's company name. When the CUSIP lookup finds no ticker, retry with /v1/mapping/name. Accept a name hit only if the issuer's most distinctive word appears in it, otherwise the first fuzzy hit can be a different company:
def to_ticker(cusip, issuer):
m = get("/v1/mapping/cusip", cusip=cusip)
if m and m[0]["matches"]:
return m[0]["matches"][0], "cusip"
words = re.sub(r"[^A-Za-z0-9 ]", " ", issuer).split()
key = max(words, key=len).upper() # must appear in the match, or it is a guess
for n in (3, 2):
for hit in get("/v1/mapping/name", name=" ".join(words[:n])):
if key in hit["name"].upper():
return hit, "name"
return {}, "none"
found = {"cusip": 0, "name": 0, "none": 0}
for cusip, issuer in sorted(cusips.items()):
best, how = to_ticker(cusip, issuer)
found[how] += 1
print(f"{cusip} {issuer[:26]:26} {best.get('ticker', ''):6} {str(best.get('cik', '')):8} via {how}")
print(found)
Output:
02005N100 ALLY FINL INC via none
02079K107 ALPHABET INC GOOGL 1652044 via cusip
02079K305 ALPHABET INC GOOGL 1652044 via cusip
025816109 AMERICAN EXPRESS CO AXP 4962 via cusip
037833100 APPLE INC AAPL 320193 via cusip
060505104 BANK OF AMER CORP BAC 70858 via cusip
14040H105 CAPITAL ONE FINL CORP COF 927628 via name
166764100 CHEVRON CORPORATION CVX 93410 via cusip
191216100 COCA COLA CO KO 21344 via cusip
23331A109 D R HORTON INC DHI 882184 via name
23918K108 DAVITA INC DVA 927066 via name
247361702 DELTA AIR LINES INC DAL 27904 via name
47233W109 JEFFERIES FINANCIAL GROUP JEF 96223 via name
500754106 KRAFT HEINZ CO KHC 1637459 via cusip
501044101 KROGER CO KR 56873 via cusip
526057104 LENNAR CORP LEN 920760 via cusip
526057302 LENNAR CORP LEN 920760 via cusip
530909100 LIBERTY LIVE HOLDINGS INC LLYVA 2078416 via cusip
530909308 LIBERTY LIVE HOLDINGS INC LLYVA 2078416 via cusip
546347105 LOUISIANA PAC CORP via none
55616P104 MACYS INC via none
615369105 MOODYS CORP MCO 1059556 via cusip
62944T105 NVR INC NVR 906163 via cusip
650111107 NEW YORK TIMES CO MTN BE NYT 71691 via cusip
670346105 NUCOR CORP NUE 73309 via cusip
674599105 OCCIDENTAL PETE CORP via none
829933100 SIRIUSXM HOLDINGS INC via none
92343E102 VERISIGN INC VRSN 1014473 via cusip
H1467J104 CHUBB LIMITED via none
{'cusip': 18, 'name': 5, 'none': 6}
23 of 29 CUSIPs resolve, 18 directly and 5 through the validated name search. The run took about 15 seconds.
The ones that do not match
The six misses all come from the way 13F filers abbreviate names. Here is each one next to the name the SEC lists:
| 13F issuer name | SEC company name | Why the name search missed |
|---|---|---|
| ALLY FINL INC | Ally Financial Inc. | FINL abbreviation |
| LOUISIANA PAC CORP | LOUISIANA-PACIFIC CORP | PAC abbreviation and the hyphen |
| OCCIDENTAL PETE CORP | OCCIDENTAL PETROLEUM CORP /DE/ | PETE abbreviation |
| CHUBB LIMITED | Chubb Ltd | LIMITED versus Ltd |
| MACYS INC | Macy's, Inc. | apostrophe |
| SIRIUSXM HOLDINGS INC | SIRIUS XM HOLDINGS INC. | spacing |
Normalize before searching to recover most of these: expand common 13F abbreviations (FINL to FINANCIAL, PAC to PACIFIC, PETE to PETROLEUM, LIMITED to LTD), drop punctuation, and compare names with spaces removed. A search on the first distinctive word alone (/v1/mapping/name?name=occidental) also finds the right company, but then check the match yourself.
Leave them unmatched rather than accept a guess. A wrong ticker silently joins the wrong price series, which is worse than a gap you can see.
Two share classes, one company
Alphabet (02079K107 class A, 02079K305 class C), Lennar and Liberty Live each have two CUSIPs that map to the same CIK and ticker. When you aggregate a portfolio by company, group by CIK; when you need the exact security, keep the CUSIP.