# Zendesk

Source: https://developers.swell.is/app-guides/zendesk

The Zendesk app connects your Swell store to Zendesk. Customer accounts become Zendesk users, and each user shows the customer's Swell orders: how many, the total spent, the last order and the most recent ones, with a link to the customer in your Swell dashboard.

The app also opens a Zendesk ticket for the customer when a payment fails, an order is canceled or refunded, or a return is requested, with the order details in an internal note. An action in the dashboard imports the customers you already have.

## How it works

| When | What the app does |
| --- | --- |
| A customer account is created, or its email, name, phone or addresses change | Creates or updates the customer's Zendesk user, if Sync customer accounts is on. |
| An order is placed, paid in full, fully fulfilled, canceled or fully refunded | Updates the Swell user fields on the customer's Zendesk user, if Show orders on Zendesk users is on. Creates the user if it doesn't exist yet. |
| A payment on an order fails | Opens a ticket for the customer, if Payment failed is on. |
| An order is canceled | Opens a ticket for the customer, if Order canceled is on. |
| An order is fully refunded | Opens a ticket for the customer, if Order refunded is on. |
| A return is created | Opens a ticket for the customer, if Return requested is on. |
| A return is updated | Lists the return's ticket on its order, if the return has one and Return requested is on. Zendesk isn't called. |
| You run Create user fields | Adds the five Swell user fields to Zendesk user profiles. |
| You run Import existing customers | Creates or updates a Zendesk user for every customer account, in the background. |

Nothing is sent to Zendesk while **Enabled** is off.

## Before you begin

- A Zendesk account, and the Admin role in it. The app acts as the Zendesk user who owns its OAuth client, and only admins can create the user fields the app fills in.
- A confidential OAuth client in Zendesk. The app gets its access tokens with the client credentials grant, which Zendesk allows for confidential clients only, and asks for the `read` and `write` scopes. You create the client in step 1.
- Your Zendesk subdomain: the `acme` in `acme.zendesk.com`. A custom domain for your help center doesn't work here.
- Zendesk IP restrictions turned off. Swell has no fixed IP addresses to allow, and while restrictions are on, Zendesk refuses the app's calls.

## Set up

Install Zendesk from the Swell App Store, and set it up in your test environment before the live one.

**1. Create an OAuth client**

In Zendesk, signed in as an admin, go to **Admin Center > Apps and integrations > APIs > OAuth clients** and add a client. Set **Client kind** to **Confidential**. If Zendesk asks for a redirect URL, enter any URL, such as your store's address. The app doesn't use it.

Save the client, then copy its **Identifier** and the secret. Zendesk shows the secret only once.

**2. Add it to the app**

In Swell, go to **Apps > Zendesk > Settings**. Enter the **Zendesk subdomain**, the **OAuth client identifier** and the **OAuth client secret**. **Enabled** is on by default.

On the **Customers** tab, choose whether to sync customer accounts and show their orders, and how many orders to show. On the **Tickets** tab, turn on the events that open a ticket, and set the **Priority**, **Ticket tags** and **Group ID**. Click **Save settings**.

**3. Check the connection**

Choose **Actions > Test connection**. The message names the Zendesk user the client belongs to and its role, such as "Connected to acme.zendesk.com as Jamie Rivera (admin).", and lists the Swell user fields that are missing. If the message reports an error, see Troubleshooting below.

**4. Create the user fields**

Choose **Actions > Create user fields**. The message lists the fields it added: Swell orders, Swell total spent, Swell last order, Swell recent orders and Swell customer. Running it again adds only the fields that are missing. Skip this step if you turned off **Show orders on Zendesk users**.

**5. Import your existing customers**

Choose **Actions > Import existing customers**, then **Start import**. The dashboard shows "Import started. Follow it in the Workflows tab of the app." The import runs in the background and creates or updates a Zendesk user for every customer account, with the settings you saved. From then on, new and changed customers are sent as they happen.

**6. Test with a return**

Place a test order as a customer with an email you can check. On the order page, ship it with **Fulfill items**, then create a return with **Return items**. In Zendesk, search for the customer's email. The user has the customer's name and the Swell fields, and a ticket "Return requested for order #…" starts with an internal note about the return, which ends with a link to the order under `/admin/test`. On the order page, the **Zendesk** tab lists the return number and the ticket's link under **Return tickets**.

