Skip to main content

Manage custom objects with extensions

Custom objects let you add your own kinds of records to Vendasta CRM, such as appointments, vehicles or memberships, next to the standard ones like contacts and companies.

This guide shows how to create a custom object once, and then give it β€” with the same fields β€” to every SMB you choose. You can do all of it through the API, so you do not need to ask Vendasta to set it up for you.

The big picture​

You bundle your custom objects into an extension. Think of it as a package that you build and own. When you turn the extension on for an SMB, the SMB gets everything in the package. Turn it off, and it disappears for that SMB again, but the SMB's data is kept.

TermWhat it meansExample
ExtensionA package you own. It holds custom object types and their fields."Acme Scheduling"
Custom object typeA kind of record you want to track."Appointment"
FieldOne detail you track on that kind of record."Appointment Status"
SMBOne of your small and medium business customers. In the API it is an account group, with an ID like AG-….A salon
RecordOne real item of that kind, in one SMB.Jane's 10:30 appointment

A few things are worth knowing before you start:

  • Everyone gets the same type. Every SMB that has the extension sees the type with the same ID. That means your own system can use a single ID for all of them.
  • SMBs can look, but not change. An SMB cannot rename or delete a type that comes from your extension. Only you, as the owner, can.
  • Records are kept. Turning an extension off for an SMB hides its types, but the SMB's records are kept. Turn it back on and they return, with the same IDs.

Which ID goes where​

The API uses a field called namespace in almost every call. It is simply the ID of whoever owns the data you are working with. Which ID to use depends on the step:

When you are…Use as the namespace
Creating, renaming or deleting an extensionYour partner ID, for example ABC
Defining the types and fields inside an extensionThe extension ID, for example Extension-1a2b3c… (you get it back when you create the extension)
Turning an extension on or off for an SMB, or working with that SMB's recordsThe SMB's ID, for example AG-EXAMPLE

If you do not have your SMBs' IDs yet, you can list them with the List Business Locations API. To learn more about namespaces, see Understanding Namespaces.

Before you start​

You need a Vendasta service account for your partner, and an access token that carries the right scopes.

  1. Create a service account in Partner Center and download its key file.
  2. Obtain an access token for it. Include the scopes below in the scope of your request.

A service account can work with your own partner and your own SMBs. If a call names another partner's ID, it fails with a 403 error.

Scopes to request​

A scope is a permission your token carries. Each API method accepts certain scopes, and your token needs at least one of them. For everything in this guide, request all four:

crm.schema crm.schema:read crm.custom-object crm.custom-object:read
ScopeWhat it allows
crm.schemaCreate, change and delete extensions, types and fields, and turn extensions on and off for SMBs.
crm.schema:readLook at them. Ask for this one as well: the calls that list fields (field-schema/list and field-schema/get-multi) accept crm.schema:read but not crm.schema.
crm.custom-objectCreate, change and delete records.
crm.custom-object:readLook at records.
note

Each method's page in the API reference lists the scopes it accepts. If your token has none of them, the call fails with a 403 that tells you which scopes would have worked.

Calling the API​

Every call is a POST with a JSON body. Send your token in the Authorization header.

EnvironmentBase URL
Productionhttps://prod.apigateway.co/grpc
Demohttps://demo.apigateway.co/grpc
Authorization: Bearer <access token>
Content-Type: application/json

You can send these calls with curl, any programming language's HTTP library, or an app like Postman or Insomnia. For Postman or Insomnia, open the CRM page in the API reference, use the Download link for the OpenAPI spec, and import it. Then set your token as the bearer token.

The examples below use curl against production. Use the demo address to practise first. Tokens only work in the environment they were made for.

Set up a custom object for your SMBs​

The steps below create an "Appointment" custom object with a dropdown field, give it to one SMB, and create a record.

1. Create your extension​

