Skip to main content

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​

JobOperationsPermission
Read Zoho statezohoConnectionStatus, zohoSyncStatus, zohoApiUsage, zohoSyncLogs, zohoSyncLogsCount, zohoMigrationStatussettings:integration:read
Run ZohozohoStartMigration, zohoResumeMigration, zohoForcePoll, zohoForceReconciliation, zohoSetApiLimit, zohoRetryFailedSyncsettings:integration:update
Read WhatsApp setupwhatsAppConfig, whatsAppTriggerCatalog, whatsAppTemplates, whatsAppChannels, whatsAppMessageLogssettings:integration:read
Set up WhatsAppupsertWhatsAppConfig, testWhatsAppConnection, updateWhatsAppTrigger, setWhatsAppEnabled, createWhatsAppTemplate, sendWhatsAppTestMessagesettings:integration:update
Read email setupemailConfigsettings:email:read
Set up emailcreateEmailConfig, updateEmailConfig, deleteEmailConfig, testEmailConfig, emailSenderSetup, setEmailSenderDomain, verifyEmailSenderDomain, removeEmailSenderDomainsettings: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:

  1. GET /zoho/auth-url returns the Zoho consent URL for the caller's organization. Pass dataCenter, the region your Zoho account lives in: us, eu, in, au, jp, ca, sa or cn. It is required. Requires settings:integration:update.
  2. 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.
  3. GET /zoho/status/<organization-id> and DELETE /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
}
  • zohoConnectionStatus gives the Zoho organization name, plan tier, currency, whether it is active and when it last synced. When nothing is connected it returns {"connected": false}.
  • zohoSyncStatus gives the count of mapped records per entity type and each entity's polling position, with its last poll, last success and error count.
  • zohoApiUsage gives 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. zohoSyncLogsCount gives 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. Poll zohoMigrationStatus for progress. zohoResumeMigration continues a stopped one from its checkpoint.
  • zohoForcePoll(entityType) queues an immediate poll of one entity type instead of waiting for the schedule.
  • zohoForceReconciliation queues 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 zohoSyncLogs when 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​

  1. upsertWhatsAppConfig(input) saves the configuration. apiKey is write-only: leave it out or empty to keep the stored key. For RESPOND_IO also pass channelId. defaultCountryCode is added to phone numbers that have none. A new API key clears the verification and turns sending off.
  2. whatsAppChannels lists the channels on the provider account for a picker. A provider that has no channels says so, and passing a channelId to it is refused.
  3. testWhatsAppConnection checks the key against the provider and records the connection as verified. It sends no message. When it fails it returns the provider's reason.
  4. whatsAppTriggerCatalog lists the triggers you can switch on, and whatsAppTemplates fetches the templates from the provider account, live.
  5. 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.
  6. sendWhatsAppTestMessage(input) sends a real test message to a phone number for one trigger. Use a number that expects it.
  7. 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.

  1. createEmailConfig(input) creates it. Give provider, fromEmail, fromName, isEnabled, and the credentials for the provider. Creating a second one is refused. Passwords and keys are stored encrypted and never returned. emailConfig reports hasApiKey and hasSmtpPassword instead.
  2. testEmailConfig(input) sends a test message to the address you give, through the configuration, and reports whether it worked.
  3. updateEmailConfig(input) changes it. Whenever the provider, host or port changes, the connection settings are checked again.
  4. deleteEmailConfig removes 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.

  1. emailSenderSetup reads the state: whether verification is available on this deployment, your domain and its DNS records, the address mail is sent from now, and why.
  2. 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.
  3. Publish the records at your DNS host.
  4. verifyEmailSenderDomain checks them with the email provider and returns the updated status: PENDING, VERIFIED or FAILED. Call it again after DNS changes spread.
  5. removeEmailSenderDomain removes 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 seeWhyWhat to do
Zoho calls fail with no connectionNo Zoho authorization is storedRun the authorize steps again
Unknown Zoho data centerThe stored data centre is not one ARC knowsReconnect and choose the right region
Zoho calls refused with a daily budget message95 percent of the daily API budget is usedWait for the next UTC day, or raise the cap with zohoSetApiLimit
Syncing stopped with no error shownThe Zoho authorization was revoked, or polling failed repeatedlyRead zohoSyncLogs and reconnect
WhatsApp cannot be enabledThe connection has not been verifiedRun testWhatsAppConnection first
A WhatsApp trigger cannot be mappedThe template is missing, not approved, or the provider could not be reachedPick an approved template from whatsAppTemplates
does not use channelsA channelId was sent to a provider that has noneLeave channelId out
Nothing sends after you rotated a keyA new key turns sending off and clears verificationTest the connection and enable again
Creating the email configuration is refusedOne already existsUse updateEmailConfig
The SMTP host is refusedIt is not a public mail serverUse the provider's public host
The provider is not yet implementedAWS_SES or MAILGUN was chosenUse SMTP or SENDGRID
The sender domain is refusedIt is a public mailbox domain, cannot receive mail, or is the platform's own domainUse a domain your organization owns
Sender-domain verification is not availableThis deployment has no provider accountContact your administrator