The logs in Swell show `Zendesk: synced` with the email and the Zendesk user ID, and `Zendesk: created ticket` with the ticket's link.

**7. Go live**

Settings are saved separately for each environment. In the live environment, repeat steps 2 to 5 with the Zendesk account your store uses. If the user fields exist already, **Create user fields** reports "All Swell user fields already exist in Zendesk."

Test customers and tickets go to the Zendesk account you connect, so use a separate Zendesk account, such as a sandbox, for the test environment if you can.

## Using Zendesk

### Customers as Zendesk users

With **Sync customer accounts** on, each customer account becomes a Zendesk user with these details:

- **Name:** the account's name, or the first and last name, or the part of the email before `@`.
- **Email:** the account's email. Zendesk doesn't send new users a verification email.
- **Phone:** the account's phone number, or the phone number of its shipping or billing address, in international format, such as `+15035550142`. Numbers without a country code use the shipping country, or the billing country. Numbers that aren't valid are left out.
- **External ID:** the Swell account ID.

Zendesk finds an existing user by external ID or email, so customers who already have a Zendesk user are updated, not duplicated. Customers who check out as guests have a Swell account too, so they become Zendesk users. The app doesn't change a user's tags.

Account updates are sent only when the email, name, phone or an address changes. The Swell user fields are updated on order events instead.

### Orders on Zendesk users

With **Show orders on Zendesk users** on, the app fills five custom user fields on each user it syncs. Run **Actions > Create user fields** once to add them to Zendesk. Fields that don't exist in Zendesk are skipped, and the user is synced without them.

| User field | Key | Value |
| --- | --- | --- |
| Swell orders | swell_order_count | The customer's number of orders, from their Swell account. |
| Swell total spent | swell_total_spent | The account's total order value, as a number without currency. |
| Swell last order | swell_last_order | The latest order's number, status, total and date, such as #1042 · complete · 120.00 USD · 2026-10-01. |
| Swell recent orders | swell_recent_orders | The latest orders, newest first, one per line in the same format. Recent orders to show sets how many. |
| Swell customer | swell_customer_url | A link to the customer in your Swell dashboard. |

The status is Swell's status value, such as `delivery_pending`, `complete` or `canceled`, and the date is the day the order was created, in UTC.

Links to your Swell dashboard, in **Swell customer** and in ticket notes, open the environment the record is in: `https://<store>.swell.store/admin/...` for live records and `https://<store>.swell.store/admin/test/...` for test records.

The fields are updated when the customer's profile changes, and when one of their orders is placed, paid in full, fully fulfilled, canceled or fully refunded. They need **Sync customer accounts** on too.

### Tickets for order and return events

Each setting on the **Tickets** tab opens a ticket for one event:

| Setting | Opens a ticket when | Subject | Default |
| --- | --- | --- | --- |
| Payment failed | A payment on an order fails. | Payment failed for order #1042 | On |
| Order canceled | An order is canceled. | Order #1042 was canceled | Off |
| Order refunded | An order is fully refunded, or marked refunded. | Order #1042 was refunded | Off |
| Return requested | A return is created for an order. | Return requested for order #1042 | On |

Each ticket has:

- **Requester:** the order's customer, by name and email.
- **Priority:** the **Priority** setting.
- **Tags:** the **Ticket tags**, plus one for the event: `swell_payment_failed`, `swell_canceled`, `swell_refunded` or `swell_return`.
- **Group:** the group in **Group ID**, when it's set.
- **Internal note:** the subject, the customer's name and email, the order's number, total and date, and the items with their quantity and variant. Refund tickets add the refunded total. Return tickets add the return number, reason code and notes, and list the returned items. The note ends with a link to the order in your Swell dashboard.

Each order gets at most one ticket per event, and each return one ticket. A second failed payment on the same order doesn't open another ticket. Tickets open only for events that happen while the setting is on.

### Internal notes and Zendesk triggers

The ticket starts with an internal note, so the app doesn't email the customer. Zendesk triggers that act on new tickets still run, and depending on its conditions, a trigger that notifies the requester can email the customer. To keep a trigger from running on the app's tickets, add the condition **Tags** **Contains none of the following** with `swell`, or with the tag for the event.

