---
title: "INVALID_CROSS_REFERENCE_KEY: the Id is valid but points at nothing usable"
url: "https://trailassembly.com/invalid-cross-reference-key/"
topic: "Apex and Development"
published: "2026-09-09T08:18:40+00:00"
updated: "2026-10-06T13:51:14+00:00"
error_codes: "ENTITY_IS_DELETED, INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY, INVALID_CROSS_REFERENCE_KEY, INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST, MALFORMED_ID"
---

# INVALID_CROSS_REFERENCE_KEY: the Id is valid but points at nothing usable

**The quick version:** an Id you put in a lookup, master-detail, or `RecordTypeId` field is well-formed but points at nothing usable — wrong object type, wrong org, deleted, or an inactive record type. Take the failing Id, read its first three characters, and check that prefix against the object the field actually points to. That one check resolves most cases.

## What the platform is telling you

The SOAP API reference defines `INVALID_CROSS_REFERENCE_KEY` as “a record with the specified ID does not exist in the database.” That undersells it. The Id can exist and you still get this error, because the check is not only “does this record exist” but “is this Id legal in this field.”

Knowing which error you did *not* get narrows things fast:

| Status code | Means |
| --- | --- |
| MALFORMED_ID | Not a valid Id at all. Docs: “An ID must be either 15 characters, or 18 characters with a valid case-insensitive extension.” |
| INVALID_CROSS_REFERENCE_KEY | Structurally fine, but not valid for this field |
| ENTITY_IS_DELETED | You’re operating on a record already in the Recycle Bin |
| INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY | The Id is valid and the record exists — the running user just can’t see it |

That last row matters. If the record genuinely exists and is the right type, you have a permissions problem, not a data problem.

## Read the key prefix first

Every record Id starts with a three-character prefix identifying its object. The Tooling API documents this on `EntityDefinition.KeyPrefix`: “the first three digits of the object’s ID, which identify the object type, such as Account or Opportunity.”

Dump the map for your org once and keep it:

```apex
sf data query --use-tooling-api \
  --query "SELECT QualifiedApiName, KeyPrefix FROM EntityDefinition WHERE KeyPrefix != null ORDER BY KeyPrefix"
```

Now when a load fails on `0035g00000ABCDEabc` in a field named `Account__c`, you can see in two seconds that `003` is Contact and `001` is Account. Someone mapped the wrong column. This is the most common cause, and it is invisible if you only stare at the Id.

Prefixes worth memorizing: `001` Account, `003` Contact, `005` User, `006` Opportunity, `00Q` Lead, `012` RecordType, `500` Case, `00G` Group.

## Causes, most frequent first

**A lookup pointing at the wrong sObject.** A CSV column labelled “Account” holding Contact Ids. A mapping file that drifted after someone reordered columns. Or Apex assigning `opp.AccountId = con.Id`, which compiles cleanly because both variables are typed `Id` and the compiler cannot tell them apart.

That last one is worth designing against. `Id` is a loose type. Carrying the sObject instead makes the assignment prove the type:

```apex
// Compiles, fails at runtime
Map<String, Id> parentByKey = buildLookup();
opp.AccountId = parentByKey.get(externalKey);

// Better: a missed lookup is a null object you can test
Map<String, Account> accountByKey = buildAccountLookup();
Account parent = accountByKey.get(externalKey);
if (parent == null) {
    continue;   // handle the miss explicitly
}
opp.Account = parent;
```

**An Id from a different org.** Classic in sandbox refreshes and migrations. You built a mapping file in the full sandbox and ran it against production. Ids are unique across all orgs, so nothing collides — the references simply resolve to nothing.