This creates the empty package. Use your partner ID as the namespace.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/field-extension/create' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "PARTNER_ID",
"extension": {
"name": "Acme Scheduling",
"description": "Custom objects and fields from Acme Scheduling"
}
}'

What you send

FieldWhat it is
namespaceYour partner ID. You become the owner of the extension.
extension.nameA short name for the package, usually the product or system the data comes from, such as "Acme Scheduling". This is required.
extension.descriptionA note about what it is for. Optional.

What you get back

{ "extensionId": "Extension-1a2b3c4d-1111-2222-3333-444455556666" }

extensionId is the ID of your new package. Keep it: you need it in every step below.

2. Add a custom object type​

Now add the kind of record you want to track. This time the namespace is the extension ID from step 1.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/custom-object-type/create' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "Extension-1a2b3c4d-1111-2222-3333-444455556666",
"customObjectType": {
"singularObjectName": "Appointment",
"pluralObjectName": "Appointments"
}
}'

What you send

FieldWhat it is
namespaceThe extension ID. The type is created inside that extension.
customObjectType.singularObjectNameWhat one record is called, such as "Appointment".
customObjectType.pluralObjectNameWhat several are called, such as "Appointments".

What you get back

{ "customObjectTypeId": "CustomObjectTypeID-9f8e7d6c-aaaa-bbbb-cccc-ddddeeeeffff" }

customObjectTypeId identifies your type. Every SMB that gets the extension sees this same ID.

An extension can hold up to 20 types.

3. Add fields​

Fields are the details you track on each record. Create them in the extension, and tell the API which type they belong to.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/field-schema/create' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "Extension-1a2b3c4d-1111-2222-3333-444455556666",
"crmObjectType": "CustomObject",
"crmObjectSubtype": "CustomObjectTypeID-9f8e7d6c-aaaa-bbbb-cccc-ddddeeeeffff",
"fieldSchema": {
"fieldName": "Appointment Status",
"fieldDescription": "Where the appointment stands",
"fieldType": "FIELD_TYPE_DROPDOWN",
"externalId": "appointment_status",
"dropdownOptions": [
{ "label": "Booked", "value": "booked" },
{ "label": "Cancelled", "value": "cancelled" }
]
}
}'

What you send

FieldWhat it is
namespaceThe extension ID.
crmObjectTypeThe kind of CRM object the field belongs to. For custom objects this is always CustomObject.
crmObjectSubtypeThe ID of your type from step 2, so the field is attached to "Appointment".
fieldSchema.fieldNameThe label people see.
fieldSchema.fieldDescriptionA short explanation of the field. Optional.
fieldSchema.fieldTypeWhat kind of value the field holds. This example is a dropdown. See Field types you can use for the full list.
fieldSchema.externalIdYour own identifier for the field. Optional.
fieldSchema.dropdownOptionsFor a dropdown: the choices. label is what people see, and value is what gets stored.

What you get back

{ "fieldId": "FieldID-0a1b2c3d-1111-2222-3333-444455556666" }

fieldId is the ID of the new field. You use it when you create records.

Field types you can use​

The fieldType you choose decides what kind of value the field holds, and which property carries that value when you create a record in step 6.

Show all field types
fieldTypeWhat it holdsHow you send the value in a record
FIELD_TYPE_STRINGFree textstringValue
FIELD_TYPE_INTEGERA whole numberintegerValue, sent as text, for example "5"
FIELD_TYPE_FLOATA number with decimalsfloatValue, for example 1.5
FIELD_TYPE_BOOLEANYes or nobooleanValue, either true or false
FIELD_TYPE_DATEA datedateValue, as a full timestamp, for example "2026-10-21T00:00:00Z". A date on its own, such as "2026-10-21", is rejected.
FIELD_TYPE_DATETIMEA date and timedateValue, for example "2026-10-21T10:30:00Z"
FIELD_TYPE_DROPDOWNOne choice from a list you definestringValue, set to the value of one of the options
FIELD_TYPE_CURRENCYAn amount of moneycurrencyValue, for example {"currencyCode": "USD", "value": 1234}. Also set currencyCode on the field when you create it.
FIELD_TYPE_EMAILAn email addressstringValue
FIELD_TYPE_PHONEA phone numberstringValue, for example "+13065551234"
FIELD_TYPE_TAGA set of tagsstringValues, for example {"values": ["vip", "new"]}
FIELD_TYPE_STRING_LISTA list of text valuesstringValues, for example {"values": ["a", "b"]}
FIELD_TYPE_GEOPOINTA location on a mapgeopointValue, for example {"lat": 52.13, "lon": -106.67}
FIELD_TYPE_TIMERA countdown to a deadlinetimerValue. See the Create Field Schema reference for its shape.