The tags also work in your own Zendesk views, triggers and automations, such as a view of `swell_payment_failed` tickets.

### Ticket links on orders and returns

The link to each ticket is saved on the record it's for. Each order gets a **Zendesk** tab with **Payment failed ticket**, **Cancellation ticket**, **Refund ticket** and **Return tickets**. Returns have no page of their own in the dashboard, so a return's ticket link is saved on the return, and **Return tickets** lists the tickets of all the order's returns, oldest first, one per line with the return number and the link, such as `R100001: https://acme.zendesk.com/agent/tickets/52`. Every ticket's note links back to the order.

The list is rebuilt from the order's returns when a return ticket is created and when one of the order's returns is updated, while **Return requested** is on. Rebuilding it doesn't call Zendesk. A return ticket created by an earlier version of the app is listed once its return is updated or another return of the order gets a ticket.

A record that already has a link for an event gets no second ticket for it, so to open a new ticket for the same event, create it in Zendesk.

### Importing existing customers

To create Zendesk users for the customers you already have, go to **Apps > Zendesk**, choose **Actions > Import existing customers**, and click **Start import**.

- The dashboard shows "Import started. Follow it in the Workflows tab of the app.", and the import runs in the background. Runs are listed on the app's **Workflows** tab as Import customers, with their status: Running, Completed, Failed or Canceled. **Started by** shows App function, because the action starts the run through an app function. Click a run to see its steps or cancel it.
- Customers are imported 100 at a time, oldest first, with the Swell user fields when **Show orders on Zendesk users** is on. The settings are read again before each 100. Each 100 goes to Zendesk as one bulk job, and the import waits up to about 2 minutes for it. If Zendesk refuses the bulk job, those customers are sent one by one instead.
- Running the import again updates users without duplicating them.
- The import sends **Swell customer** too, with a link to the customer in the dashboard of the environment you run it in.
- The import sends customers only. Past payments, cancellations, refunds and returns don't open tickets.
- When it finishes, the logs show the totals: created, updated, skipped, failed and still processing. Users still processing were sent to Zendesk, but their job hadn't finished when the import stopped checking.
- If **Enabled** or **Sync customer accounts** is off, a connection setting is empty, or **Zendesk subdomain** isn't a Zendesk subdomain, the run fails before it sends anyone, with the reason. If you turn off **Enabled**, clear a connection setting or change the subdomain to one that isn't valid during an import, it stops at its next step without calling Zendesk and fails with the totals until then. Customers in a bulk job Zendesk hadn't finished count as still processing. If you turn off **Sync customer accounts**, it finishes the 100 customers it's sending, then stops and fails the same way.

**Import existing customers** is in the Actions menu only while **Sync customer accounts** is on, and **Create user fields** only while **Show orders on Zendesk users** is on.

### In the dashboard

| Where | What it's for |
| --- | --- |
| Apps > Zendesk > Settings | The Zendesk subdomain, the OAuth client, Enabled, and the Customers and Tickets tabs. |
| Apps > Zendesk > Actions | Test connection, Create user fields and Import existing customers. Save the settings first: the actions are unavailable while there are unsaved changes. |
| Apps > Zendesk > Workflows | The runs of Import existing customers, with their status, when they started, and how long they took. |
| Orders, Zendesk tab | Links to the order's Payment failed, Cancellation and Refund tickets. Return tickets lists the return number and ticket link of each of the order's returns. |
| Developer > Console > Logs | Each user the app syncs, each ticket it creates, and each error. |

## Settings

Settings are in **Apps > Zendesk > Settings** and are saved separately for each environment.

**Zendesk subdomain**, **OAuth client identifier**, **OAuth client secret** and **Enabled** are at the top. The other settings are on the **Customers** and **Tickets** tabs.

