Integrations and messaging
This guide covers three outbound connections an organization sets up itself: Zoho for accounting sync, WhatsApp for customer messages, and email for the organization's own sending. All three live in settings, all three keep their secrets write-only, and all three have the same shape: save the configuration, test it against the real service, then switch it on.
For the exact field list of every type, see the API reference. The examples use <...> for values you supply, and @example.com addresses.
1. Plan the integration
| Job | Operations | Permission |
|---|---|---|
| Read Zoho state | zohoConnectionStatus, zohoSyncStatus, zohoApiUsage, zohoSyncLogs, zohoSyncLogsCount, zohoMigrationStatus | settings:integration:read |
| Run Zoho | zohoStartMigration, zohoResumeMigration, zohoForcePoll, zohoForceReconciliation, zohoSetApiLimit, zohoRetryFailedSync | settings:integration:update |
| Read WhatsApp setup | whatsAppConfig, whatsAppTriggerCatalog, whatsAppTemplates, whatsAppChannels, whatsAppMessageLogs | settings:integration:read |
| Set up WhatsApp | upsertWhatsAppConfig, testWhatsAppConnection, updateWhatsAppTrigger, setWhatsAppEnabled, createWhatsAppTemplate, sendWhatsAppTestMessage | settings:integration:update |
| Read email setup | emailConfig | settings:email:read |
| Set up email | createEmailConfig, updateEmailConfig, deleteEmailConfig, testEmailConfig, emailSenderSetup, setEmailSenderDomain, verifyEmailSenderDomain, removeEmailSenderDomain | settings:email:update or settings:tenant:update |
Every call is scoped to your organization, which comes from the token and never from an argument. The Zoho queries still accept an organizationId argument for old clients. It is ignored, so leave it out.
2. Connect Zoho
Zoho Inventory syncs with ARC in both directions. ARC pushes documents it creates, such as sales orders, invoices, payments and purchase orders, and polls Zoho for changes. A nightly reconciliation compares stock.
Authorize
Connecting uses OAuth, and the consent step happens in a browser, so it is REST and not GraphQL:
GET /zoho/auth-urlreturns the Zoho consent URL for the caller's organization. PassdataCenter, the region your Zoho account lives in:us,eu,in,au,jp,ca,saorcn. It is required. Requiressettings:integration:update.- The user approves in Zoho. Zoho redirects to the callback, which stores the tokens encrypted and starts the setup. A failure redirects back to the Zoho settings page with the reason in the query string.
GET /zoho/status/<organization-id>andDELETE /zoho/disconnect/<organization-id>read and remove the connection. The organization ID must be the caller's own.
Zoho tells ARC which data centre the account is in, and that answer wins over the one the user picked. An unknown data centre is refused rather than defaulted. Disconnecting stops polling, revokes the token and archives the record mappings. Sync history stays.
Read the state
All six reads return a JSON string, so parse it.
query ZohoState {
zohoConnectionStatus
zohoSyncStatus
zohoApiUsage
}
zohoConnectionStatusgives the Zoho organization name, plan tier, currency, whether it is active and when it last synced. When nothing is connected it returns{"connected": false}.zohoSyncStatusgives the count of mapped records per entity type and each entity's polling position, with its last poll, last success and error count.zohoApiUsagegives today's calls, the daily limit and the percent used.zohoSyncLogs(limit, offset, entityType, status)lists sync entries, newest first, at most 100 per request.zohoSyncLogsCountgives the total for a pager.
The API budget
Each organization has a daily Zoho API budget. At 80 percent used a warning is flagged. At 95 percent every further call is refused until the next UTC day, so a push waits and a poll skips. zohoSetApiLimit(limit) sets your own daily cap, for example when you bought more API calls than your plan tier assumes. Zero or no value goes back to the plan tier's limit.
Run it
zohoStartMigration(warehouseIds)starts the one-time backfill from ARC to Zoho: warehouses, contacts, products, opening stock and open sales orders, in that order. It needs a connected, active organization and no migration already running. It returns the checkpoint. PollzohoMigrationStatusfor progress.zohoResumeMigrationcontinues a stopped one from its checkpoint.zohoForcePoll(entityType)queues an immediate poll of one entity type instead of waiting for the schedule.zohoForceReconciliationqueues an immediate reconciliation run.zohoRetryFailedSync(syncLogId)queues a retry of one failed log entry and returns the job name.
mutation Retry($syncLogId: String!) {
zohoRetryFailedSync(syncLogId: $syncLogId)
}
Know what sync will and will not do:
- A change pulled from Zoho never triggers a push back.
- Stock changes pulled from Zoho are recorded in the sync log and are not applied to ARC stock. Reconciliation reports stock differences and never corrects them.
- Reconciliation does soft-delete a product or customer whose Zoho record is gone. It refuses to do so when Zoho returns an empty list, because that usually means the call failed.
- A pulled status never moves a document backwards. A status that the document's transitions do not allow is skipped and logged.
- Tax is not sent to Zoho. A document in a currency that is not set up in Zoho fails or is skipped, and is never booked in Zoho's base currency.
- A revoked Zoho authorization looks like a network failure. Check
zohoSyncLogswhen syncing stops.
3. Set up WhatsApp
ARC sends customer messages, such as order approved, dispatched and delivered, through a WhatsApp provider you connect. The providers are INTERAKT and RESPOND_IO. Each message is sent from an approved template that you map to a trigger, an event ARC can send for. There is no inbound path: ARC does not receive replies.
Each organization has one configuration per provider, and only one provider can be enabled at a time. Trigger and template mappings live on each provider's configuration, so switching providers means mapping the templates again.
Order of calls
upsertWhatsAppConfig(input)saves the configuration.apiKeyis write-only: leave it out or empty to keep the stored key. ForRESPOND_IOalso passchannelId.defaultCountryCodeis added to phone numbers that have none. A new API key clears the verification and turns sending off.whatsAppChannelslists the channels on the provider account for a picker. A provider that has no channels says so, and passing achannelIdto it is refused.testWhatsAppConnectionchecks the key against the provider and records the connection as verified. It sends no message. When it fails it returns the provider's reason.whatsAppTriggerCataloglists the triggers you can switch on, andwhatsAppTemplatesfetches the templates from the provider account, live.updateWhatsAppTrigger(input)switches one trigger on or off and maps it to a template.createWhatsAppTemplate(input)submits a new template to the provider for approval.sendWhatsAppTestMessage(input)sends a real test message to a phone number for one trigger. Use a number that expects it.setWhatsAppEnabled(enabled: true)turns sending on. It is refused until the connection has been verified. Enabling one provider turns the others off.
mutation MapTrigger($input: UpdateWhatsAppTriggerInput!) {
updateWhatsAppTrigger(input: $input) {
provider
isEnabled
}
}
{ "input": { "triggerKey": "<trigger key from the catalog>", "templateName": "<approved template name>", "languageCode": "<language code>", "enabled": true } }
Every operation that takes a provider uses the enabled provider when you leave it out, and otherwise the most recently edited one.
A trigger needs a template before it can be switched on. The template must exist in the provider account and be APPROVED. A template the provider has not approved yet is refused, and so is a name ARC cannot confirm because the provider could not be reached. The trigger key must come from the catalog.
whatsAppMessageLogs(filter) lists the message log, newest first, across every provider, 25 entries by default. A message is only sent when the organization is enabled, the trigger is on and mapped, and a phone number was found for the recipient. When one of the first two fails nothing is sent and nothing is logged. A missing phone number is logged as a failed send.
Reminders
customerReminderEligibility, sendCustomerReminder and sendCustomerReminders send a customer a WhatsApp reminder for an overdue invoice or a quote, through the consent and sending rules. They need communications:message:send and only reach customers in the caller's own book. Each invoice or quote is reminded at most once per cooldown period. A customer who cannot be reminded is skipped with a reason, and the rest are still sent.
4. Configure email
Email setup has two parts. The email configuration is how the organization sends its own mail, such as reports, alerts and notifications. The sender domain is the domain document emails are sent from.
The email configuration
An organization has one configuration.
createEmailConfig(input)creates it. Giveprovider,fromEmail,fromName,isEnabled, and the credentials for the provider. Creating a second one is refused. Passwords and keys are stored encrypted and never returned.emailConfigreportshasApiKeyandhasSmtpPasswordinstead.testEmailConfig(input)sends a test message to the address you give, through the configuration, and reports whether it worked.updateEmailConfig(input)changes it. Whenever the provider, host or port changes, the connection settings are checked again.deleteEmailConfigremoves it.
{
"input": {
"provider": "SMTP",
"fromEmail": "no-reply@example.com",
"fromName": "<sender name>",
"smtpHost": "<public mail server>",
"smtpPort": 587,
"smtpUser": "<user name>",
"smtpPassword": "<password>",
"isEnabled": true,
"sendReports": true,
"sendAlerts": true,
"sendNotifications": true
}
}
Only the SMTP and SENDGRID providers can send today. Choosing AWS_SES or MAILGUN is refused with a message that the provider is not yet implemented, and the remaining enum values are refused as an invalid provider. An SMTP host must be a public mail server. A host that cannot be resolved, or that points at a private or reserved address, is refused, and the address is checked again on every connection.
The configuration also holds the default recipients, an email signature and the schedule for reports: dailyReportTime, weeklyReportDay and monthlyReportDate.
The sender domain
Document email, such as quotes, invoices and receipts, goes out from the platform's sending service. To send from your own domain, register it and publish its DNS records.
emailSenderSetupreads the state: whether verification is available on this deployment, your domain and its DNS records, the address mail is sent from now, and why.setEmailSenderDomain(input)registers the domain, replacing an earlier one, and returns the DNS records to publish. A bare domain, an address or a web address are all accepted.- Publish the records at your DNS host.
verifyEmailSenderDomainchecks them with the email provider and returns the updated status:PENDING,VERIFIEDorFAILED. Call it again after DNS changes spread.removeEmailSenderDomainremoves the domain.
sendingFromReason in emailSenderSetup says why mail comes from the address it does: TENANT_DOMAIN (your domain is verified and used), NO_FROM_ADDRESS, NO_VERIFIED_DOMAIN or ADDRESS_NOT_ON_DOMAIN. Until the domain is verified, document mail goes out from the platform's own address.
5. Common errors
| What you see | Why | What to do |
|---|---|---|
| Zoho calls fail with no connection | No Zoho authorization is stored | Run the authorize steps again |
Unknown Zoho data center | The stored data centre is not one ARC knows | Reconnect and choose the right region |
| Zoho calls refused with a daily budget message | 95 percent of the daily API budget is used | Wait for the next UTC day, or raise the cap with zohoSetApiLimit |
| Syncing stopped with no error shown | The Zoho authorization was revoked, or polling failed repeatedly | Read zohoSyncLogs and reconnect |
| WhatsApp cannot be enabled | The connection has not been verified | Run testWhatsAppConnection first |
| A WhatsApp trigger cannot be mapped | The template is missing, not approved, or the provider could not be reached | Pick an approved template from whatsAppTemplates |
does not use channels | A channelId was sent to a provider that has none | Leave channelId out |
| Nothing sends after you rotated a key | A new key turns sending off and clears verification | Test the connection and enable again |
| Creating the email configuration is refused | One already exists | Use updateEmailConfig |
| The SMTP host is refused | It is not a public mail server | Use the provider's public host |
| The provider is not yet implemented | AWS_SES or MAILGUN was chosen | Use SMTP or SENDGRID |
| The sender domain is refused | It is a public mailbox domain, cannot receive mail, or is the platform's own domain | Use a domain your organization owns |
| Sender-domain verification is not available | This deployment has no provider account | Contact your administrator |