Fix NetSuite Custom Transaction Attach Error
record.attach() rejects custom transaction types with a misleading FILE_ATTACH_ERROR permission message. The real cause is unsupported record types, not roles.

On this page
When you call record.attach() from a RESTlet and target a custom transaction type, the API rejects the call with FILE_ATTACH_ERROR and the message "You do not have permission to perform this operation." The same code works against Purchase Orders and Employee Expense Reports. The difference comes down to how record.attach() resolves the to record type.
What the Error Message Actually Means
The error text is misleading. It reads like a role or permission problem, which is why the first instinct is to check the Integration role's permission sublist on the custom transaction type. That is not where the failure happens.
record.attach() in SuiteScript 2.1 maps to the SuiteTalk attach operation. The API only recognizes a fixed set of record types in the to object. Custom transaction types are not part of that set, so the attach call fails before any permission check runs. The FILE_ATTACH_ERROR is the generic wrapper NetSuite throws when the target record type is not supported by the attach operation.
This also explains why your UI test passes. When you attach a file through the Communication > Files subtab, NetSuite uses the internal file sublist rather than the record.attach() API. Those are two different code paths. The UI path accepts any transaction, including custom types. The API path does not.
The Fix: Use the Generic Transaction Type
The solution is to stop naming the custom transaction type in the to object and use the generic transaction type instead. The to object needs the internal ID of the record you created, but the type must be transaction, not the custom type's internal ID.
Here is the corrected RESTlet pattern:
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*/
define(['N/record'], (record) => {
const post = (requestBody) => {
// requestBody.fileId and requestBody.transactionId come from the caller
record.attach({
record: {
type: 'file',
id: requestBody.fileId
},
to: {
type: 'transaction', // generic type, not the custom type
id: requestBody.transactionId
}
});
return { success: true };
};
return { post };
});The key change is type: 'transaction'. That is how NetSuite's attach operation expects custom transaction records to be addressed. The SuiteScript Records Guide confirms that custom transactions use the generic transaction search record, and the same principle applies to the attach operation's RecordRef.
The same applies to SuiteTalk SOAP: the attach request targets a RecordRef with type="transaction" and the record's internal ID. When retrieving a custom transaction via SOAP, you use CustomTransactionRef with the typeId field identifying the custom transaction type, but the internalId field still points to the specific transaction instance.
Why the Permission Sublist Never Helped
You said you added the Integration role to the permission sublist on the custom transaction type. That step is still correct and worth keeping, but it governs who can create and edit records of that type, not who can attach files to them. The Customization Guide breaks this down clearly: the permission levels (View, Create, Edit, Full) describe what users can do with transaction instances themselves. None of those levels extend the attach operation's supported record types.
A role with Full access on the custom type can still hit FILE_ATTACH_ERROR from the API because the attach operation itself does not recognize the type. The permission sublist and the attach operation's record-type restrictions are two separate layers.
Test This With a Single Record First
Before you route your whole integration through the change, validate it with one record. Take the internal ID of a custom transaction instance you already created, run the corrected record.attach() call against it, and confirm the file lands on the record's Communication > Files subtab.
Here is the validation step. After the attach succeeds, load the record and check the file count:
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record', 'N/search', 'N/ui/serverWidget'], (record, search, serverWidget) => {
const onRequest = (context) => {
const transactionId = context.request.parameters.transactionid;
const fileSearch = search.create({
type: 'file',
filters: [
['availablewithoutlogin', 'is', 'F'],
'and',
['attachedto', 'anyof', transactionId]
],
columns: ['name']
});
const fileCount = fileSearch.runPaged().count;
context.response.writeLine('Attached files: ' + fileCount);
};
return { onRequest };
});If the count increments, the attach path is working. If it stays at zero, check that the file record itself has availablewithoutlogin set correctly, since that filter can hide files from the search even when attached.
Edge Case: Files Already in the Cabinet
If the file is already in the File Cabinet, you do not need to upload it again. The attach operation links an existing file record to the transaction. If your RESTlet currently uploads the file and then attaches it in the same call, keep those as two separate steps: create the file record with record.create({ type: 'file' }), then attach it using the generic transaction type.
One more trap: the to.id must be the internal ID of the transaction instance, not the custom transaction type's type ID. Confusing the two produces a "record does not exist" style failure even after you fix the type. Trace the data flow: the record you created returns an internal ID in its response, and that is the value you pass to to.id.
What to Check Next
If you still see FILE_ATTACH_ERROR after the type fix, the remaining causes are narrower:
- The file record is not accessible to the Integration role. Confirm the role has File Cabinet permission (Setup > Users/Roles > Manage Roles > File > View).
- The transaction was created under a different subsidiary or employee than the file. Some attach operations respect subsidiary-based access.
- The role lacks transaction-level attach permission. In the UI, verify you can attach a file to the same record while logged in as the Integration role user, not as Administrator.
The UI check is the fastest way to separate a genuine permission gap from the record-type limitation. If the UI attach works under the Integration role, the problem is the API call, and the generic transaction type is your fix. If the UI attach also fails, you have a role configuration issue and should review the custom transaction type's permission sublist and the role's File > View permission together.