| Setting | Description | Default |
| --- | --- | --- |
| Zendesk subdomain | The first part of your Zendesk address, such as acme for acme.zendesk.com. The full address also works. Required. | None |
| OAuth client identifier | The client's Identifier in Zendesk Admin Center > Apps and integrations > APIs > OAuth clients. Required. | None |
| OAuth client secret | The client's secret, shown once when you save a confidential client in Zendesk. Required. | None |
| Enabled | Turns off everything the app sends to Zendesk without uninstalling it. Test connection still works while it's off. | On |
| Customers > Sync customer accounts | Creates or updates a Zendesk user when a customer account is created or changed. The Swell user fields and Import existing customers need it. | On |
| Customers > Show orders on Zendesk users | Keeps the order count, total spent, last order and recent orders on the user. Create user fields is available only while it's on. | On |
| Customers > Recent orders to show | The number of latest orders in Swell recent orders, from 1 to 10. | 5 |
| Tickets > Payment failed | Opens a ticket when a payment on an order fails. | On |
| Tickets > Order canceled | Opens a ticket when an order is canceled. | Off |
| Tickets > Order refunded | Opens a ticket when an order is fully refunded. | Off |
| Tickets > Return requested | Opens a ticket when a return is created. | On |
| Tickets > Priority | The priority of new tickets: Low, Normal, High or Urgent. | Normal |
| Tickets > Ticket tags | Comma-separated tags added to every ticket, next to one for the event, such as swell_payment_failed. Tags are lowercased, and spaces become underscores. | swell |
| Tickets > Group ID | The ID of the Zendesk group that gets the tickets. When it's empty or not a whole number, the app sets no group. | None |

## Limitations

- Data goes one way, from Swell to Zendesk. Changes made in Zendesk don't reach Swell.
- Tickets open only for the four events in the settings, and only for events that happen while the setting is on.
- Each order gets at most one ticket per event, and each return one ticket.
- **Return tickets** lists the first 100 returns of an order. A return ticket created by an earlier version of the app shows there only after its return is updated or another return of the order gets a ticket.
- Refund tickets open when an order is fully refunded or marked refunded. Partial refunds don't open a ticket.
- The Swell user fields are updated on profile changes and when an order is placed, paid in full, fully fulfilled, canceled or fully refunded. Other changes, such as a partial refund, show at the next of these.
- **Swell recent orders** shows up to 10 orders.
- Phone numbers that aren't valid are left out of the Zendesk user.
- If the test and live environments connect to the same Zendesk account, both send users and tickets to it.
- If Zendesk can't be reached, rate-limits the app or has a server error, Swell runs the function again later, at growing intervals. If it keeps failing for about 4 days, Swell turns off the function and emails the store's admins. Reinstall the app to turn it back on. Other errors, such as a rejected OAuth client, are logged and the change isn't sent again.
- If Zendesk created a ticket but the app didn't get its answer, a retry within 2 hours returns the same ticket. A retry after that can create a second one.

## Troubleshooting

**Test connection** and **Create user fields** show their messages in the dashboard. The import's messages are on its run: open it on the app's **Workflows** tab. The app also logs each user it syncs, each ticket it creates and each error. In the Swell dashboard, the logs are under **Developer > Console > Logs**.

### "Add the … and save the settings first."

A connection setting is empty, and the message names it. Fill it in, click **Save settings**, and try again.

### "… isn't a Zendesk subdomain. …"

**Zendesk subdomain** has a value that isn't a Zendesk subdomain, such as a custom help-center domain. The message starts with the value, and goes on "Enter the first part of your Zendesk address, e.g. acme for acme.zendesk.com; a custom help-center domain can't be used." Until it's fixed, the app sends nothing to Zendesk. Enter only the `acme` of `acme.zendesk.com`, click **Save settings**, and try again.

### "Zendesk rejected the OAuth client identifier or secret. …"

Copy the **Identifier** again from **Admin Center > Apps and integrations > APIs > OAuth clients** in Zendesk, and check that the client's **Client kind** is **Confidential**. Zendesk shows the secret only once, so if you don't have it, add a new client. Paste both, click **Save settings**, and test again.

### "Zendesk does not allow this client to get tokens with its secret. …"

The OAuth client is public. Set its **Client kind** to **Confidential** in Zendesk, or add a confidential client.

### "Zendesk refused … (403 Forbidden). …"

The message names the call Zendesk refused. Check that the OAuth client belongs to a Zendesk admin, and that Zendesk IP restrictions are off. Swell has no fixed IP addresses to allow.

### "No Zendesk account found at … Check the Zendesk subdomain."