The fix is structural: never migrate on Salesforce Ids. Put an [External Id](https://trailassembly.com/upsert-with-external-ids/) field on each object, populate it in both orgs, and upsert on that. When you must translate Ids, build the map by querying the *target* org, not from a spreadsheet saved last quarter.

**A RecordTypeId that doesn’t belong to the object.** A `012` Id is a record type, but a record type belongs to exactly one object. Set an Account record type on a Contact and you get this error even though the record type is perfectly valid where it lives. An inactive record type does the same — the Id resolves, but the platform will not let you assign it.

```apex
sf data query --query "SELECT Id, DeveloperName, SobjectType, IsActive FROM RecordType WHERE SobjectType = 'Contact'"
```

Check `IsActive`, and check the record type is available to the profile of the user running the load. Availability is per profile, and an integration user’s profile is usually the one nobody updated after a new record type shipped. In Apex, resolve by developer name rather than hardcoding — record type Ids differ between sandbox and production:

```apex
Id rtId = Schema.SObjectType.Contact
    .getRecordTypeInfosByDeveloperName()
    .get('Partner_Contact')
    .getRecordTypeId();
```

**The referenced record was deleted.** A soft-deleted parent still has an Id, and a spreadsheet built before the delete still carries it. Confirm with `ALL ROWS`:

```apex
SELECT Id, Name, IsDeleted FROM Account WHERE Id = '001xx000003DGb2AAG' ALL ROWS
```

Either undelete the parent or repoint the child. Note the distinction: referencing a deleted record from a lookup gives you this error, while updating the deleted record itself gives `ENTITY_IS_DELETED`.

**15-character Ids mangled by a spreadsheet.** The Object Reference is direct about the risk: 15-character Ids are case-sensitive, and “to Salesforce, `000000000000Abc` isn’t the same as `000000000000aBC`.” Excel autocorrect or a lowercasing step in an ETL job breaks them silently. The same page notes [18-character](https://trailassembly.com/invalid-id-15-vs-18/) Ids are “case-safe, but not case-insensitive”. Change the case by hand and Salesforce detects the suffix no longer matches and errors. Use [18-character](https://trailassembly.com/invalid-id-15-vs-18/) Ids and treat them as opaque strings.

**Something that looks like a picklist but isn’t.** A [restricted picklist](https://trailassembly.com/invalid-or-null-for-restricted-picklist/) normally returns `INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST` (“you specified an invalid or null value for a restricted picklist”) not this error. If you get a cross-reference failure on a dropdown, the field is probably a lookup rendered as a picker, which is common on `OwnerId`, queue references, and `Pricebook2Id`.

## Diagnosing a load that failed on 40 of 20,000 rows

Get per-row errors rather than one aggregated message. The Apex Developer Guide notes that with `allOrNone` false, “if a record fails, the remainder of the DML operation can still succeed. Your application can then inspect the rejected records and possibly retry the operation.”

```apex
List<Database.SaveResult> results = Database.insert(records, false);

Map<String, Integer> failureCounts = new Map<String, Integer>();
for (Integer i = 0; i < results.size(); i++) {
    if (results[i].isSuccess()) {
        continue;
    }
    for (Database.Error e : results[i].getErrors()) {
        String key = e.getStatusCode() + ' on ' + String.join(e.getFields(), ',');
        Integer seen = failureCounts.get(key);
        failureCounts.put(key, seen == null ? 1 : seen + 1);
    }
}
System.debug(failureCounts);
```

`getFields()` names the offending lookup, which is the whole answer when a record has six of them. Grouping by status code plus field turns 40 scattered failures into “38 are the same mapping bug and 2 are deleted parents.”

For Data Loader, pivot the error CSV on the error column. For Bulk API 2.0, request the failed-results file rather than reading the job summary. The summary gives you the count, the results file gives you the rows.

## Preventing it

Migrate on External Ids and upsert, never on Salesforce Ids. This removes the wrong-org, deleted-parent, and mangled-Id causes outright. The cost is a field per object plus the discipline to populate it, and I have never regretted paying it.

Validate mapped Ids before the load rather than during it. A pre-flight check that reads each Id’s three-character prefix and compares it to the target field’s referenced object catches wrong-object mapping in seconds instead of after a 20-minute Bulk API job. Against the `EntityDefinition` query above that is about ten lines of script.

What I would skip: try/catch wrappers that swallow the error and carry on. You end up with silently missing relationships, which is worse than a failed load because nobody notices for six months.

## Frequently asked

### Why does the error not tell me which field is wrong?

From a UI save it usually does not. From Apex, `Database.Error.getFields()` returns the affected field names, and Data Loader’s error file includes them. Route loads through something that surfaces the field rather than guessing.

### Is this the same as MALFORMED_ID?

No. `MALFORMED_ID` means the string is not a valid Id. Wrong length or a bad checksum on an 18-character value. This error means the Id is structurally valid but not acceptable in that field.

### Can I get this on a field that is not a lookup?

Yes: `RecordTypeId`, `OwnerId`, `Pricebook2Id`, and queue assignments all behave as cross-references even when the UI presents them as pickers.

### Why did the same load work in the sandbox?

Almost always record type Ids or other hardcoded Ids that differ between orgs. Resolve [record types](https://trailassembly.com/record-types/) by developer name at runtime and keep environment-specific Ids in custom metadata rather than in code or the CSV.

### Do I need to convert 15-character Ids to 18 before loading?

You do not need to, but do it anyway. The 18-character form is case-safe, so it survives a trip through a spreadsheet or a tool that normalizes case. The 15-character form does not.

---

*Reference: [Core Data Types Used in API Calls](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_concepts_core_data_objects.htm) (SOAP API Developer Guide), [Field Types](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/field_types.htm) and [Relationships Among Objects](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/relationships_among_objects.htm) (Object Reference), [EntityDefinition](https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_entitydefinition.htm) (Tooling API), and [DML Statements vs. Database Class Methods](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_dml_database.htm) (Apex Developer Guide).*

---

Source: https://trailassembly.com/invalid-cross-reference-key/
Trail Assembly — independent Salesforce write-ups.
