GraphQL Contacts
This page documents GraphQL operations for managing customer and vendor contacts in Crane Ledger.
Contact Queries
Get Contact by ID
Retrieve a specific contact by unique identifier.
query GetContact($id: ID!) {
contact(id: $id) {
id
name
contactType
email
phone
taxId
billingAddress
billingCity
billingState
billingPostalCode
billingCountry
createdAt
updatedAt
metadata
}
}
Arguments:
id: ID!- Contact unique identifier
Returns:
- Complete contact information
- Billing address details
- Contact type and metadata
List Organization Contacts
Get all contacts for the organization.
query GetAllContacts {
organization {
contacts {
id
name
contactType
email
phone
billingCountry
createdAt
}
}
}
Filter Contacts by Type
Get contacts of specific types.
query GetCustomers {
organization {
contacts(where: { contactType: { eq: CUSTOMER } }) {
id
name
email
phone
billingAddress
billingCity
billingCountry
}
}
}
query GetVendors {
organization {
contacts(where: { contactType: { eq: VENDOR } }) {
id
name
email
phone
billingAddress
billingCity
billingCountry
}
}
}
Contact Relationships
Contact Transactions
Get transactions involving a specific contact.
query GetContactTransactions($id: ID!, $limit: Int) {
contact(id: $id) {
id
name
contactType
transactions(limit: $limit) {
id
description
date
amount
status
currency {
code
symbol
}
entries {
accountId
entryType
amount
description
}
}
}
}
Arguments:
limit: Int- Maximum transactions to return (default: 50)
Contact Invoices
Get all invoices for a contact.
query GetContactInvoices($id: ID!) {
contact(id: $id) {
id
name
invoices {
id
invoiceNumber
invoiceDate
dueDate
status
subtotal
taxAmount
total
currency {
code
symbol
}
items {
description
quantity
unitPrice
lineTotal
}
}
}
}
Contact Bills
Get all bills for a contact.
query GetContactBills($id: ID!) {
contact(id: $id) {
id
name
bills {
id
billNumber
billDate
dueDate
status
subtotal
taxAmount
total
currency {
code
symbol
}
items {
description
quantity
unitPrice
lineTotal
}
}
}
}
Contact Mutations
Create Contact
Add a new contact to the organization.
mutation CreateContact(
$name: String!
$contactType: ContactType!
$email: String
$phone: String
$taxId: String
) {
createContact(
name: $name
contactType: $contactType
email: $email
phone: $phone
taxId: $taxId
) {
id
name
contactType
email
phone
taxId
createdAt
}
}
Required Fields:
name: Contact display namecontactType: CUSTOMER, VENDOR, or EMPLOYEE
Optional Fields:
email: Email addressphone: Phone numbertaxId: Tax ID or VAT number
Update Contact
Modify an existing contact.
mutation UpdateContact(
$id: ID!
$name: String
$email: String
$phone: String
$taxId: String
) {
updateContact(
id: $id
name: $name
email: $email
phone: $phone
taxId: $taxId
) {
id
name
email
phone
taxId
updatedAt
}
}
Delete Contact
Remove a contact from the organization.
mutation DeleteContact($id: ID!) {
deleteContact(id: $id)
}
Contact Types
Contact Type Enum
enum ContactType {
CUSTOMER # Customers who buy from you
VENDOR # Suppliers you buy from
EMPLOYEE # Company employees
}
Type-Specific Operations
// Customer-specific operations
const getCustomerInvoices = async (customerId) => {
const query = `
query GetCustomerInvoices($id: ID!) {
contact(id: $id) {
name
invoices {
id
invoiceNumber
total
status
dueDate
}
}
}
`;
return await graphql.request(query, { id: customerId });
};
// Vendor-specific operations
const getVendorBills = async (vendorId) => {
const query = `
query GetVendorBills($id: ID!) {
contact(id: $id) {
name
bills {
id
billNumber
total
status
dueDate
}
}
}
`;
return await graphql.request(query, { id: vendorId });
};
Contact Validation
Contact Name Validation
const validateContactName = (name) => {
if (!name || name.trim().length === 0) {
throw new Error('Contact name is required');
}
if (name.length > 255) {
throw new Error('Contact name must be less than 255 characters');
}
// Check for valid characters
const validNameRegex = /^[a-zA-Z0-9\s\-'&.,()]+$/;
if (!validNameRegex.test(name)) {
throw new Error('Contact name contains invalid characters');
}
return true;
};
Email Validation
const validateEmail = (email) => {
if (!email) return true; // Email is optional
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email format');
}
if (email.length > 255) {
throw new Error('Email must be less than 255 characters');
}
return true;
};
Phone Validation
const validatePhone = (phone) => {
if (!phone) return true; // Phone is optional
// Remove all non-digit characters
const digitsOnly = phone.replace(/\D/g, '');
// Check for valid length (10-15 digits for international)
if (digitsOnly.length < 10 || digitsOnly.length > 15) {
throw new Error('Phone number must be 10-15 digits');
}
return true;
};
Tax ID Validation
const validateTaxId = (taxId, country = 'US') => {
if (!taxId) return true; // Tax ID is optional
// US EIN format: XX-XXXXXXX
if (country === 'US') {
const einRegex = /^\d{2}-\d{7}$/;
if (!einRegex.test(taxId)) {
throw new Error('US Tax ID must be in format XX-XXXXXXX');
}
}
// EU VAT format: Country code + number
if (['DE', 'FR', 'GB', 'IT', 'ES'].includes(country)) {
const vatRegex = /^[A-Z]{2}\d{8,12}$/;
if (!vatRegex.test(taxId)) {
throw new Error('EU VAT number must start with country code followed by 8-12 digits');
}
}
return true;
};
Address Management
Address Fields
Contacts can have billing addresses:
type ContactNode {
billingAddress: String
billingCity: String
billingState: String
billingPostalCode: String
billingCountry: String
}
Address Validation
const validateAddress = (address) => {
const required = ['billingAddress', 'billingCity', 'billingCountry'];
for (const field of required) {
if (!address[field] || address[field].trim().length === 0) {
throw new Error(`${field} is required for complete address`);
}
}
// Postal code validation by country
if (address.billingCountry === 'US') {
const usZipRegex = /^\d{5}(-\d{4})?$/;
if (!usZipRegex.test(address.billingPostalCode || '')) {
throw new Error('US postal code must be in format 12345 or 12345-6789');
}
}
return true;
};
Complete Contact Creation
const createCompleteContact = async (contactData) => {
// Validate all fields
validateContactName(contactData.name);
validateEmail(contactData.email);
validatePhone(contactData.phone);
validateTaxId(contactData.taxId, contactData.billingCountry);
if (contactData.billingAddress) {
validateAddress(contactData);
}
// Create contact
const mutation = `
mutation CreateContact($input: CreateContactInput!) {
createContact(input: $input) {
id
name
contactType
email
phone
taxId
billingAddress
billingCity
billingState
billingPostalCode
billingCountry
}
}
`;
return await graphql.request(mutation, { input: contactData });
};
Contact Analytics
Contact Summary
Get summary statistics for contacts.
query GetContactSummary {
organization {
contacts {
contactType
createdAt
}
}
}
# Process on client side to count by type, etc.
Active Contacts
Get contacts with recent activity.
query GetActiveContacts {
organization {
contacts {
id
name
contactType
transactions(limit: 1) {
id
date
}
invoices {
id
status
createdAt
}
bills {
id
status
createdAt
}
}
}
}
Contact Aging
Get aging information for customer balances.
query GetCustomerAging {
organization {
contacts(where: { contactType: { eq: CUSTOMER } }) {
id
name
transactions(limit: 100) {
id
description
date
amount
entries {
accountId
entryType
amount
}
}
}
}
}
# Process transactions to calculate aging buckets
Contact Import/Export
Bulk Contact Creation
const createBulkContacts = async (contacts) => {
const mutations = contacts.map((contact, index) => `
contact${index}: createContact(
name: "${contact.name}"
contactType: ${contact.contactType}
email: "${contact.email || ''}"
phone: "${contact.phone || ''}"
) {
id
name
}
`);
const mutation = `
mutation CreateBulkContacts {
${mutations.join('\n')}
}
`;
return await graphql.request(mutation);
};
Contact Export
query ExportContacts {
organization {
contacts {
id
name
contactType
email
phone
taxId
billingAddress
billingCity
billingState
billingPostalCode
billingCountry
createdAt
updatedAt
}
}
}
Error Handling
Contact Creation Errors
VALIDATION_ERROR: Invalid contact dataCONFLICT: Contact with same name/email already exists
Contact Update Errors
NOT_FOUND: Contact not foundVALIDATION_ERROR: Invalid contact dataFORBIDDEN: Insufficient permissions
Contact Deletion Errors
NOT_FOUND: Contact not foundVALIDATION_ERROR: Contact has active transactions/invoices
Best Practices
Contact Naming Conventions
const formatContactName = (name, contactType) => {
// Capitalize properly
const capitalized = name.toLowerCase()
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
// Add type indicators for clarity
if (contactType === 'VENDOR') {
return `${capitalized} (Vendor)`;
}
return capitalized;
};
Contact Categorization
const categorizeContact = (contact) => {
const categories = {
VIP: contact.transactions?.length > 100,
NEW: new Date(contact.createdAt) > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
INACTIVE: !contact.transactions?.length,
OVERDUE: contact.invoices?.some(inv => inv.status === 'OVERDUE')
};
return Object.keys(categories).filter(key => categories[key]);
};
Duplicate Prevention
const checkDuplicateContact = async (name, email) => {
const existingByName = await graphql.request(`
query CheckName($name: String!) {
organization {
contacts(where: { name: { eq: $name } }) {
id
}
}
}
`, { name });
if (existingByName.organization.contacts.length > 0) {
throw new Error('Contact with this name already exists');
}
if (email) {
const existingByEmail = await graphql.request(`
query CheckEmail($email: String!) {
organization {
contacts(where: { email: { eq: $email } }) {
id
}
}
}
`, { email });
if (existingByEmail.organization.contacts.length > 0) {
throw new Error('Contact with this email already exists');
}
}
};
Contact Maintenance
const cleanupContacts = async () => {
// Find contacts with no activity
const inactiveContacts = await graphql.request(`
query GetInactiveContacts {
organization {
contacts {
id
name
transactions(limit: 1) {
id
}
invoices {
id
}
bills {
id
}
}
}
}
`);
// Filter for contacts with no activity
const toCleanup = inactiveContacts.organization.contacts.filter(contact =>
contact.transactions.length === 0 &&
contact.invoices.length === 0 &&
contact.bills.length === 0
);
// Optionally delete or mark as inactive
for (const contact of toCleanup) {
console.log(`Inactive contact: ${contact.name} (${contact.id})`);
// Could delete or update status
}
return toCleanup;
};
Need help?
Create a free account to access our support portal. Once signed in, use the Support tab in your dashboard to submit a support ticket — our team typically responds within 24 hours.
- ✨ For LLMs/AI assistants: Read our structured API reference