Check **Zendesk subdomain**. Enter only the `acme` part of `acme.zendesk.com`.

### "… could not be reached. Check the Zendesk subdomain."

No Zendesk account answers at that address. Check **Zendesk subdomain** for typos.

### "Zendesk did not grant the read and write scopes. …"

The app asks for `read` and `write` access. Check the OAuth client in Zendesk Admin Center, or add a new confidential client.

### "Zendesk request failed, try again later: …"

Zendesk couldn't be reached, rate-limited the app or had a server error. The reason follows the message. Try again in a minute.

### "Zendesk request failed: …"

Zendesk answered with another error. The reason and the refused call follow the message.

### "The OAuth client belongs to a non-admin user, so Create user fields will not work."

Create the OAuth client again while signed in to Zendesk as an admin, and enter its identifier and secret.

### "Zendesk refused to create user fields. The OAuth client must belong to a Zendesk admin."

Create the OAuth client again while signed in to Zendesk as an admin, enter its identifier and secret, and run **Create user fields** again.

### "The Zendesk app is turned off in its settings. Turn it on to create the user fields."

Turn on **Enabled**, click **Save settings**, and run **Create user fields** again.

### Customers don't appear in Zendesk

Check that **Enabled** and **Sync customer accounts** are on, and click **Save settings**. The logs show "Zendesk: missing" followed by the setting when a connection setting is empty, and "isn't a Zendesk subdomain" when **Zendesk subdomain** can't be used. Account updates are sent only when the email, name, phone or an address changes. Customers who existed before you set up the app are added when you run **Import existing customers**.

### The Swell fields on a user are empty

Run **Test connection**: it lists the user fields missing in Zendesk. Run **Create user fields** to add them, and check that **Show orders on Zendesk users** is on. Each user's fields are filled at the customer's next profile change or order event, or when you run the import.

### A Swell link in Zendesk opens your storefront

Users synced and tickets created before version 1.0.4 of the app link to your store's address without `/admin`. A user's **Swell customer** link is corrected at the customer's next profile change or order event, or when you run the import. Ticket notes keep the link they were created with.

### No ticket was created

Check that the event's setting is on in the **Tickets** tab. The logs show "already created" when the order or return has a ticket link for the event, and "skipped, no customer email" when the order's customer has no email. Refund tickets open only for full refunds.

When Zendesk refuses the ticket, the logs show "Zendesk:" with the event and order, then "failed:" and Zendesk's reason. Check the **Group ID** and **Ticket tags** against your Zendesk account.

### The customer got an email about the ticket

The app's tickets start with an internal note, so the email came from a Zendesk trigger. Add the condition **Tags** **Contains none of the following** with `swell` to the trigger. See Internal notes and Zendesk triggers above.

### "Import not started: …"

The run fails without sending anyone, with "Import not started: the Zendesk app is turned off in its settings." when **Enabled** is off, "Import not started: Sync customer accounts is off in the app settings.", "Import not started: add the … in Apps → Zendesk." when a connection setting is empty, or "Import not started:" followed by "… isn't a Zendesk subdomain. …" when **Zendesk subdomain** can't be used. Fix the setting, click **Save settings**, and run the import again.

### "Import stopped: …"

**Enabled** or **Sync customer accounts** was turned off, a connection setting was cleared, or **Zendesk subdomain** was changed to one that isn't valid, while the import was running. The message gives the reason and the totals until then, such as "Import stopped: the app was turned off in its settings. Before stopping: created 100, updated 0, skipped 0, failed 0, still processing 0." Fix the setting and run the import again. Customers already sent are updated, not duplicated.

### "Import failed on page …"

Zendesk refused the import, for example because of a rejected OAuth client or a 403. The message gives the same explanation as **Test connection** and the totals until then. Fix the cause and run the import again.

On temporary errors, such as when Zendesk can't be reached, rate-limits the app or has a server error, each group of 100 customers is tried up to 5 more times before the import fails. Open the run on the **Workflows** tab to see the step that failed.

### The import counts users as still processing

Zendesk was still running the bulk job when the import stopped checking it, after about 2 minutes or because a setting changed. Zendesk finishes the job on its own. Search for a few of the customers in Zendesk to check.

### An action isn't in the Actions menu

