Adding/Editing Records
A record is a set of named field values plus, optionally, numbered line items. Over the API you build a Record, fill its values with AddData, and hand it to SaveRecordAsync. The same method both creates and updates: a record whose ID is 0 is created and the server assigns the ID, and a record with an existing ID is updated. Field names are the field names of the Record Type, compared without regard to case.
Add a record
A record belongs to whichever Record Type its values match. In the standard setup every Record Type has a field named RecordType whose filter value is the Record Type's name, so you set the type by adding a RecordType value equal to that name. If an administrator has set a Record Type up differently, its definition shows which field and filter value identify it; send that field instead. Then add one value per field. The saved record comes back with its new ID, and the helper also writes that ID onto the object you passed in.
var vendor = new Record();
vendor.AddData("RecordType", "Vendors");
vendor.AddData("VendorNo", "V-1001");
vendor.AddData("Name", "Acme Supply");
vendor.AddData("Status", "Active");
var saved = await client.SaveRecordAsync(vendor);
Console.WriteLine($"Created record {saved.ID}");
Raw request:
POST /rest/v2/records
Authorization: Bearer dmapi_...
Content-Type: application/json
{
"ID": 0,
"Data": [
{ "DataName": "RecordType", "DataValue": "Vendors" },
{ "DataName": "VendorNo", "DataValue": "V-1001" },
{ "DataName": "Name", "DataValue": "Acme Supply" },
{ "DataName": "Status", "DataValue": "Active" }
]
}
The response is the created record, including its ID, and a Location header pointing at it. Values are always strings on the wire. Send dates in an unambiguous format such as 2026-09-22 and numbers without thousands separators, and the Record Type's field definitions decide how they are stored and displayed.
The helper adds one value for you: a @SOURCE field naming your application, so administrators can see which integration created a record. Pass your application's name as the appKey argument when you construct the client to make that value meaningful, or pass addSourceVariable: false to SaveRecordAsync to leave it out.
Find before you add
Most integrations must not create a duplicate, so the usual pattern is search first, then create or update. This function returns the ID either way:
public async Task<int> UpsertVendorAsync(dmRestHelper client, int vendorTypeID, string vendorNo, string name)
{
var criteria = new List<SearchValueObj>
{
new SearchValueObj { SearchVariable = "VendorNo", SearchValue = vendorNo }
};
var existing = await client.SearchRecordsAsync(criteria, numPerPage: 1, recordTypeID: vendorTypeID);
Record record;
if (existing.Count > 0)
{
record = await client.GetRecordAsync(existing[0].ID);
}
else
{
record = new Record();
record.AddData("RecordType", "Vendors");
record.AddData("VendorNo", vendorNo);
}
record.AddData("Name", name);
var saved = await client.SaveRecordAsync(record);
return saved.ID;
}
Fetch the full record with GetRecordAsync before editing rather than editing the search result. Search results are complete for most purposes, but the fetched record is the authoritative copy and the safest base for a save.
Edit a record
AddData updates an existing value when the field is already on the record and adds it when it is not, so editing is the same call as adding. It returns true when the value actually changed, which lets you skip the save when nothing did.
var record = await client.GetRecordAsync(recordID);
var changed = record.AddData("Status", "Inactive");
changed |= record.AddData("Notes", "Closed per AP request");
if (changed)
await client.SaveRecordAsync(record);
Raw request:
PUT /rest/v2/records
Authorization: Bearer dmapi_...
Content-Type: application/json
{
"ID": 123,
"Data": [
{ "DataName": "Status", "DataValue": "Inactive" },
{ "DataName": "Notes", "DataValue": "Closed per AP request" }
]
}
On an update, fields you do not send are left as they are, so a raw request can carry only the values that changed. Sending the whole record back, which is what the helper does, is equally fine. To clear a field, send it with an empty string as the value.
When you only need to write field values and nothing else about the record, SyncDataAsync takes a list of Data objects and writes them without a full record save. Each Data must carry the RecordID it belongs to, which is already true for values read from a fetched record:
var record = await client.GetRecordAsync(recordID);
record.AddData("LastSyncDate", DateTime.Today.ToString("yyyy-MM-dd"));
await client.SyncDataAsync(record.Data);
Line items
Line items are field values with a line number of 1 or higher. Add them with the lineNumber argument of AddData, using the Record Type's line item field names, and number the lines from 1 without gaps. On an update a line item value is matched by field name and line number just like a record field, so sending line 2's amount changes only that value and leaves the other lines alone.
var invoice = new Record();
invoice.AddData("RecordType", "Invoices");
invoice.AddData("InvoiceNo", "INV-2041");
invoice.AddData("Item", "Widget", lineNumber: 1);
invoice.AddData("Qty", "10", lineNumber: 1);
invoice.AddData("Amount", "250.00", lineNumber: 1);
invoice.AddData("Item", "Bracket", lineNumber: 2);
invoice.AddData("Qty", "4", lineNumber: 2);
invoice.AddData("Amount", "36.00", lineNumber: 2);
var saved = await client.SaveRecordAsync(invoice);
To read them back, GetDataValue("Amount", 2) returns line 2's amount, and record.Data.Where(d => d.LineNumber > 0) gives you every line item value to group by LineNumber.
Delete a record
DeleteRecordAsync(id) moves the record to the recycle bin, where an administrator can restore it. PurgeRecordAsync(id) removes it permanently and cannot be undone, so reserve it for cleanup you are certain about.
await client.DeleteRecordAsync(recordID);
What happens after a save
Saving a record over the API is the same as saving it in the web app. Required fields are enforced, the Record Type's rules run, and any workflow that starts on record creation or on a field change starts. A save that fails validation throws a DocMgtRestException with status 400 and the validation message in Message, such as a required field left blank, so log that message and expect to see it during development.
TIPS
- Search before you add and update the match; the API does not stop duplicates for you.
- RecordType is a field value, the Record Type's name, not a separate property on the record.
- Send values as strings in a format the field can parse; ISO dates and plain numbers are safest.
- Number line items from 1 without gaps, and address an existing line by its number when editing.
- Fetch, then edit, then save. Do not edit search results in place.