Dropdown fields also need dropdownOptions, and currency fields need a currencyCode. The other types do not need any extra settings.

4. Turn the extension on for an SMB​

This is the step that gives the package to an SMB. Use the SMB's ID as the namespace. Repeat the call for every SMB that should have it.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/added-field-extension/add' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "AG-EXAMPLE",
"extensionId": "Extension-1a2b3c4d-1111-2222-3333-444455556666"
}'

What you send

FieldWhat it is
namespaceThe ID of the SMB, such as AG-EXAMPLE.
extensionIdThe ID of your extension from step 1.

What you get back

An empty response, {}. It means the extension is now on for that SMB.

5. Check what the SMB sees​

List the SMB's custom object types. Your extension's types appear next to the SMB's own, with the same IDs.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/custom-object-type/list' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{ "namespace": "AG-EXAMPLE" }'

You get back a list called customObjectTypes. Look for your "Appointment" type and its customObjectTypeId.

To see the type's fields for that SMB, list its field schemas:

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/field-schema/list' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "AG-EXAMPLE",
"crmObjectType": "CustomObject",
"crmObjectSubtype": "CustomObjectTypeID-9f8e7d6c-aaaa-bbbb-cccc-ddddeeeeffff",
"pagingOptions": { "pageSize": 50 }
}'

pagingOptions.pageSize is how many results to return in one go. The response has a list called fieldSchemas. It contains your field as well as the standard fields every custom object has.

Find the field whose externalId is standard__customobject_name, and note its fieldId. This is the record's name, and every record needs one. You use it in the next step.

6. Create a record for an SMB​

Records belong to the SMB, so use the SMB's ID as the namespace. Give each record a name and fill in your own fields.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/custom-object/create' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "AG-EXAMPLE",
"crmObject": {
"crmObjectSubtype": "CustomObjectTypeID-9f8e7d6c-aaaa-bbbb-cccc-ddddeeeeffff",
"externalId": "your-system-appointment-1042",
"fields": [
{ "fieldId": "<fieldId of standard__customobject_name>", "stringValue": "Jane Smith β€” 10:30" },
{ "fieldId": "FieldID-0a1b2c3d-1111-2222-3333-444455556666", "stringValue": "booked" }
]
}
}'

What you send

FieldWhat it is
namespaceThe ID of the SMB the record belongs to.
crmObject.crmObjectSubtypeThe ID of your type, so the API knows this is an "Appointment".
crmObject.externalIdYour own system's ID for this record. It is optional, and it lets you find the record again by your own ID.
crmObject.fieldsThe values on the record. Each entry pairs a fieldId with its value. Include the name field, plus any of your own fields. Which property carries a value depends on the field's type: see Field types you can use. For a dropdown, send the choice's value, such as booked.

What you get back

{ "crmObjectId": "CustomObjectID-5a4b3c2d-1111-2222-3333-444455556666" }

crmObjectId is the ID of the new record. Read records back with custom-object/get-multi, and delete them with custom-object/delete.

Change things later​