**Import existing customers** needs **Sync customer accounts** on, and **Create user fields** needs **Show orders on Zendesk users** on. Turn the setting on and click **Save settings**.

## Uninstalling

Uninstalling the app stops syncing customers, updating the Swell user fields and opening tickets. It removes the app's functions and settings, including the OAuth client secret, the **Zendesk** tab on orders, and the **Test connection**, **Create user fields** and **Import existing customers** actions. Uninstalling applies to the environment you are in, so remove the app from the test and live environments separately.

The ticket links saved on orders and returns are kept, and the order's links show again on its **Zendesk** tab if you reinstall. Users, the Swell user fields with their last values, and tickets stay in Zendesk, and the **Swell customer** fields and ticket notes keep their links to your Swell dashboard.

The app isn't notified when it's uninstalled. If you no longer need the OAuth client, delete it in Zendesk under **Admin Center > Apps and integrations > APIs > OAuth clients**. To remove the Swell user fields, delete them under **Admin Center > People > Configuration > User fields**. To pause the app without uninstalling it, turn off **Enabled**.

## For developers

| Function | Runs on | What it does |
| --- | --- | --- |
| customer-sync | account.created and account.updated | Reads the account and creates or updates the Zendesk user. Skips updates that change none of email, first_name, last_name, name, phone, shipping and billing. |
| order-context | order.submitted, order.paid, order.delivered, order.canceled and order.refunded | Reads the order's account and syncs it with the Swell user fields. Needs sync_customers and order_context. |
| order-tickets | order.payment_failed, order.canceled and order.refunded | Reads the order with its account and items, and creates the event's ticket when its setting is on and the order has no link for it. |
| return-tickets | return.created and return.updated | On return.created, reads the return and its order with the account and items, creates the return ticket, saves its link on the return and rebuilds the order's list of return tickets. On either event, a return that already has a ticket only gets the list rebuilt, without a Zendesk call. |
| test-connection | The Test connection action | Gets a token, reads the client's user and lists the missing user fields. |
| setup-fields | The Create user fields action | Creates the Swell user fields that don't exist yet. |
| start-import | The Import existing customers action | Starts the import-customers workflow with an env parameter: test when the action's Swell-Env header is test, otherwise null. Returns "Import started. Follow it in the Workflows tab of the app." |
| import-customers | Started by start-import | A workflow. Reads accounts 100 at a time in id order, one step per page, each step retried up to 5 times with exponential backoff from 30 seconds. |

The app adds `zendesk_ticket_payment_failed`, `zendesk_ticket_canceled`, `zendesk_ticket_refunded` and `zendesk_ticket_return` to orders and `zendesk_ticket` to returns, under `$app.zendesk`. The ticket fields hold the ticket's agent URL, `https://{subdomain}.zendesk.com/agent/tickets/{id}`. `zendesk_ticket_return` holds one `<return number>: <link>` line for each of the order's returns with a `zendesk_ticket`, from `GET /returns` with `where[order_id]`, sorted by `date_created` and limited to 100. It's rebuilt after each new return ticket, with the new link included even if saving it on the return failed, and on `return.updated`, and written only when it changes. Only the return's `zendesk_ticket` keeps a second ticket from opening for the return. The order fields are shown on the order's **Zendesk** tab, and returns have no tab.

Dashboard links, in `swell_customer_url` and the ticket note, are built from the request's `store.admin_url`, the store's address without `/admin`, or from `https://{store.id}.swell.store` when it's missing, plus `/admin`. Functions in the test environment get the `Swell-Env: test` header, and their links use `/admin/test`. Workflow requests have no headers and no `store.admin_url`, so `start-import` passes the environment as `env` in the run's data, and the import uses it only when `workflow.trigger` is `function`.

Settings are read from `zendesk` in the app settings, with defaults for values not saved yet. The subdomain accepts `acme`, `acme.zendesk.com` or `https://acme.zendesk.com/`. An empty value counts as missing. A value that isn't a single DNS label after that, such as `help.acme.com`, is reported as not a Zendesk subdomain by Test connection, Create user fields and the import, and logged by the event functions, which then send nothing. Tags are lowercased, spaces become underscores, and duplicates are dropped. A Group ID that isn't a positive whole number is ignored. Every function except `test-connection` and `start-import` stops when `enabled` is `false`. `start-import` doesn't read the settings, and the run it starts fails at its first step instead.

Each function run and workflow step gets a new access token with the client credentials grant: `POST https://{subdomain}.zendesk.com/oauth/tokens` with `grant_type: client_credentials`, `client_id`, `client_secret`, `scope: "read write"` and `expires_in: 1800`. There's no refresh token, and the token isn't stored. The workflow's step output holds only flags, counts, IDs and messages, never the secret or a token.

Calls go to `https://{subdomain}.zendesk.com` with a bearer token:

- `POST /api/v2/users/create_or_update` with `external_id` (the account ID), `email`, `name`, `phone` and `skip_verify_email: true`, and `user_fields` when order context is on. Zendesk matches an existing user by `external_id` or email.
- `GET /api/v2/user_fields?page[size]=100` before each sync, following `links.next` for up to 20 pages. Only the Swell fields that exist are sent, and the customer's recent orders are read from Swell only when `swell_last_order` or `swell_recent_orders` exists.
- `POST /api/v2/user_fields` for Create user fields: `swell_order_count` (integer), `swell_total_spent` (decimal), `swell_last_order` (text), `swell_recent_orders` (textarea) and `swell_customer_url` (text).
- `POST /api/v2/tickets` with an `Idempotency-Key` header equal to the ticket's `external_id`, `swell-{event}-{order or return id}`.
- `POST /api/v2/users/create_or_update_many` and `GET /api/v2/job_statuses/{id}` for the import.
- `GET /api/v2/users/me` for Test connection.

Ticket functions check the record for a saved link before getting a token. The note leaves out the order status and timestamps, so a retried request sends the same body, and within Zendesk's 2-hour idempotency window Zendesk returns the ticket it already created. If saving the link fails, the error is logged and the function succeeds, so Swell doesn't retry it. A ticket request looks like this:

```json
{
  "ticket": {
    "subject": "Payment failed for order #1042",
    "comment": {
      "body": "Payment failed for order #1042.\n\nCustomer: Maya Patel <maya.patel@example.com>\nOrder: #1042 · 128.00 USD · 2026-10-01\n\nItems:\n- 1 × Oak Desk Lamp\n\nSwell order: https://alderco.swell.store/admin/orders/6acc9e5f3b1d2a0012f4c771",
      "public": false
    },
    "requester": { "name": "Maya Patel", "email": "maya.patel@example.com" },
    "priority": "normal",
    "tags": ["swell", "swell_payment_failed"],
    "external_id": "swell-payment_failed-6acc9e5f3b1d2a0012f4c771"
  }
}
```

Phone numbers are parsed with libphonenumber-js's `parsePhoneNumberWithError`, using the account's `phone`, then `shipping.phone`, then `billing.phone`, with `shipping.country` or `billing.country` as the default country. Numbers that fail to parse or `isValid()` are dropped, and the rest are sent as E.164.

The import reads accounts sorted by `id`, with an `id` `$gt` cursor, and reads recent orders for 10 accounts at a time. Each page is one `create_or_update_many` job, Zendesk's limit of 100 users. On a 400, 403 or 422 response, the page is sent with `create_or_update`, 3 users at a time, counting 201 as created, 200 as updated and rejected users as failed. After each job, the workflow sleeps 10 seconds and checks the job status, up to 12 times. Each page and each check reads the settings first, and the run stops without a Zendesk call when `enabled` is `false`, a connection setting is missing or the subdomain isn't valid, and before a page when `sync_customers` is `false`. Users of a job still running count as `pending`, and users a finished job doesn't report on count as `failed`.

Page and job steps are retried on network errors, 429, 5xx and Swell API errors. Other Zendesk errors are returned instead of thrown, so the run fails right away. The workflow returns `created`, `updated`, `skipped`, `failed` and `pending`. The import sends `swell_customer_url` like the event functions, built from `store.id`.

In the event functions, network errors, 429 and 5xx responses are rethrown, so Swell retries the function. Other errors are logged with `console.error`, with the refused call, and the function succeeds.

## Support

Swell builds and maintains the Zendesk app. For help with the app, email support@swell.is. For questions about your Zendesk account, users, triggers or tickets, contact Zendesk.