Rename the extension or a type. An update only changes the fields you list in fieldMask.paths. Everything else is left as it is. The example below renames the type, and every SMB that has the extension sees the new names.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/custom-object-type/update' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "Extension-1a2b3c4d-1111-2222-3333-444455556666",
"customObjectType": {
"customObjectTypeId": "CustomObjectTypeID-9f8e7d6c-aaaa-bbbb-cccc-ddddeeeeffff",
"singularObjectName": "Booking",
"pluralObjectName": "Bookings"
},
"fieldMask": { "paths": ["singularObjectName", "pluralObjectName"] }
}'

For the extension itself, call field-extension/update with "paths": ["name", "description"].

Add a field. Repeat step 3. SMBs that already have the extension see the new field.

Retire a field. Call field-schema/archive with the extension ID as the namespace, plus the crmObjectType, crmObjectSubtype and fieldId.

Remove an extension​

Work in the order below, because each step depends on the one before it. Each removal call returns an empty response, {}, when it succeeds.

1. Delete the records you no longer want​

Records belong to an SMB, so use the SMB's ID as the namespace. Repeat for each record.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/custom-object/delete' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "AG-EXAMPLE",
"crmObjectId": "CustomObjectID-5a4b3c2d-1111-2222-3333-444455556666"
}'

What you send

FieldWhat it is
namespaceThe ID of the SMB the record belongs to.
crmObjectIdThe ID of the record, which you got back when you created it.

What you get back

An empty response, {}.

2. Turn the extension off for each SMB​

Use the SMB's ID as the namespace. Repeat for every SMB that has the extension turned on.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/added-field-extension/remove' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "AG-EXAMPLE",
"extensionId": "Extension-1a2b3c4d-1111-2222-3333-444455556666"
}'

What you send

FieldWhat it is
namespaceThe ID of the SMB.
extensionIdThe ID of your extension.

What you get back

An empty response, {}. The SMB no longer sees the extension's types, and its records are kept.

Not sure which SMBs still have it? Ask each SMB which extensions it has turned on:

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/added-field-extension/list' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "AG-EXAMPLE",
"pagingOptions": { "pageSize": 50 }
}'

The response lists the IDs of the extensions that SMB has on. If yours is not in the list, it is off for that SMB. The list is paged, so if an SMB has many extensions, see Paging.

{
"extensionIds": ["Extension-1a2b3c4d-1111-2222-3333-444455556666"],
"pagingMetadata": { "nextCursor": "…" }
}

3. Archive the extension's fields​

Fields live in the extension, so the namespace is the extension ID. Repeat for each field.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/field-schema/archive' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "Extension-1a2b3c4d-1111-2222-3333-444455556666",
"crmObjectType": "CustomObject",
"crmObjectSubtype": "CustomObjectTypeID-9f8e7d6c-aaaa-bbbb-cccc-ddddeeeeffff",
"fieldId": "FieldID-0a1b2c3d-1111-2222-3333-444455556666"
}'

What you send

FieldWhat it is
namespaceThe extension ID.
crmObjectTypeThe kind of CRM object the field belongs to. For custom objects this is always CustomObject.
crmObjectSubtypeThe ID of the type the field belongs to.
fieldIdThe ID of the field to archive.

What you get back

An empty response, {}.

4. Delete the extension's types​

The namespace is the extension ID. Repeat for each type.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/custom-object-type/delete' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "Extension-1a2b3c4d-1111-2222-3333-444455556666",
"customObjectTypeId": "CustomObjectTypeID-9f8e7d6c-aaaa-bbbb-cccc-ddddeeeeffff"
}'

What you send

FieldWhat it is
namespaceThe extension ID.
customObjectTypeIdThe ID of the type to delete.

What you get back

An empty response, {}.

5. Delete the extension​

Use your partner ID as the namespace, the same as when you created the extension.

curl -X POST 'https://prod.apigateway.co/grpc/v1/crm/field-extension/delete' \
-H 'Authorization: Bearer <access token>' \
-H 'Content-Type: application/json' \
-d '{
"namespace": "PARTNER_ID",
"extensionId": "Extension-1a2b3c4d-1111-2222-3333-444455556666"
}'

What you send

FieldWhat it is
namespaceYour partner ID.
extensionIdThe ID of the extension to delete.

What you get back

An empty response, {}.

The API will not delete an extension while any SMB still has it turned on. Instead you get an error, with HTTP status 400 and code 9, that tells you to turn it off for those SMBs first. Use the list call in step 2 to find them.

{
"code": 9,
"message": "extension Extension-1a2b3c4d-1111-2222-3333-444455556666 is still turned on for one or more accounts: remove it from those accounts before deleting it"
}

If the extension was turned on for another partner's SMBs, only Vendasta or that partner can turn it off.

warning

Delete the types and fields before you delete the extension. Once the extension is gone, you can no longer change its types and fields. Deleting a type is also permanent: there is no way to restore it, and it is not blocked when records of that type still exist. Delete those records first.

Any partner can turn an extension on for its own SMBs if it knows the extension ID. Only the owner can change or delete it.

Limits​

  • An extension can hold up to 20 custom object types.
  • An SMB can have up to 3 custom object types of its own. Types that come from extensions do not count toward that limit.

Which API for which job​

I want to…CallScopes (any one)
Create an extensionPOST /v1/crm/field-extension/createcrm, crm.schema
Look up extensions by IDPOST /v1/crm/field-extension/get-multicrm, crm.schema, crm.schema:read
Rename an extensionPOST /v1/crm/field-extension/updatecrm, crm.schema
Delete an extensionPOST /v1/crm/field-extension/deletecrm, crm.schema
Turn an extension on for an SMBPOST /v1/crm/added-field-extension/addcrm, crm.schema
Turn it off for an SMBPOST /v1/crm/added-field-extension/removecrm, crm.schema
See which extensions an SMB hasPOST /v1/crm/added-field-extension/listcrm, crm.schema, crm.schema:read
Check whether an SMB has specific extensionsPOST /v1/crm/added-field-extension/get-multicrm, crm.schema, crm.schema:read
Add a custom object typePOST /v1/crm/custom-object-type/createcrm, crm.schema
See custom object typesPOST /v1/crm/custom-object-type/list, …/get-multicrm, crm.schema, crm.schema:read
Rename a custom object typePOST /v1/crm/custom-object-type/updatecrm, crm.schema
Delete a custom object typePOST /v1/crm/custom-object-type/deletecrm, crm.schema
Add, change or retire a fieldPOST /v1/crm/field-schema/create, …/update, …/archivecrm, crm.schema
See fieldsPOST /v1/crm/field-schema/list, …/get-multicrm, crm.schema:read
Create, change or delete a recordPOST /v1/crm/custom-object/create, …/update, …/deletecrm, crm.custom-object
See recordsPOST /v1/crm/custom-object/get-multi, …/listcrm, crm.custom-object, crm.custom-object:read

When something goes wrong​

A failed call returns an HTTP status and a JSON body with a code, a message and sometimes details. See Errors for the full shape.

StatuscodeWhat it usually meansWhat to do
40116The token is missing, wrong or has expired.Get a new token.
4037The message says missing one of the required scopes. Your token does not carry a permission this call needs.Get a new token with one of the scopes the message lists.
4037The message says Permission Denied. The token is fine, but your service account is not allowed to act on that ID. For example, the ID belongs to another partner, or the type belongs to an extension you do not own.Check the ID you used as the namespace, and who owns the extension.
4003The request was not accepted. It might have a missing or misspelled field, or you may have reached a limit (3 own types per SMB, 20 types per extension).Read the message and details. They name the field or the limit.
4009You tried to delete an extension that SMBs still have turned on.Turn it off for those SMBs, then delete it.
4045The extension, type, field or record was not found.Check the ID, and that you are using the right environment.