# README

This space contains the legal documentation for the Tern Trade-in App, including our Privacy Policy, Data Processing Agreement, and Storefront Privacy Notice.

For support, contact us at <support@tern.eco>.


# Privacy Policy (Merchants)

*Last updated: March 2026*

This Privacy Policy is addressed to **merchants** who install and use the Tern Trade-in App ("the App"). It describes how Tern Circular Ltd collects, uses, and shares personal information about merchants in the operation of the App, and explains Tern's role as a data processor in relation to your customers' personal data.

If you are a customer using a trade-in service on a retailer's website, please refer to the [Storefront Privacy Notice](/v1/legal/privacy-policy-storefront) and the retailer's own privacy policy instead.

The App comprises two interfaces:

* **The Admin**: A cloud-hosted merchant dashboard, operated by Tern Circular Ltd, used by merchants to manage trade-in programmes and view trade-in data.
* **The Storefront**: A JavaScript module that can be embedded in any website — including a Shopify Online Store — to provide your customers with a trade-in interface.

### **Personal Information the App Collects**

#### For All Merchants

Personal information is collected in two distinct contexts:

**Your data (as merchant — Tern Circular Ltd is the data controller):**

* Your name, email address, and business details, collected when you register for and access the Admin.
* Technical information from your use of the Admin, including IP addresses and browser details, retained in server logs for security and troubleshooting purposes.

**Your customers' data (Tern Circular Ltd is data processor on your behalf):**

Via the Storefront, we process personal information about your customers in order to operate the trade-in service. This includes names, email addresses, phone numbers, postal addresses, order details, product information, and condition assessments submitted during a trade-in. This processing is carried out under your instructions as data controller and is governed by our [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement).

We collect personal information directly from the relevant individual, through your Shopify account (where applicable), or using the following technologies:

"**Cookies and local storage**" — For customer sessions, we store details about your current trade-in session in your browser's local storage rather than in cookies. This data is used only to maintain session state during a trade-in and is not used for advertising or tracking purposes. Other session-related cookies may be set by third-party services embedded within the merchant's storefront. For more information about cookies, and how to disable them, visit <http://www.allaboutcookies.org>.

"**Log files**" — We collect server log data including IP addresses, browser type, Internet service provider, referring/exit pages, and date/time stamps, for security and troubleshooting purposes.

"**Analytics events**" — We send limited service usage events (such as the fact that a trade-in has been completed) to Google Analytics from our backend infrastructure. These are server-side events sent from our systems (they do not place Google Analytics cookies as part of the Tern Storefront widget). We do not send direct customer identifiers (such as names, email addresses, or postal addresses) in these events; however, events may include pseudonymous identifiers and internal transaction references (such as trade-in or order identifiers) for measurement and debugging.

Additionally, where a merchant has independently installed Google Analytics on their storefront — whether on Shopify or any other platform — GA may also fire in the context of that merchant's existing configuration when a customer interacts with the Storefront (including the setting of cookies by the merchant's own analytics implementation). This is governed by the merchant's own privacy policy and their relationship with Google, not by Tern Circular Ltd.

#### For Shopify Merchants (only)

Tern Circular Ltd is a registered Shopify Partner and the App is distributed via the Shopify App Store. When you install the App, you will be presented with Shopify's standard OAuth authorisation flow, which clearly lists the categories of store data the App is requesting access to. By installing the App and completing the OAuth authorisation flow, you authorise that access so we can provide and operate the App for you in accordance with this Privacy Policy, our agreement with you, and (where applicable) our [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement). This process is also governed by [Shopify's Partner API Terms](https://www.shopify.com/partners/terms).

The categories of Shopify store data the App may access include: merchant account details; product and inventory data; order history and fulfilment information; customer records; online store configuration; and discounts. The specific API permissions requested are displayed in full during the installation flow.

**How we use Shopify data — two distinct stages:**

* **At installation**

  When you install the App, we retrieve your store's order and transaction history in order to support trade-in valuation under your configured rules and to match returning customers to their previous purchases. At this stage, we do not retrieve or store personally identifiable customer information (such as names, email addresses, or addresses). Only non-identifying transactional data is imported.
* **When a customer uses the Storefront**

  When one of your customers visits the trade-in Storefront and initiates a trade-in, we access the relevant customer record and order details from Shopify in order to facilitate that transaction. Personally identifiable information (such as name, email address, phone number, and postal address) is only retrieved and stored if the customer actively begins a trade-in. It is not collected on a bulk or speculative basis.

### **How Do We Use Your Personal Information?**

The following table describes how we use the personal information we hold about **you as a merchant**. Processing of your customers' personal data is addressed separately in our [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement).

| Purpose                                                                                                             | Lawful Basis                                  |
| ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| To operate the App and maintain your merchant account (onboarding, account management, transactional emails to you) | Performance of contract with the merchant     |
| To communicate with you about your account, support requests, or operational matters                                | Performance of contract; legitimate interests |
| To improve and develop the App (using aggregated service analytics, bug fixing, product development)                | Legitimate interests                          |
| To comply with legal obligations                                                                                    | Legal obligation                              |

Where we rely on **legitimate interests**, we have assessed that those interests are not overridden by your rights and freedoms. You have the right to object to processing based on legitimate interests — see your rights below.

To the extent that Tern Circular Ltd processes personal information of your customers as a "data processor" or "service provider" under applicable data protection laws, including the EU or UK General Data Protection Regulation and applicable US state privacy laws (including the California Consumer Privacy Act), this is subject to our [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement). In that context, the merchant is the data controller and is responsible for:

* ensuring a valid lawful basis exists for processing their customers' personal data;
* providing customers with an appropriate privacy notice at the point of data collection (typically via the merchant's own privacy policy and disclosures on their website); and
* responding to any data subject requests made by their customers.

Where we process end-customer personal data on your behalf, we do so as a data processor or service provider under the [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement). The merchant remains responsible for providing customer-facing privacy information and for responding to end-customer rights requests as data controller. Tern Circular Ltd will assist the merchant as required by the Data Processing Agreement and applicable law.

### **Sharing Your Personal Information**

We share personal information with a number of third-party subprocessors in order to provide the Service and operate the App. This includes both your merchant account data and, where applicable in our role as your data processor, your customers' personal data processed under the [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement). A full list of our subprocessors is available on our [Third Party Subprocessors](/v1/legal/privacy-policy-merchants/third-party-subprocessors) page. These include:

* **Amazon Web Services (AWS)**

  Who host our app and provide the resources for storing data collected through the App.
* **Google LLC**

  Who provide additional cloud hosting infrastructure.
* **Amazon SES**

  Who provide email transmission services for transactional emails sent by the App.
* **Cloudflare**

  Who provide load balancing and DDoS protection services.
* **Popout, Inc. DBA Shippo** and **Auctane, Inc. DBA ShipEngine**

  Who provide trade-in fulfilment and logistics services.
* **Stripe Payments UK, Ltd.**

  Who provide payment processing services.

Finally, we may also share your personal information to comply with applicable laws and regulations, to respond to a subpoena, search warrant or other lawful request for information we receive, or to otherwise protect our rights.

### **Your Rights as a Merchant**

This section describes your rights in relation to the personal information that Tern Circular Ltd holds about **you as a merchant** (for example, your name, email address, and account details). It does not cover the rights of your customers in relation to the trade-in data you control — those rights are exercised through your own privacy policy and are your responsibility as data controller, as set out in the [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement).

#### UK and European Economic Area Residents

If you are a resident of the United Kingdom or European Economic Area, you have the following rights under the UK GDPR, EU GDPR, or equivalent applicable law:

* **Right of access** — You may request a copy of the personal information we hold about you.
* **Right to rectification** — You may ask us to correct inaccurate or incomplete personal information.
* **Right to erasure** — You may ask us to delete your personal information in certain circumstances.
* **Right to restrict processing** — You may ask us to pause processing of your personal information in certain circumstances, for example while accuracy is disputed.
* **Right to data portability** — Where processing is based on your consent or a contract, you may request your data in a structured, machine-readable format.
* **Right to object** — Where we rely on legitimate interests as our lawful basis, you have the right to object to that processing. We will cease processing unless we can demonstrate compelling legitimate grounds that override your interests.
* **Right to withdraw consent** — Where processing is based on consent, you may withdraw that consent at any time without affecting the lawfulness of prior processing.
* **Right to complain** — You have the right to lodge a complaint with a supervisory authority. In the UK, this is the Information Commissioner's Office (ICO) at [ico.org.uk](https://ico.org.uk). If you are an EU resident, you may contact your local data protection authority.

To exercise any of these rights, please contact us using the details in the Contact Us section below. We will respond within one month of receiving a valid request.

**International transfers:** Where your personal information is transferred outside of the UK or EEA — including to the United States — we ensure appropriate safeguards are in place in accordance with UK GDPR, including the use of International Data Transfer Agreements (IDTAs) or the UK Addendum to the EU Standard Contractual Clauses, as approved by the ICO. Further details are set out in our [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement).

#### US State Residents (Including California)

This section applies to you if you are an **individual or sole-trader merchant** residing in California or another US state with applicable consumer privacy legislation. Corporate entities generally fall outside the scope of CCPA/CPRA as data subjects; however, if you are an individual merchant, you may have the following rights under the California Consumer Privacy Act (CCPA), California Privacy Rights Act (CPRA), or equivalent state law:

* **Right to know** — You may request details of the personal information we have collected about you, the sources from which it was collected, the purposes for which it is used, and the third parties with whom it is shared.
* **Right to deletion** — You may request that we delete personal information we have collected from you, subject to certain exceptions.
* **Right to correct** — You may request that we correct inaccurate personal information.
* **Right to opt out of sale or sharing** — We do not sell personal information, nor do we share it for cross-context behavioural advertising purposes.
* **Right to non-discrimination** — We will not discriminate against you for exercising any of these rights. You will not receive a different level of service or be charged different prices as a result of making a rights request.

To submit a request, please contact us by email at <contact@tern.eco> or via our contact form at [tern.eco/contact](https://tern.eco/contact). We will acknowledge your request within 10 business days and respond in full within 45 days. If we require additional time, we will notify you of the extension and the reason for it.

### **Age Restrictions**

The App and its Services are directed solely at businesses (merchants) and are not intended for use by individuals under the age of 16 (children). We do not knowingly collect personal information from children. If you believe that a child has provided personal information through the App without appropriate consent, please contact us at <contact@tern.eco> and we will take steps to delete that information promptly. Merchants are responsible for ensuring their use of the Storefront complies with applicable laws relating to children's data in their own jurisdiction.

### **Automated Decision-Making**

Tern Circular Ltd does not set trade-in pricing, make valuation decisions, or determine the criteria used to generate trade-in offers. Any offer presented to an end customer is defined and directed by the merchant, and the App only applies the merchant's configured rules and eligibility criteria.

### **Data Retention**

We retain different categories of data for different periods, in accordance with the principle of storage limitation:

| Data Type                                                                       | Retention Period                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Server and access logs                                                          | 14 days, after which they are automatically deleted                                                                                                                                                                                                                                                                                                  |
| Database backups                                                                | Retained on a rolling basis (daily backups for 7 days, weekly backups for 1 month, and monthly backups for 3 months), after which they are automatically purged                                                                                                                                                                                      |
| Customer personal data held in the database (names, email addresses, addresses) | Retained while the merchant's account is active and the relevant trade-in is being processed. If we receive a valid erasure request, we will remove direct identifiers from the trade-in record unless retention is required by law or needed to establish, exercise, or defend legal claims.                                                        |
| Trade-in submission records                                                     | Retained for up to 7 years from the date of submission to support merchant reporting, dispute resolution, and regulatory compliance. If the merchant's account is closed or the App is uninstalled, we remove direct identifiers and retain the remaining trade-in record in a de-identified form. After 7 years, records are deleted or anonymised. |
| Merchant account data (name, email address)                                     | Retained for the duration of the merchant's active account and deleted upon account closure, subject to any legal obligation to retain business records                                                                                                                                                                                              |

You may request deletion of your personal information at any time by contacting us using the details in the Contact Us section. Where we are required or permitted to retain a record (for example, for dispute resolution or legal compliance), we will remove direct identifiers where appropriate and retain only a de-identified record. We will respond to valid requests within one month.

### **Changes**

We may update this privacy policy from time to time to reflect changes to our practices or for operational, legal, or regulatory reasons. Where changes are material, we will notify merchants by email to the address associated with their account prior to the changes taking effect. The "Last Updated" date at the top of this page will always reflect when the policy was most recently revised. We encourage you to review this policy periodically.

### **Contact Us**

For more information about our privacy practices, if you have questions, or if you would like to make a complaint or exercise your data rights, please contact us by email at <contact@tern.eco> or by mail using the details provided below:

Tern Circular Ltd\
159 High Street,\
Barnet,\
United Kingdom,\
EN5 5SU


# Data Processing Agreement

*Last updated: March 2026*

**DATA PROCESSING AGREEMENT**

This Data Processing Agreement ("DPA") is entered into between:

1. **Users of the Tern Trade-in app** (the "Merchant" or "Data Controller"), who accept this DPA by installing the App from the Shopify App Store or by accessing or using the Services in any capacity. By doing so, the Merchant represents that they have the authority to bind the organisation on whose behalf they are acting to this DPA.
2. **Tern Circular Ltd** ("Data Processor"), a company registered in the United Kingdom of Great Britain and Northern Ireland, with its registered office at 159 High Street, Barnet, EN5 5SU, UK.

Collectively referred to as the "Parties."

> **Note for merchants:** This DPA is currently accepted through your installation and use of the App. Tern Circular Ltd recommends that enterprise merchants or those requiring a countersigned DPA contact <contact@tern.eco> to arrange a formally executed version.

WHEREAS:

1. The Data Controller has accepted this DPA by installing or using the Tern Trade-in App, and in doing so assumes the responsibilities of the Data Controller as outlined herein.
2. The Data Controller, as part of its business operations, may disclose certain Personal Data to the Data Processor for the purposes of processing as outlined in this agreement.
3. The Data Processor agrees to process Personal Data on behalf of the Data Controller in accordance with the Data Controller's instructions and in compliance with applicable data protection laws and regulations.
4. The Data Controller and Data Processor desire to outline their respective rights and obligations with respect to the processing of Personal Data in compliance with the UK General Data Protection Regulation (UK GDPR), as retained in UK law by the European Union (Withdrawal) Act 2018, the Data Protection Act 2018, the EU General Data Protection Regulation (EU GDPR) (Regulation (EU) 2016/679) where applicable, and any other applicable data protection laws in jurisdictions where the Data Controller operates.

NOW, THEREFORE, the Parties agree as follows:

**1. Definitions**

1.1. "Data Protection Laws" means all applicable laws and regulations relating to the processing of Personal Data, including but not limited to the UK General Data Protection Regulation (UK GDPR), the Data Protection Act 2018, the EU General Data Protection Regulation (EU GDPR) (Regulation (EU) 2016/679), and any other applicable national or international data protection laws.

1.2. "Personal Data" means any information relating to an identified or identifiable natural person that is processed by the Data Processor on behalf of the Data Controller in connection with the Services.

1.3. "Personal Data Breach" means a breach of security leading to the accidental or unlawful destruction, loss, alteration, unauthorised disclosure of, or access to, Personal Data transmitted, stored or otherwise processed.

1.4. "Services" means the services provided by the Data Processor to the Data Controller as described in a separate agreement or statement of work.

**2. Scope and Purpose**

2.1. The Data Controller appoints the Data Processor to process Personal Data on behalf of the Data Controller for the purpose of providing the Services.

2.2. The Data Processor agrees to process the Personal Data only in accordance with the Data Controller's documented instructions and for the purposes defined in this DPA, unless required to do otherwise by applicable laws.

**3. Data Processor's Obligations**

3.1. Compliance with Data Protection Laws: The Data Processor shall process Personal Data in compliance with all applicable Data Protection Laws.

3.2. Confidentiality: The Data Processor shall ensure that any person authorised to process the Personal Data on its behalf is under appropriate obligations of confidentiality.

3.3. Security Measures: The Data Processor shall implement appropriate technical and organisational measures to protect the Personal Data from unauthorised access, accidental loss, destruction, alteration, or disclosure. Upon reasonable request, the Data Processor shall provide the Data Controller with a high-level summary of such measures.

3.4. Sub-processing: In the course of providing the Services, the Data Controller acknowledges and hereby grants the Data Processor general written authorisation to use Subprocessors, listed online at: [Tern Circular Ltd's Subprocessors](/v1/legal/privacy-policy-merchants/third-party-subprocessors) ("Subprocessor List"), to Process the Personal Data. The Data Processor shall notify the Data Controller of any intended changes to the Subprocessor List — including the addition of new subprocessors or the replacement of existing ones — by updating the Subprocessor List and providing notice by email at least 14 days prior to such changes taking effect. The Data Controller may object to any such change on reasonable data protection grounds by notifying the Data Processor in writing within 14 days of receiving notice. If the parties cannot reach a resolution, either Party may terminate the relevant Services on written notice without penalty.

The Data Processor shall ensure that any Subprocessor it appoints is engaged under a written contract that imposes data protection obligations on the Subprocessor that are no less protective than those set out in this DPA. The Data Processor shall remain fully liable to the Data Controller for the performance of the Subprocessor's obligations.

3.5. Data Subject Requests: The Data Processor shall assist the Data Controller in responding to data subject requests and fulfil the Data Controller's obligations under applicable Data Protection Laws, including by providing the Data Controller with such information as is reasonably required to enable a complete and timely response.

3.6. Personal Data Breach Notification: In the event that the Data Processor becomes aware of a Personal Data Breach, the Data Processor shall:

(a) notify the Data Controller without undue delay, and in any event within 48 hours of becoming aware of the breach, to allow the Data Controller sufficient time to meet its own notification obligations under applicable Data Protection Laws (including the 72-hour deadline to notify the relevant supervisory authority under UK GDPR and EU GDPR);

(b) provide the Data Controller with sufficient information to allow it to meet any obligations to report or inform data subjects of the breach, including: the nature of the breach; the categories and approximate number of data subjects and Personal Data records concerned; the likely consequences of the breach; and the measures taken or proposed to address the breach.

(c) cooperate with the Data Controller and take such reasonable steps as are directed by the Data Controller to assist in the investigation, mitigation, and remediation of each such breach.

3.7. Audit Rights: The Data Processor shall, on reasonable prior written notice (no less than 30 days except in the case of a reasonably suspected breach), make available to the Data Controller all information necessary to demonstrate compliance with the obligations set out in this DPA, and shall allow for and contribute to audits and inspections conducted by the Data Controller or an independent auditor appointed by the Data Controller. Such audits shall be conducted during normal business hours, shall not unreasonably disrupt the Data Processor's operations, and shall be subject to any reasonable confidentiality requirements of the Data Processor. The Data Controller shall bear the costs of any such audit unless the audit reveals a material breach of this DPA, in which case costs shall be borne by the Data Processor.

3.8. Unlawful Instructions: The Data Processor shall promptly inform the Data Controller if, in its reasonable opinion, any instruction from the Data Controller infringes applicable Data Protection Laws.

3.9. DPIAs and Prior Consultation: Taking into account the nature of the processing and the information available to the Data Processor, the Data Processor shall provide reasonable assistance to the Data Controller with data protection impact assessments and, where applicable, consultations with supervisory authorities, in each case to the extent required under applicable Data Protection Laws.

**4. Data Controller's Obligations**

4.1. Lawful Basis for Processing: The Data Controller shall ensure that it has a valid lawful basis for the processing of Personal Data and shall provide the necessary information to the Data Processor to fulfil its obligations under this DPA.

4.2. Data Subject Requests: The Data Controller shall be responsible for responding to any data subject requests concerning the exercise of data subjects' rights under applicable Data Protection Laws.

4.3. Instructions: The Data Controller shall provide the Data Processor with clear and documented instructions for the processing of Personal Data in connection with the Services.

**5. International Transfers**

5.1. **Transfers from the UK:** The Data Processor may transfer Personal Data to countries or territories outside the United Kingdom that are not subject to UK adequacy regulations, provided that such transfers are subject to appropriate safeguards in accordance with the UK GDPR and the Data Protection Act 2018. Such safeguards shall include, but are not limited to, the use of International Data Transfer Agreements (IDTAs) or the UK Addendum to the EU Standard Contractual Clauses, as approved by the Information Commissioner's Office (ICO).

5.2. **Transfers from the EEA:** Where the Data Controller is established in the European Economic Area and Personal Data originating in the EEA is transferred to a country not subject to an EU adequacy decision, the Data Processor shall ensure such transfers are subject to appropriate safeguards under the EU GDPR, including the use of Standard Contractual Clauses (SCCs) as approved by the European Commission.

5.3. The Data Processor shall maintain an up-to-date list of its subprocessors and the countries to which Personal Data is transferred, available at the [Third Party Subprocessors](/v1/legal/privacy-policy-merchants/third-party-subprocessors) page.

**6. Term and Termination**

6.1. This DPA shall remain in effect until the completion of the Services or until terminated by either Party in accordance with the terms of the main agreement between the Parties.

6.2. Upon termination or completion of the Services, the Data Processor shall, at the Data Controller's option, delete, anonymise, or return all Personal Data, unless otherwise required by applicable law.

6.3. For the avoidance of doubt, this Section 6 does not require the deletion of information that has been irreversibly anonymised such that it no longer constitutes Personal Data.

**7. Governing Law and Jurisdiction**

7.1. This DPA shall be governed by and construed in accordance with the laws of the United Kingdom of Great Britain and Northern Ireland. Any dispute arising out of or in connection with this DPA shall be subject to the exclusive jurisdiction of the courts of the United Kingdom of Great Britain and Northern Ireland.

For any further questions, please reach out to us at <contact@tern.eco>

***

## Annex 1 — Details of Processing

This Annex forms part of the Data Processing Agreement and sets out the details of processing carried out by Tern Circular Ltd as Data Processor on behalf of the Merchant as Data Controller, as required by Article 28 of the UK GDPR and EU GDPR.

### Subject Matter

The provision of the Tern Trade-in App and associated services, enabling merchants to operate product trade-in programmes for their customers.

### Duration of Processing

For the duration of the Merchant's use of the Services, and for such period thereafter as is necessary to fulfil legal obligations or as directed by the Data Controller, subject to the termination provisions in Section 6 of this DPA.

### Nature and Purpose of Processing

The Data Processor processes Personal Data for the following purposes:

* Authenticating customers accessing the trade-in Storefront
* Retrieving and displaying relevant order history to facilitate trade-in eligibility checks
* Recording and managing trade-in submissions made by customers
* Communicating with customers regarding the status of their trade-in via transactional email
* Facilitating fulfilment and logistics in respect of trade-in collections where applicable
* Facilitating payment processing in respect of trade-in payouts where applicable
* Enabling the Merchant to review, manage, and respond to trade-in requests via the Admin

Processing operations include: collection, recording, storage, retrieval, use, disclosure by transmission, and deletion.

### Types of Personal Data

The following categories of Personal Data may be processed:

* **Customer identifying information**: name, email address, phone number, postal address
* **Order and transaction data**: order identifiers, product details, purchase history (non-PII elements collected at installation; PII elements only upon active trade-in initiation)
* **Trade-in submission data**: product condition descriptions, images, and any additional information submitted by the customer as part of the trade-in process
* **Technical data**: IP addresses, browser type, session identifiers (held in server logs; not linked to individual customer profiles)
* **Payment data**: processed by Stripe on behalf of Tern Circular Ltd; Tern Circular Ltd does not store full payment card details

### Categories of Data Subjects

* The Merchant's customers who access the trade-in Storefront and initiate a trade-in
* The Merchant's staff who access the Admin dashboard

### Special Categories of Personal Data

None. The Services are not designed to process special categories of Personal Data as defined under UK GDPR Article 9 or EU GDPR Article 9. Merchants must not submit or permit submission of special category data through the Services.

### Competent Supervisory Authority

* **UK:** Information Commissioner's Office (ICO), [ico.org.uk](https://ico.org.uk)
* **EU:** The supervisory authority of the EU member state in which the Data Controller is established, or the lead supervisory authority determined in accordance with EU GDPR Article 56 where applicable.


# Third Party Subprocessors

*Last updated: March 2026*

Tern Circular Ltd uses third-party subprocessors in order to provide our services. Core subprocessors are those that we can't offer our service without. Additional subprocessors might apply if additional services are used. Tern Circular Ltd engages these third-party subprocessors in accordance with our [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement). Merchants will be notified of any changes to this list in accordance with our Data Processing Agreement.

**Data residency note:** Our primary application hosting and data storage are configured to operate from the United Kingdom (London region) at the time of writing. However, some third parties may process or route data internationally (for example, via global network delivery, email routing, analytics processing, logistics providers, payment networks, or support operations). The table below summarises primary processing locations and where processing/transfers may occur.

### Core third-party subprocessors

| Subprocessor                       | Service Provided                   | Primary Processing Location(s) | May Process / Transfer To                                                                                      | Data Processed                                                                                 |
| ---------------------------------- | ---------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Amazon Web Services (AWS)          | Cloud hosting                      | UK (London region)             | UK; potentially other countries depending on service configuration and support operations                      | All platform data                                                                              |
| Google Cloud Platform (Google LLC) | Cloud hosting                      | UK (London region)             | UK; potentially other countries depending on service configuration and support operations                      | All platform data                                                                              |
| Amazon Simple Email Service (SES)  | Email transmission                 | UK/EEA (service-dependent)     | USA and other countries depending on email routing and service operations                                      | Personal data necessary to provide transactional emails                                        |
| Cloudflare                         | Load balancing and DDoS protection | Global edge network            | Global                                                                                                         | Network traffic and related technical data necessary to provide security and delivery services |
| Google Analytics (Google LLC)      | Service analytics                  | USA and other countries        | USA and other countries                                                                                        | Service usage and conversion events (configured to avoid sending direct customer identifiers)  |
| Popout, Inc. DBA Shippo            | Fulfillment services               | USA                            | USA and other countries                                                                                        | Personal data necessary to provide shipping and fulfillment services                           |
| Auctane, Inc. DBA ShipEngine       | Fulfillment services               | USA                            | USA and other countries                                                                                        | Personal data necessary to provide shipping and fulfillment services                           |
| Stripe Payments UK, Ltd.           | Payment processing                 | UK / Ireland                   | UK/EEA; and other countries as necessary for payment processing (including card networks and banking partners) | Personal data necessary to provide payment processing services                                 |


# Storefront Privacy Notice (Customers)

*Last updated: March 2026*

This notice explains how your personal information is handled when you use the Tern Trade-in Service (the "Service") on a retailer's website.

## Who is responsible for your data?

The retailer whose website you are using (the "Merchant") is the **data controller** for your personal information. This means the Merchant is legally responsible for how your data is collected, used, and protected in connection with your trade-in.

Tern Circular Ltd operates the Service on behalf of the Merchant as a **data processor**. We act only on the Merchant's instructions and do not use your personal data for our own purposes beyond what is necessary to deliver the Service.

## What data is collected and when?

Personal information is accessed and stored at different stages:

* **When you visit the trade-in Storefront**: We may look up your order history held by the Merchant to identify products that are eligible for trade-in and to present them to you. At this stage, your personally identifiable information (such as your name or address) is not stored by Tern.
* **Only if you start a trade-in**: If you actively begin a trade-in submission, we will access and store the personal information necessary to process it — such as your name, email address, phone number, postal address, and relevant order details.

We also collect standard technical data (such as IP address and browser type) in server logs for security purposes. These logs are retained for 14 days.

## How is your data used?

Your personal data is used solely to provide the trade-in Service — including processing your submission, arranging collection of your items, issuing your discount code, and communicating with you about your trade-in.

## Who else can see your data?

To operate the Service, Tern Circular Ltd works with a number of third-party service providers, including providers of cloud hosting, email delivery, shipping and logistics, and payment processing. A full list is available on our [Third Party Subprocessors](/v1/legal/privacy-policy-merchants/third-party-subprocessors) page.

Your data may be transferred to and processed in the United States, subject to appropriate data transfer safeguards.

## How long is your data kept?

Trade-in records are retained for up to 7 years from the date of your submission to support merchant reporting, dispute resolution, and regulatory compliance.

If you request deletion, Tern Circular Ltd will remove direct identifiers (such as your name, email address, and postal address) from the trade-in record unless retention is required by law or needed to establish, exercise, or defend legal claims. A de-identified record may be retained for the remainder of the retention period.

If the Merchant stops using the Service or uninstalls the App, Tern Circular Ltd will remove direct identifiers and retain only de-identified trade-in records for the remainder of the retention period.

## Your rights

Because the Merchant is the data controller, your primary point of contact for exercising your data rights (such as access, correction, deletion, or objection) is the **Merchant**, via their own privacy policy which is accessible from their website footer.

The Merchant's privacy policy also explains the lawful basis they rely on for processing your personal data in connection with the trade-in service and provides the Merchant's contact details.

If you have a query specifically about how Tern Circular Ltd has processed your data as a service provider, you may contact us directly at <contact@tern.eco>.

If you are a UK or EU resident and are not satisfied with how your request is handled, you have the right to complain to a supervisory authority — in the UK, this is the Information Commissioner's Office at [ico.org.uk](https://ico.org.uk).

## Further information

For full details of how the Merchant uses your data, please refer to the Merchant's privacy policy on their website.

For full details of Tern Circular Ltd's data processing practices and obligations as a processor, please refer to our [Merchant Privacy Policy](/v1/legal/privacy-policy-merchants) and [Data Processing Agreement](/v1/legal/privacy-policy-merchants/data-processing-agreement).

These terms are governed alongside the [Tern Trade-in Terms and Conditions](https://github.com/Tern-Eco/tern-docs/blob/main/legal/terms-and-conditions.md), which apply to your use of the Service.


# Outbound Webhooks

## Overview

Outbound webhooks let your systems receive a notification whenever a trade-in's status changes in Tern, without needing to poll Tern.

Today there is a single event:

| Event                     | Fires when                                                                             |
| ------------------------- | -------------------------------------------------------------------------------------- |
| `trade_in.status_changed` | A trade-in transitions to a new status (e.g. Opened → Confirmed → Received → Rewarded) |

Delivery is asynchronous. When a status change happens, Tern queues a delivery for each matching, enabled webhook and sends it in the background. Expect delivery to happen shortly after the status change, not synchronously with it, and do not assume deliveries for the same trade-in arrive in strict order — each is queued and delivered independently.

## Registering a Webhook

Webhooks are managed in the Tern admin app: open **Settings → Integrations** and use the **Webhooks** card to register, edit, pause, or delete webhooks. Managing webhooks requires the **Webhooks** team role — for team members without it, the card is disabled.

Each webhook has the following settings:

| Setting                            | Description                                                                                                                                                            |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Webhook URL**                    | The endpoint Tern delivers events to. Must satisfy the [endpoint requirements](#endpoint-requirements) below.                                                          |
| **Event**                          | The event the webhook fires for — currently fixed to `trade_in.status_changed`.                                                                                        |
| **Status filters**                 | Optional. Limits the webhook to specific trade-in statuses — see [Event and Status Filtering](#event-and-status-filtering). Leave empty to receive all status changes. |
| **Secret**                         | The HMAC signing secret (minimum 32 characters). Leave empty and Tern generates a random 40-character secret for you.                                                  |
| **Basic Auth username / password** | Optional. See [Basic Auth](#optional-http-basic-auth).                                                                                                                 |
| **Enabled**                        | Toggle on each registered webhook. Switch it off to pause deliveries without deleting the webhook.                                                                     |

{% hint style="warning" %}
The signing secret is only visible immediately after you register the webhook — copy it into your integration's configuration straight away. Afterwards Tern only shows whether a secret is set, never its value, and it cannot be retrieved again.

To rotate the secret, edit the webhook and enter a new value of your own (32+ characters) in the Secret field.
{% endhint %}

When editing a webhook, the Basic Auth password field is always shown blank; leave it blank to keep the current password. To stop sending Basic Auth entirely, use the **Remove credentials** action on the registered webhook.

## Endpoint Requirements

Your webhook URL must pass validation both when you save it and again immediately before every delivery attempt. A URL that fails validation at delivery time (for example, because its DNS record changed after you saved it) is silently skipped for that delivery rather than retried.

Requirements:

* Must use `https://`. Plain HTTP is rejected.
* Must use the standard HTTPS port (443). Explicitly specifying any other port is rejected.
* The hostname (or its resolved IP addresses) must not be a private, loopback, link-local, or otherwise reserved address — this includes `localhost`, `metadata.google.internal`, and hostnames ending in `.local`, `.internal`, or `.localhost`.
* If the host is a plain hostname, its DNS A/AAAA records are resolved and checked against the same restrictions.

In practice: your endpoint must be a publicly resolvable HTTPS host, reachable on port 443, that does not resolve to an internal or private network address.

### Redirects

Delivery requests do not follow redirects. Your endpoint must respond directly at the configured URL — a 3xx response is treated as a failed delivery, not followed.

### Optional HTTP Basic Auth

If you configure Basic Auth credentials on a webhook, every delivery to it includes an `Authorization: Basic` header with those credentials.

## Delivery

Each delivery is an HTTP `POST` request:

* **Content-Type:** `application/json`
* **Body:** the JSON-encoded payload (see [Payload Reference](#payload-reference))
* **Timeout:** 15 seconds

### Headers

| Header                     | Description                                                             |
| -------------------------- | ----------------------------------------------------------------------- |
| `Content-Type`             | Always `application/json`.                                              |
| `User-Agent`               | `Tern-Webhook/1.0`                                                      |
| `X-Tern-Webhook-Id`        | The ID of the registered webhook, as a string.                          |
| `X-Tern-Webhook-Event`     | The event name, e.g. `trade_in.status_changed`.                         |
| `X-Tern-Webhook-Timestamp` | Unix timestamp (seconds), as a string, of when the request was sent.    |
| `X-Tern-Webhook-Signature` | Hex-encoded HMAC-SHA256 signature of the request body — see below.      |
| `Authorization`            | `Basic <credentials>`, only if Basic Auth is configured on the webhook. |

## Verifying Signatures

`X-Tern-Webhook-Signature` is computed as:

```
hex(HMAC_SHA256(secret, raw_request_body))
```

The signature covers the **raw JSON request body only** — the exact bytes that were sent, encoded as UTF-8. It does not cover the timestamp, event name, or any other header.

{% hint style="warning" %}
Because `X-Tern-Webhook-Timestamp` is not part of the signed content, its authenticity is not verified by the signature check. Don't treat it as a trustworthy replay-prevention value on its own. If replay protection matters for your integration, deduplicate on something inside the verified payload instead — for example, the combination of `trade_in.id` and `trade_in.latest_status_tracking.id` — rather than rejecting requests based on the timestamp header.
{% endhint %}

To verify a delivery:

1. Read the **raw** request body — do not parse and re-serialize the JSON before verifying, since re-encoding can change byte-for-byte output (key order, whitespace) and break the comparison.
2. Compute `HMAC-SHA256(your_secret, raw_body)` and hex-encode it.
3. Compare it to `X-Tern-Webhook-Signature` using a timing-safe comparison.
4. Only after verification succeeds, parse the body as JSON and process it.

### PHP

```php
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TERN_WEBHOOK_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $rawBody, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$payload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
```

### Node.js

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyTernWebhook(rawBody, signatureHeader, secret) {
  const expected = createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex');

  const expectedBuffer = Buffer.from(expected, 'hex');
  const givenBuffer = Buffer.from(signatureHeader ?? '', 'hex');

  if (expectedBuffer.length !== givenBuffer.length) {
    return false;
  }

  return timingSafeEqual(expectedBuffer, givenBuffer);
}

// Express: use express.raw({ type: 'application/json' }) on this route so
// req.body is the raw Buffer, not an already-parsed object.
app.post('/webhooks/tern', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body; // Buffer
  const signature = req.get('X-Tern-Webhook-Signature');

  if (!verifyTernWebhook(rawBody, signature, process.env.TERN_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const payload = JSON.parse(rawBody.toString('utf8'));
  // ... handle payload
  res.sendStatus(200);
});
```

## Payload Reference

{% hint style="info" %}
Webhook payloads contain customer personal data — name, email, phone, and address. Your endpoint, and anything downstream of it (logs, third-party processors), receives this data: secure it accordingly and account for it in your data-processing arrangements.
{% endhint %}

Every delivery has the same envelope:

```json
{
  "event": "trade_in.status_changed",
  "triggered_at": "2026-08-28T09:15:32+00:00",
  "trade_in": { }
}
```

| Field          | Type              | Description                                      |
| -------------- | ----------------- | ------------------------------------------------ |
| `event`        | string            | Always `trade_in.status_changed` for this event. |
| `triggered_at` | string (ISO 8601) | When the status change was recorded.             |
| `trade_in`     | object            | The trade-in, in the shape below.                |

### `trade_in`

| Field                             | Type                                                     | Description                                                                                                                                                                                                                                         |
| --------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                              | integer                                                  |                                                                                                                                                                                                                                                     |
| `uuid`                            | string                                                   |                                                                                                                                                                                                                                                     |
| `shop_id`                         | integer                                                  |                                                                                                                                                                                                                                                     |
| `shopify_customer_id`             | integer, nullable                                        |                                                                                                                                                                                                                                                     |
| `name`                            | string                                                   | Trade-in reference/display name.                                                                                                                                                                                                                    |
| `created_at`                      | datetime                                                 |                                                                                                                                                                                                                                                     |
| `trade_in_opened`                 | datetime, nullable                                       | When the trade-in was confirmed/opened.                                                                                                                                                                                                             |
| `trade_in_closed`                 | datetime, nullable                                       | When the trade-in was closed.                                                                                                                                                                                                                       |
| `status_id`                       | integer                                                  | Current status ID.                                                                                                                                                                                                                                  |
| `notification_email`              | string, nullable                                         |                                                                                                                                                                                                                                                     |
| `total_price`                     | number                                                   |                                                                                                                                                                                                                                                     |
| `net_price`                       | number                                                   |                                                                                                                                                                                                                                                     |
| `total_quantity`                  | integer                                                  |                                                                                                                                                                                                                                                     |
| `currency_symbol`                 | string                                                   | The shop's currency symbol.                                                                                                                                                                                                                         |
| `credit_amount_granted`           | number                                                   | Total reward value issued so far.                                                                                                                                                                                                                   |
| `is_non_monetary_reward`          | boolean                                                  |                                                                                                                                                                                                                                                     |
| `non_monetary_reward_description` | string, nullable                                         |                                                                                                                                                                                                                                                     |
| `non_monetary_reward_terms`       | string, nullable                                         |                                                                                                                                                                                                                                                     |
| `non_monetary_reward_count`       | integer, nullable                                        |                                                                                                                                                                                                                                                     |
| `marketing_consent`               | boolean                                                  |                                                                                                                                                                                                                                                     |
| `status`                          | object                                                   | See [`status`](#status).                                                                                                                                                                                                                            |
| `customer`                        | object                                                   | See [`customer`](#customer).                                                                                                                                                                                                                        |
| `address`                         | object                                                   | See [`address`](#address).                                                                                                                                                                                                                          |
| `shipment`                        | object, nullable                                         | See [`shipment`](#shipment). `null` if the trade-in has no shipment.                                                                                                                                                                                |
| `latest_status_tracking`          | object                                                   | See [`latest_status_tracking`](#latest_status_tracking).                                                                                                                                                                                            |
| `latest_shipment_tracking`        | object, present only if a shipment tracking event exists | The shipment's current tracking event, the same one the admin and Returns Centre show. It only ever moves forward in time, so a late-arriving older carrier event never replaces it. See [`shipment_tracking entries`](#shipment_tracking-entries). |
| `notes_for_customer`              | array of object                                          | See [`notes_for_customer`](#notes_for_customer).                                                                                                                                                                                                    |
| `discounts`                       | array of object                                          | See [`discounts`](#discounts).                                                                                                                                                                                                                      |
| `exchange_purchase_order_refund`  | object, nullable                                         | See [`exchange_purchase_order_refund`](#exchange_purchase_order_refund). `null` if not applicable to this trade-in.                                                                                                                                 |
| `items`                           | array of object                                          | See [`items`](#items).                                                                                                                                                                                                                              |

`deductions` and the full `shipment_tracking` history are not included in this payload — only the latest shipment tracking event is (`latest_shipment_tracking`).

#### `status`

| Field  | Type    |
| ------ | ------- |
| `id`   | integer |
| `name` | string  |
| `slug` | string  |

#### `customer`

| Field                 | Type              | Notes                                              |
| --------------------- | ----------------- | -------------------------------------------------- |
| `id`                  | integer           |                                                    |
| `shop_id`             | integer           |                                                    |
| `shopify_customer_id` | integer, nullable |                                                    |
| `first_name`          | string, nullable  |                                                    |
| `last_name`           | string, nullable  |                                                    |
| `email`               | string, nullable  |                                                    |
| `phone`               | string, nullable  |                                                    |
| `note`                | string, nullable  |                                                    |
| `currency`            | string, nullable  |                                                    |
| `created_at`          | string, nullable  | Formatted date string (not ISO 8601).              |
| `default_address`     | object, nullable  | `null` if the customer record has been anonymised. |
| `number_of_orders`    | integer, nullable |                                                    |
| `total_trade_ins`     | integer, nullable |                                                    |

#### `address`

| Field                | Type              |
| -------------------- | ----------------- |
| `id`                 | integer or string |
| `shopify_address_id` | integer, nullable |
| `first_name`         | string, nullable  |
| `last_name`          | string, nullable  |
| `address1`           | string, nullable  |
| `address2`           | string, nullable  |
| `city`               | string, nullable  |
| `company`            | string, nullable  |
| `country`            | string, nullable  |
| `country_code`       | string, nullable  |
| `phone`              | string, nullable  |
| `province`           | string, nullable  |
| `province_code`      | string, nullable  |
| `zip`                | string, nullable  |
| `is_completed`       | boolean           |

#### `shipment`

| Field                   | Type             | Description                                                                          |
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------ |
| `id`                    | integer          |                                                                                      |
| `created_at`            | datetime         |                                                                                      |
| `label_provider_type`   | string, nullable | `carrier` for a booked carrier label, `custom` for a merchant's own shipping option. |
| `label_carrier_name`    | string, nullable | Display name of the carrier behind the selected shipping option.                     |
| `label_service_name`    | string, nullable | Name of the selected carrier service.                                                |
| `label_format`          | string, nullable |                                                                                      |
| `label_url`             | string, nullable |                                                                                      |
| `label_qr_code_url`     | string, nullable |                                                                                      |
| `label_tracking_number` | string, nullable |                                                                                      |
| `label_tracking_url`    | string, nullable |                                                                                      |
| `custom_description`    | string, nullable | Only set for custom/manual carriers.                                                 |
| `custom_link`           | string, nullable | Only set for custom/manual carriers.                                                 |

Before the September 2026 backend release, `label_provider_type`, `label_carrier_name`, `label_service_name`, `custom_description` and `custom_link` were always `null`. They are populated from that release on; the field set itself is unchanged.

#### `latest_status_tracking`

| Field        | Type                             |
| ------------ | -------------------------------- |
| `id`         | integer                          |
| `user_id`    | integer, nullable                |
| `status_id`  | integer                          |
| `status`     | object — see [`status`](#status) |
| `created_at` | datetime                         |

#### `shipment_tracking` entries

Used for `latest_shipment_tracking`.

| Field                        | Type               |
| ---------------------------- | ------------------ |
| `id`                         | integer            |
| `status`                     | string             |
| `status_details`             | string, nullable   |
| `status_detail_code`         | string, nullable   |
| `status_detail_description`  | string, nullable   |
| `carrier_status_code`        | string, nullable   |
| `carrier_status_description` | string, nullable   |
| `status_updated_at`          | datetime, nullable |
| `tracking_number`            | string, nullable   |
| `created_at`                 | datetime           |
| `updated_at`                 | datetime           |

#### `notes_for_customer`

| Field          | Type     |
| -------------- | -------- |
| `id`           | integer  |
| `note_type_id` | integer  |
| `name`         | string   |
| `note`         | string   |
| `created_at`   | datetime |

#### `discounts`

| Field                          | Type                              |
| ------------------------------ | --------------------------------- |
| `id`                           | integer                           |
| `created_at`                   | string (formatted date)           |
| `starts_at`                    | string (formatted date)           |
| `shopify_id`                   | string, nullable                  |
| `shopify_resource_id`          | string, nullable                  |
| `trade_in_id`                  | integer                           |
| `type`                         | string                            |
| `title`                        | string, nullable                  |
| `code`                         | string, nullable                  |
| `amount_type`                  | string                            |
| `value`                        | number                            |
| `applies_on_each_item`         | boolean                           |
| `usage_limit`                  | integer, nullable                 |
| `applies_once_per_customer`    | boolean                           |
| `can_be_used_by_all_customers` | boolean                           |
| `combines_with`                | object                            |
| `minimum_order_value`          | number, nullable                  |
| `expires_at`                   | string, nullable (formatted date) |
| `redeemed_at`                  | string, nullable (formatted date) |
| `redeemed_value`               | number, nullable                  |

`combines_with` shape: `{ "orders": boolean, "products": boolean, "shipping": boolean }`.

#### `exchange_purchase_order_refund`

Base refund fields plus:

| Field                  | Type                              |
| ---------------------- | --------------------------------- |
| `shopify_created_at`   | string, nullable (formatted date) |
| `shopify_processed_at` | string, nullable (formatted date) |
| `line_items`           | array of object                   |

#### `items`

| Field                  | Type              | Description                                                               |
| ---------------------- | ----------------- | ------------------------------------------------------------------------- |
| `id`                   | integer           |                                                                           |
| `trade_in_id`          | integer           |                                                                           |
| `quantity_expected`    | integer           |                                                                           |
| `status_id`            | integer           |                                                                           |
| `reward_type`          | string, nullable  |                                                                           |
| `strategy`             | string, nullable  |                                                                           |
| `reward_value_offered` | number, nullable  |                                                                           |
| `created_at`           | datetime          |                                                                           |
| `quantity_received`    | integer, nullable |                                                                           |
| `image_url`            | string, nullable  |                                                                           |
| `status`               | object            | See [`status`](#status).                                                  |
| `recordable`           | object, nullable  | The record backing this item — shape depends on the item type, see below. |
| `repair_services`      | array of object   | See [`repair_services`](#repair_services).                                |

`recordable` is one of two shapes depending on how the item was traded in:

* **Category/product-option record** (customer chose a product from your category tree):

  | Field                  | Type               |
  | ---------------------- | ------------------ |
  | `id`                   | integer            |
  | `uuid`                 | string             |
  | `product_option_id`    | integer, nullable  |
  | `product_option_title` | string, nullable   |
  | `category_id`          | integer, nullable  |
  | `category_title`       | string, nullable   |
  | `category_path`        | string, nullable   |
  | `created_at`           | datetime           |
  | `updated_at`           | datetime           |
  | `deleted_at`           | datetime, nullable |
* **Linked Shopify product record** (item is tied to a specific Shopify product/variant): the underlying record's own fields (IDs, pricing, etc.). The related product, variant, order line item, and offer/profile type objects are not expanded in this payload.

#### `repair_services`

| Field               | Type             |
| ------------------- | ---------------- |
| `id`                | integer          |
| `trade_in_item_id`  | integer          |
| `repair_service_id` | integer          |
| `description`       | string, nullable |
| `warranty`          | string, nullable |
| `cost`              | number, nullable |
| `note`              | string, nullable |
| `created_at`        | datetime         |
| `updated_at`        | datetime         |

## Retries and Failure Handling

A delivery is considered successful if your endpoint returns any `2xx` status code.

If a delivery fails — a non-`2xx` response, or a connection-level error such as a timeout or DNS failure — Tern retries once (2 attempts total for that event), waiting 200ms between attempts. If both attempts fail, that delivery is abandoned; it is not queued for later retry, and the underlying status change is not automatically re-sent.

When a delivery ultimately fails, Tern emails the shop owner with the failure details (endpoint URL, status code, and a summary of the response body). To avoid flooding the owner's inbox from a persistently broken endpoint, these failure emails are rate-limited to one per webhook per hour.

Repeated failures do not automatically disable a webhook — it keeps receiving delivery attempts (and you keep receiving failure emails, subject to the hourly limit) until you fix the endpoint or switch the webhook off in the admin.

## Event and Status Filtering

Two settings control which status changes a webhook receives:

* **Event** — the webhook only fires for its selected event type. Today the only supported event is `trade_in.status_changed`.
* **Status filters** — optional. If set, the webhook only fires when a trade-in transitions **into** one of the selected statuses. If left empty, the webhook fires for every trade-in status change on the shop.

Filtering is evaluated per webhook, so different webhooks on the same shop can watch different subsets of statuses.


# Storefront Integration

Integration guidance for Shopify, non-Shopify, and standalone storefronts using Tern

## Choose your integration path

<table data-view="cards"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><a href="/v1/storefront-integration/storefront-integration/headless-storefront-third-party"><strong>Shopify headless storefront guide</strong></a></td><td>Recommended path for Shopify headless storefronts using merchant-signed bootstrap, with optional logged-in customer context.</td></tr><tr><td><a href="/v1/storefront-integration/storefront-integration/non-shopify-headless-storefront-third-party"><strong>Non-Shopify headless storefront guide</strong></a></td><td>Merchant-signed headless integration for non-Shopify storefronts without Shopify customer authentication.</td></tr><tr><td><a href="/v1/storefront-integration/storefront-integration/standalone-storefront-third-party"><strong>Standalone storefront guide</strong></a></td><td>Fallback anonymous no-proof integration for standalone storefronts when authenticated headless bootstrap is not available.</td></tr><tr><td><a href="/v1/storefront-integration/storefront-integration/storefront-styling-guide"><strong>Storefront styling guide</strong></a></td><td>Theme variables, supported styling hooks, and custom CSS guidance for restyling the Returns Centre widget.</td></tr></tbody></table>


# Shopify headless storefront guide

## Purpose

This guide explains how a Shopify headless storefront should prepare a signed config payload, expose it to the Tern storefront module, and mount `<tern-trade-in>`.

This is the recommended integration path when the storefront can identify the shop and, optionally, the logged-in Shopify customer.

If you are not a Shopify merchant, use [Non-Shopify Headless Storefront Integration Guide](/v1/storefront-integration/storefront-integration/non-shopify-headless-storefront-third-party).

If you cannot use headless authentication at all, use [Standalone Storefront Integration Guide](/v1/storefront-integration/storefront-integration/standalone-storefront-third-party).

## Recommendation First

Use this guide when:

1. The storefront belongs to a Shopify shop.
2. You can generate merchant-signed bootstrap payloads on your backend.
3. You may need anonymous or logged-in customer bootstrap.
4. You want the recommended trust model for headless integrations.

## Summary

The implementation has three steps:

1. Get the merchant shared secret from Tern.
2. Use that shared secret on your backend to sign the bootstrap request payload.
3. Expose `window.ternStorefrontConfig` before mounting `<tern-trade-in>`.

## Shared Secret

Before implementing the bootstrap call, obtain the merchant shared secret from the Tern admin interface for the relevant shop.

Implementation guidance:

1. Generate or copy the shared secret from the Tern admin interface.
2. Store the shared secret on your backend.
3. Do not expose the shared secret in browser code.
4. Use the shared secret only on the server when generating the HMAC signature.

## Config Format

Expose `window.ternStorefrontConfig` with these fields:

1. `shopDomain`
2. `locale`
3. `proof`
4. Optional `customerLogin`

Browser config uses `shopDomain`. The storefront runtime converts that to the API field `shop_domain` before calling the bootstrap endpoint.

### Optional `customerLogin`

Use `customerLogin` when you want to override where unauthenticated customers are redirected for login.

`customerLogin` supports:

1. `path`: the login path on the current origin (must start with `/`)
2. `redirectParam`: query-string key used to return from login

If omitted, storefront uses:

1. `path = /customer_authentication/login`
2. `redirectParam = checkout_url`

Example:

```json
{
  "shopDomain": "example.myshopify.com",
  "locale": "en",
  "customerLogin": {
    "path": "/account/login",
    "redirectParam": "checkout_url"
  },
  "proof": {
    "type": "merchant_signed",
    "timestamp": 1710000000,
    "signature": "hex-hmac-signature"
  }
}
```

## Supported Bootstrap Flows

### Merchant-Signed Anonymous Bootstrap

Use this when you want a signed storefront config without attaching a customer.

```json
{
  "shopDomain": "example.myshopify.com",
  "locale": "en",
  "proof": {
    "type": "merchant_signed",
    "timestamp": 1710000000,
    "signature": "hex-hmac-signature"
  }
}
```

### Merchant-Signed Logged-In Bootstrap

Use this when you want the storefront config to identify a logged-in customer.

```json
{
  "shopDomain": "example.myshopify.com",
  "locale": "en",
  "proof": {
    "type": "merchant_signed",
    "timestamp": 1710000000,
    "signature": "hex-hmac-signature",
    "customer": {
      "id": 7639131717921,
      "email": "customer@example.com"
    }
  }
}
```

## How To Sign The Request

When using `merchant_signed`, your backend must sign the full canonical string shown below with HMAC-SHA256 using the merchant shared secret.

The result of that HMAC operation becomes `proof.signature` in `window.ternStorefrontConfig`.

Only these four values are part of the signature, joined in alphabetical key order:

1. `email`
2. `id`
3. `shop_domain`
4. `timestamp`

Sign this exact canonical string:

```
email=<normalized-email>&id=<id-or-0>&shop_domain=<shop-domain>&timestamp=<unix-seconds>
```

Rules:

1. `email` must be lowercased and trimmed.
2. `shop_domain` must be the Shopify shop domain for the target store.
3. `id` must be the logged-in customer id, or `0` when the bootstrap is anonymous.
4. `timestamp` must be a Unix timestamp in seconds.
5. The signature must be lowercase hex.
6. Generate the signature on your backend, never in browser code.

### Node.js Example: Anonymous Signing

```js
import { createHmac } from 'node:crypto';

const sharedSecret = process.env.TERN_SHARED_SECRET;
const timestamp = Math.floor(Date.now() / 1000);

const canonical = [
  ['email', ''],
  ['id', '0'],
  ['shop_domain', 'example.myshopify.com'],
  ['timestamp', String(timestamp)]
].map(([key, value]) => `${key}=${value}`).join('&');

const signature = createHmac('sha256', sharedSecret)
  .update(canonical, 'utf8')
  .digest('hex')
  .toLowerCase();

const payload = {
  shopDomain: 'example.myshopify.com',
  locale: 'en',
  proof: {
    type: 'merchant_signed',
    timestamp,
    signature
  }
};
```

### Node.js Example: Logged-In Customer Signing

```js
import { createHmac } from 'node:crypto';

const sharedSecret = process.env.TERN_SHARED_SECRET;
const timestamp = Math.floor(Date.now() / 1000);
const email = 'customer@example.com'.trim().toLowerCase();
const shopifyCustomerId = '7639131717921';

const canonical = [
  ['email', email],
  ['id', shopifyCustomerId],
  ['shop_domain', 'example.myshopify.com'],
  ['timestamp', String(timestamp)]
].map(([key, value]) => `${key}=${value}`).join('&');

const signature = createHmac('sha256', sharedSecret)
  .update(canonical, 'utf8')
  .digest('hex')
  .toLowerCase();

const payload = {
  shopDomain: 'example.myshopify.com',
  locale: 'en',
  proof: {
    type: 'merchant_signed',
    timestamp,
    signature,
    customer: {
      id: Number(shopifyCustomerId),
      email
    }
  }
};
```

In the logged-in example:

1. The signed string includes the normalized email and Shopify customer id.
2. The `customer` object in the request must match the values used to build the signed canonical string.
3. If these values do not match, Tern will reject the request.

For anonymous merchant-signed bootstrap, omit `proof.customer` entirely and sign with an empty `email` and `id=0`.

## Bootstrap Response

After the storefront module initializes, it obtains a bootstrap response that includes:

1. `token`
2. `locale`
3. `ga_measurement_id`
4. `storefront_version` (`1` or `2` — see [Bundle URL By Storefront Version](#bundle-url-by-storefront-version))
5. Optional `shopify_customer`

Example response:

```json
{
  "token": "tern-jwt",
  "locale": "en",
  "ga_measurement_id": "",
  "storefront_version": 2,
  "shopify_customer": {
    "id": 7639131717921,
    "shopify_customer_id": 7639131717921,
    "first_name": "Ben",
    "last_name": "Yarwood",
    "email": "ben@tern.eco",
    "address": {}
  }
}
```

## Browser Runtime And Mount

Your page must load the Tern storefront runtime before you try to mount `<tern-trade-in>`.

If you are using the built browser bundle, load it first:

```html
<script src="https://prod.tern.eco/js/storefront/trn-nrc-umd.js"></script>
```

### Bundle URL By Storefront Version

The bundle URL depends on which storefront version the shop has been migrated to:

* **v1 (default)**: `https://prod.tern.eco/js/storefront/trn-nrc-umd.js`
* **v2 (migrated shops)**: `https://prod.tern.eco/js/storefront/v2/trn-nrc-umd.js`

Tern's own embedded Shopify integration switches this automatically server-side based on the shop's `storefront_version`. Third-party headless integrations that load the bundle directly must switch this URL themselves when the shop is migrated to v2. Confirm which version applies to your shop with Tern before changing the bundle URL.

Then define `window.ternStorefrontConfig` before mounting `<tern-trade-in>`. The storefront module uses that config to initialize the storefront session internally.

### Example `head`

```html
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tern Shopify Headless Storefront</title>
  <script src="https://prod.tern.eco/js/storefront/trn-nrc-umd.js"></script>
</head>
```

### Example `body`

```html
<body>
  <div id="tern-storefront-root"></div>

  <script>
    window.ternStorefrontConfig = {
      shopDomain: 'example.myshopify.com',
      locale: 'en',
      customerLogin: {
        path: '/account/login',
        redirectParam: 'checkout_url'
      },
      proof: {
        type: 'merchant_signed',
        timestamp: 1710000000,
        signature: 'hex-hmac-signature',
        customer: {
          id: 7639131717921,
          email: 'customer@example.com'
        }
      }
    };

    const element = document.createElement('tern-trade-in');
    document.getElementById('tern-storefront-root').appendChild(element);
  </script>
</body>
```

## What The JavaScript Example Assumes

The JavaScript snippets in this guide are integration snippets, not complete standalone pages.

They assume:

1. Your page already includes the Tern storefront bundle, for example `https://prod.tern.eco/js/storefront/trn-nrc-umd.js` (or the `v2` path for migrated shops — see [Bundle URL By Storefront Version](#bundle-url-by-storefront-version)).
2. Your page already contains a mount target such as `<div id="tern-storefront-root"></div>`.
3. The HMAC signature was generated on your backend before the browser set `window.ternStorefrontConfig`.
4. The browser can load the Tern storefront bundle successfully.

If you paste the snippet into a page without those prerequisites, it will not work on its own.

## Implementation Checklist

1. Obtain the merchant shared secret from the Tern admin interface.
2. Store the shared secret on your backend.
3. Build and sign the canonical payload on the backend.
4. Expose `shopDomain`, `locale`, and `proof` in `window.ternStorefrontConfig` before mounting the storefront.
5. Optionally set `customerLogin.path` and `customerLogin.redirectParam` to control login redirect behavior.
6. Load the storefront browser bundle, for example `https://prod.tern.eco/js/storefront/trn-nrc-umd.js` (or the `v2` path for migrated shops — see [Bundle URL By Storefront Version](#bundle-url-by-storefront-version)).
7. Mount `<tern-trade-in>` after `window.ternStorefrontConfig` is defined.
8. Let the storefront module initialize the storefront session from that config.
9. Use the returned `token` from the bootstrap response as the storefront session token.


# Non-Shopify headless storefront guide

## Purpose

This guide explains how a non-Shopify headless storefront should prepare a signed config payload, expose it to the Tern storefront module, and mount `<tern-trade-in>`.

This path supports merchant-signed headless bootstrap without Shopify customer authentication.

If the storefront belongs to a Shopify shop and you need logged-in customer support, use [Shopify Headless Storefront Integration Guide](/v1/storefront-integration/storefront-integration/headless-storefront-third-party).

If you cannot use headless authentication at all, use [Standalone Storefront Integration Guide](/v1/storefront-integration/storefront-integration/standalone-storefront-third-party).

## Recommendation First

This guide is for non-Shopify headless storefronts that can still generate merchant-signed bootstrap payloads on their backend.

It is stronger than the standalone no-proof flow because the request is signed server-side, but it does not include Shopify customer authentication.

## Summary

The implementation has three steps:

1. Get the merchant shared secret from Tern.
2. Use that shared secret on your backend to sign the bootstrap request payload.
3. Expose `window.ternStorefrontConfig` before mounting `<tern-trade-in>`.

## What Is Different From Shopify Headless

This guide uses the same merchant-signed proof type as the Shopify guide, but with these restrictions:

1. No logged-in Shopify customer context.
2. No `proof.customer` object.
3. The signature must always use an empty `email` value.
4. The signature must always use `id=0`.

## Shared Secret

Before implementing the bootstrap call, obtain the merchant shared secret from the Tern admin interface for the relevant shop.

Implementation guidance:

1. Generate or copy the shared secret from the Tern admin interface.
2. Store the shared secret on your backend.
3. Do not expose the shared secret in browser code.
4. Use the shared secret only on the server when generating the HMAC signature.

## Config Format

Expose `window.ternStorefrontConfig` with these fields:

1. `shopDomain`
2. `locale`
3. `proof`

Browser config uses `shopDomain`. The storefront runtime converts that to the API field `shop_domain` before calling the bootstrap endpoint.

Example:

```json
{
  "shopDomain": "example-store.com",
  "locale": "en",
  "proof": {
    "type": "merchant_signed",
    "timestamp": 1710000000,
    "signature": "hex-hmac-signature"
  }
}
```

## How To Sign The Request

When using `merchant_signed`, your backend must sign the full canonical string shown below with HMAC-SHA256 using the merchant shared secret.

Only these four values are part of the signature, joined in alphabetical key order:

1. `email`
2. `id`
3. `shop_domain`
4. `timestamp`

Sign this exact canonical string:

```
email=<normalized-email>&id=<id-or-0>&shop_domain=<shop-domain>&timestamp=<unix-seconds>
```

For non-Shopify headless bootstrap:

1. `email` must be an empty string.
2. `shop_domain` must be the configured shop domain for the storefront.
3. `id` must be `0`.
4. `timestamp` must be a Unix timestamp in seconds.
5. The signature must be lowercase hex.
6. Generate the signature on your backend, never in browser code.

### Node.js Example

```js
import { createHmac } from 'node:crypto';

const sharedSecret = process.env.TERN_SHARED_SECRET;
const timestamp = Math.floor(Date.now() / 1000);

const canonical = [
  ['email', ''],
  ['id', '0'],
  ['shop_domain', 'example-store.com'],
  ['timestamp', String(timestamp)]
].map(([key, value]) => `${key}=${value}`).join('&');

const signature = createHmac('sha256', sharedSecret)
  .update(canonical, 'utf8')
  .digest('hex')
  .toLowerCase();

const payload = {
  shopDomain: 'example-store.com',
  locale: 'en',
  proof: {
    type: 'merchant_signed',
    timestamp,
    signature
  }
};
```

Do not include `proof.customer` in this flow.

## Bootstrap Response

After the storefront module initializes, it obtains a bootstrap response that includes:

1. `token`
2. `locale`
3. `ga_measurement_id`
4. `storefront_version` (`1` or `2` — see [Bundle URL By Storefront Version](/v1/storefront-integration/storefront-integration/headless-storefront-third-party#bundle-url-by-storefront-version))

`shopify_customer` is not expected in this flow.

Example response:

```json
{
  "token": "tern-jwt",
  "locale": "en",
  "ga_measurement_id": "",
  "storefront_version": 2
}
```

The returned token is a Tern storefront session token. It is not proof of Shopify customer identity.

## Browser Runtime And Mount

Use the same runtime loading and mount pattern as [Shopify Headless Storefront Integration Guide](/v1/storefront-integration/storefront-integration/headless-storefront-third-party#browser-runtime-and-mount), including the version-specific bundle URL — see [Bundle URL By Storefront Version](/v1/storefront-integration/storefront-integration/headless-storefront-third-party#bundle-url-by-storefront-version). Confirm which version applies to your shop with Tern before changing the bundle URL.

The only difference is the config payload. Use this body snippet instead:

```html
<body>
  <div id="tern-storefront-root"></div>

  <script>
    window.ternStorefrontConfig = {
      shopDomain: 'example-store.com',
      locale: 'en',
      proof: {
        type: 'merchant_signed',
        timestamp: 1710000000,
        signature: 'hex-hmac-signature'
      }
    };

    const element = document.createElement('tern-trade-in');
    document.getElementById('tern-storefront-root').appendChild(element);
  </script>
</body>
```

For shared prerequisites, see [What The JavaScript Example Assumes](/v1/storefront-integration/storefront-integration/headless-storefront-third-party#what-the-javascript-example-assumes).

## Implementation Checklist

1. Obtain the merchant shared secret from the Tern admin interface.
2. Store the shared secret on your backend.
3. Build the canonical payload with empty `email` and `id=0`.
4. Sign the payload on your backend.
5. Expose `shopDomain`, `locale`, and `proof` in `window.ternStorefrontConfig` before mounting the storefront.
6. Load the storefront browser bundle (v1 or v2 URL depending on the shop's migration status — see [Bundle URL By Storefront Version](/v1/storefront-integration/storefront-integration/headless-storefront-third-party#bundle-url-by-storefront-version)).
7. Mount `<tern-trade-in>` after `window.ternStorefrontConfig` is defined.
8. Let the storefront module initialize the storefront session from that config.
9. Use the returned `token` as the storefront session token.
10. If you later need authenticated customer context, move to [Shopify Headless Storefront Integration Guide](/v1/storefront-integration/storefront-integration/headless-storefront-third-party).


# Standalone storefront guide

## Purpose

This guide explains how to install the Tern storefront on a standalone shop that does not use headless authentication.

This is the anonymous no-proof storefront path. It is the lower-trust option and should only be used when the recommended authenticated headless flow is not possible.

For new integrations, prefer an authenticated headless guide:

1. [Shopify Headless Storefront Integration Guide](/v1/storefront-integration/storefront-integration/headless-storefront-third-party)
2. [Non-Shopify Headless Storefront Integration Guide](/v1/storefront-integration/storefront-integration/non-shopify-headless-storefront-third-party)

## Recommendation First

Use the standalone no-proof flow only as a fallback.

Tern recommends the authenticated headless approach because it provides a stronger trust signal, supports merchant-signed bootstrap, and can attach customer context when needed.

Use the authenticated guide when you need any of the following:

1. Logged-in customer context.
2. Server-generated proof of who is bootstrapping the storefront.
3. Stronger protection against unauthorized or copied integrations.
4. A path that can safely identify a Shopify customer.

## Security Concerns

This standalone flow has important security limitations:

1. There is no merchant-signed proof on the bootstrap request.
2. The browser does not prove customer identity.
3. The storefront session is anonymous only.
4. Anyone who can serve code from the configured standalone origin can attempt to bootstrap the storefront.
5. You must not treat the returned storefront token as proof that a customer is logged in.

Because of those limits, this mode is appropriate only for anonymous standalone storefront experiences.

If you need customer-aware behavior or stronger trust guarantees, stop here and use one of the authenticated headless guides instead.

## Summary

The simplest standalone implementation has two steps:

1. Load the storefront runtime.
2. Add `<tern-trade-in>` to the page.

Optional config overrides can be added later if you want to force a locale.

## Minimal Install

For standalone storefronts, the minimal install does not need `window.ternStorefrontConfig`.

If the page is served from the exact standalone hostname configured in Tern, the storefront can resolve the shop from the request origin and initialize its session from subsequent storefront API calls.

### Minimal `head`

```html
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tern Standalone Storefront</title>
  <script src="https://prod.tern.eco/js/storefront/trn-nrc-umd.js"></script>
</head>
```

### Bundle URL By Storefront Version

The bundle URL depends on which storefront version the shop has been migrated to:

* **v1 (default)**: `https://prod.tern.eco/js/storefront/trn-nrc-umd.js`
* **v2 (migrated shops)**: `https://prod.tern.eco/js/storefront/v2/trn-nrc-umd.js`

Confirm which version applies to your shop with Tern before changing the bundle URL.

### Minimal `body`

```html
<body>
  <tern-trade-in></tern-trade-in>
</body>
```

### Why This Works

When no config is provided:

1. The storefront uses the page origin to identify the standalone shop.
2. Locale falls back to the document language first, then the browser language.
3. The first storefront API calls can initialize the standalone session from that origin.

Use the minimal install unless you need a specific locale override.

## Optional Config Overrides

If you want to override the locale explicitly, add one extra step to the minimal install:

1. Define `window.ternStorefrontConfig` before mounting `<tern-trade-in>`.

## Config Format

Expose `window.ternStorefrontConfig` only if you need an override such as a forced locale.

For standalone anonymous bootstrap, the optional config should usually include only:

1. `locale`

Example:

```json
{
  "locale": "en"
}
```

Rules:

1. Use `locale` only when you need to override the default locale.

## Session Behavior

In the minimal standalone install, there is no explicit bootstrap request to configure first.

Instead:

1. The storefront mounts on the page.
2. Early storefront API requests use the page origin to resolve the standalone shop.
3. The storefront session token is established and refreshed as those API requests complete.

That token is a Tern storefront session token for anonymous storefront traffic. It is not customer authentication.

If you add `window.ternStorefrontConfig` for a locale override, the runtime can make the explicit headless bootstrap request before mount. The minimal install does not depend on that call.

## Browser Runtime And Mount

The minimal install is the recommended starting point.

If you need an override such as `locale`, add `window.ternStorefrontConfig` before the same direct `<tern-trade-in>` mount:

```html
<body>
  <script>
    window.ternStorefrontConfig = {
      locale: 'en'
    };
  </script>

  <tern-trade-in></tern-trade-in>
</body>
```

For shared runtime prerequisites around loading the bundle before mount, see [What The JavaScript Example Assumes](/v1/storefront-integration/storefront-integration/headless-storefront-third-party#what-the-javascript-example-assumes).

Standalone-specific prerequisite:

1. The page must be served from the exact standalone hostname configured in Tern.

## Operational Notes

Keep these constraints in mind:

1. This flow is origin-based and anonymous.
2. Logged-in customer identification is not supported.
3. If the page origin does not match the configured standalone shop domain, bootstrap can fail or resolve the wrong shop.

If you later need authenticated sessions, do not extend this pattern in the browser. Move to one of the authenticated headless guides instead.

## Implementation Checklist

1. Configure the standalone shop record in Tern.
2. Confirm the shop uses the exact standalone hostname you will serve from.
3. Serve your storefront page from that hostname.
4. Load the storefront browser bundle, for example `https://prod.tern.eco/js/storefront/trn-nrc-umd.js` (or the `v2` path for migrated shops — see [Bundle URL By Storefront Version](#bundle-url-by-storefront-version)).
5. Add `<tern-trade-in>` to the page.
6. Only add `window.ternStorefrontConfig` if you need an override such as `locale`.
7. Let the storefront module initialize the anonymous storefront session.
8. Treat the storefront session token only as the Tern storefront session token for anonymous storefront APIs.
9. If stronger security or customer context is needed, switch to [Shopify Headless Storefront Integration Guide](/v1/storefront-integration/storefront-integration/headless-storefront-third-party) or [Non-Shopify Headless Storefront Integration Guide](/v1/storefront-integration/storefront-integration/non-shopify-headless-storefront-third-party).


# Storefront styling guide

## Purpose

This guide explains how to restyle the **Returns Centre** — the trade-in widget Tern renders on your storefront — so it matches the rest of your site. It applies whether Tern embeds the widget for you or you mount `<tern-trade-in>` yourself using one of the integration guides in this section.

The Returns Centre picks up your shop's fonts and colours automatically, but you can go further. There are two ways to change how it looks, and both live in the Tern admin under **Copy Customisation**:

1. **Theme settings** — colours, fonts and corner rounding that restyle the whole widget consistently. No CSS knowledge needed. Start here.
2. **Custom CSS** — your own style rules, for fine-grained control over individual parts of the widget.

Everything on this page is **supported**: it will keep working from release to release. We add new options over time, but we don't rename or remove anything listed here without telling you first.

## Terminology

A few terms this guide uses precisely:

* **The widget** — the Returns Centre UI, rendered inside the `<tern-trade-in>` element on your page. Its root element is `#trn-app`.
* **Theme variable** — a CSS custom property (also called a CSS variable) with a `--trn-` prefix. These are the widget's design tokens: named values for colours, fonts and corner rounding that the whole UI reads from.
* **Styling hook** — a stable class name with a `.trn-` prefix (for example `.trn-button`). These follow a BEM-flavoured naming scheme (`block__element--modifier`) and are the supported targets for custom CSS.
* **Utility class** — a short, generated class such as `mt-6` or `flex`. These are build output, not a stable API — see [What not to target](#what-not-to-target).

## How your styles reach the widget

The widget renders inside a **shadow root** attached to the `<tern-trade-in>` element. Shadow DOM is the browser's style-encapsulation mechanism: styles outside it don't leak in, and the widget's styles don't leak out onto your page.

Two practical consequences:

1. **Rules in your site's own stylesheets will not restyle the widget's internals.** The supported route is the **Custom CSS** field in Copy Customisation — Tern delivers that CSS inside the widget for you.
2. **The widget can never break your page's styling**, however heavily you customise it.

Your custom CSS is applied **after** the widget's own styles and always takes precedence — you never need `!important` to override us (existing `!important` rules keep working fine).

> **For CSS experts:** the widget's own styles live in named cascade layers, while your custom CSS is injected unlayered after them. Per the cascade, unlayered author styles beat layered ones regardless of specificity — that's why a plain `#trn-app .trn-button { … }` rule always wins.

## 1. Theme settings (colours, fonts, corners)

These are applied as theme variables on the widget. Most merchants set them through the theme fields in the admin; if you prefer, you can also set them yourself at the top of your custom CSS:

```css
#trn-app {
  --trn-brand: #0f4c81;
  --trn-radius-md: 12px;
}
```

**Colour format:** colours are plain hex codes, used exactly like any other CSS colour — no `rgb()` wrapper needed:

```css
/* ✓ works */ --trn-error: #c6454a;
```

### Brand

Your accent colour — used for buttons and the widget's other key accents.

| Variable         | What it changes                               |
| ---------------- | --------------------------------------------- |
| `--trn-brand`    | Buttons and key accents — your "brand" colour |
| `--trn-on-brand` | Text on those buttons                         |

### Page

Your base palette — the widget's background, card colour and main text colour.

| Variable           | What it changes                                                   |
| ------------------ | ----------------------------------------------------------------- |
| `--trn-background` | The widget's background                                           |
| `--trn-surface`    | Card and panel backgrounds (defaults to the background colour)    |
| `--trn-foreground` | Your ink colour — main text, and what your greys are derived from |

### Status

Colours for error and success messages, and for the status badges on the trade-in history pages.

| Variable        | What it changes                                   |
| --------------- | ------------------------------------------------- |
| `--trn-error`   | Error states                                      |
| `--trn-success` | Success states                                    |
| `--trn-warning` | Status badge colour for waiting states (amber)    |
| `--trn-info`    | Status badge colour for in-progress states (blue) |

### Corners

Corner rounding, in three sizes.

| Variable            | What it changes                                                                                                                       |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `--trn-radius-sm`   | Corner rounding on small elements — inputs, tags (default 4px)                                                                        |
| `--trn-radius-md`   | Corner rounding on cards and dialogs (default 8px)                                                                                    |
| `--trn-radius-full` | The "pill" radius used by buttons (default 9999px; lower it for squarer buttons, or use `--trn-button-radius` to change buttons only) |

### Derived colours

A further seven variables are computed automatically from your foreground and background colours, so cards, borders and secondary text always stay in proportion to the rest of your theme — you don't need to pick them by hand, and they keep up automatically if you change your page or brand colours later. Set one yourself only if you want to break that link and fix its value instead; like any other variable on this page, it can be overridden in your custom CSS.

| Variable                | What it changes                                                                 |
| ----------------------- | ------------------------------------------------------------------------------- |
| `--trn-surface-muted`   | Subtle tint on a card — callouts, empty states, hover fills, image placeholders |
| `--trn-surface-strong`  | A stronger tint on a card — heavier fills and higher-contrast callouts          |
| `--trn-text-muted`      | Secondary text                                                                  |
| `--trn-text-subtle`     | The least prominent text — captions, helper copy                                |
| `--trn-border`          | Standard borders and dividers                                                   |
| `--trn-border-subtle`   | Faint borders and dividers, close to the background colour                      |
| `--trn-button-bg-hover` | Button background on hover                                                      |

### Component-level theming

A second layer of variables restyles one part of the widget without touching the base palette — useful when, say, you want squarer buttons but everything else unchanged. Each defaults to the base colour/corner setting above, so you only need to set the ones you want to differ:

| Variable              | What it changes                                                          | Defaults to                |
| --------------------- | ------------------------------------------------------------------------ | -------------------------- |
| `--trn-button-bg`     | Button background specifically (independent of the general brand colour) | `--trn-brand`              |
| `--trn-button-text`   | Button text colour specifically                                          | `--trn-on-brand`           |
| `--trn-button-radius` | Button corner rounding                                                   | `--trn-radius-full` (pill) |
| `--trn-card-radius`   | Product/start-card corner rounding                                       | `--trn-radius-md`          |
| `--trn-card-shadow`   | Drop shadow on cards, e.g. `0 2px 8px rgba(0, 0, 0, 0.08)`               | none                       |
| `--trn-input-border`  | Form field border colour                                                 | `--trn-border`             |

```css
/* Square buttons, everything else untouched */
#trn-app {
  --trn-button-radius: 4px;
}
```

### Fonts and casing

| Variable                       | What it changes                                                                                                                                                                        |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--trn-font-body`              | Font for all text, e.g. `'Inter', sans-serif`. (`--font-main` is the legacy name for the same knob and is still honoured — the body font it holds flows through to `--trn-font-body`.) |
| `--trn-font-heading`           | Headings only (defaults to `--trn-font-body`)                                                                                                                                          |
| `--trn-heading-text-transform` | Set to `uppercase` to render headings in all caps; default `none`                                                                                                                      |
| `--trn-button-text-transform`  | Set to `uppercase` to render button labels in all caps; default `none`                                                                                                                 |

### Renamed variables

Some variables on this page previously went by different names. If your custom CSS **sets** one of the old names, it still works — the value flows through to its replacement automatically — but use the new names in anything you write from now on:

| Old name                        | New name                |
| ------------------------------- | ----------------------- |
| `--trn-button-background-color` | `--trn-brand`           |
| `--trn-button-text-color`       | `--trn-on-brand`        |
| `--trn-gray-1`                  | `--trn-button-bg-hover` |
| `--trn-gray-2`                  | `--trn-text-muted`      |
| `--trn-gray-3`                  | `--trn-text-subtle`     |
| `--trn-gray-4`                  | `--trn-border`          |
| `--trn-gray-5`                  | `--trn-surface-strong`  |
| `--trn-gray-6`                  | `--trn-border-subtle`   |
| `--trn-gray-light`              | `--trn-surface-muted`   |
| `--trn-border-inner`            | `--trn-radius-sm`       |
| `--trn-border-outer`            | `--trn-radius-md`       |
| `--trn-border-full`             | `--trn-radius-full`     |

One difference worth knowing: the old grey scale was a set of fixed colours, while the new derived colours are computed live from your foreground and background — so they now stay in step when you change your base palette, instead of needing to be re-picked.

## 2. Custom CSS

Two habits make your CSS reliable:

* **Start every rule with `#trn-app`** so it can only affect the widget, never the rest of your page.
* **Target the supported styling hooks below** — they are stable across releases.

### Recipes

```css
/* Restyle the main button */
#trn-app .trn-button {
  background: #0f4c81;
  border-radius: 6px;
}

/* The secondary (outlined) button */
#trn-app .trn-button.trn-button--outlined {
  border-width: 2px;
}

/* Product cards */
#trn-app .trn-product-card {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

/* Headings inside the widget */
#trn-app .h2 {
  letter-spacing: 0.02em;
}

/* Reuse your theme colours anywhere — no wrapping needed */
#trn-app .trn-alert {
  border-color: var(--trn-error);
}

/* Modal dialogs */
#trn-app .trn-modal__card {
  border-radius: 10px;
}
```

### Replacing the start-page card artwork

The cards on the start page ("trade in from your order history" / "registered products") ship with built-in illustrations. You can swap in your own artwork with custom CSS alone — no upload to Tern needed, just an image hosted at a public URL. If you're on Shopify, **Content → Files** in your Shopify admin gives you a CDN URL in two clicks.

Any image format the browser can load works: **SVG, PNG, WebP, JPEG or GIF**. SVG or a transparent PNG will look best against the card background.

```css
/* 1. Hide the built-in illustration (its 80×80px box stays as the frame) */
#trn-app .trn-start-card__icon path { display: none; }

/* 2. Show your own image inside that frame */
#trn-app .trn-start-card__icon {
  background: url(https://cdn.shopify.com/s/files/.../my-artwork.svg) center / contain no-repeat;
}
```

To size the frame differently, set `width` and `height` on `.trn-start-card__icon` in the same rule.

**A different image per card** — the register / product-catalogue card carries a `.trn-start-card--register` modifier:

```css
#trn-app .trn-start-card__icon path { display: none; }

/* order-history card */
#trn-app .trn-start-card .trn-start-card__icon {
  background: url(.../history.svg) center / contain no-repeat;
}

/* registered-products card (keep this rule after the one above) */
#trn-app .trn-start-card--register .trn-start-card__icon {
  background: url(.../register.svg) center / contain no-repeat;
}
```

**A different image per section** — if you have both trade-ins and repairs enabled, the two start pages share the same cards. To vary the artwork by section, scope the rule with the card-row hooks `.trn-start-cards--trade-in` and `.trn-start-cards--repairs`:

```css
#trn-app .trn-start-cards--repairs .trn-start-card__icon {
  background: url(.../repairs.svg) center / contain no-repeat;
}
```

**Matching the theme colour** — the built-in illustrations are drawn in your foreground colour. If you want your artwork tinted the same way, use it as a `mask` instead of a background: the image's own colours are ignored and its shape is filled with the current text colour (`currentColor`). This variant needs an image with transparency (SVG or transparent PNG/WebP — a JPEG won't work as a mask):

```css
#trn-app .trn-start-card__icon path { display: none; }

#trn-app .trn-start-card__icon {
  background-color: currentColor;
  mask: url(.../my-artwork.svg) center / contain no-repeat;
}
```

One thing to keep in mind: the image stays on your server, so if it's ever deleted or moved the cards will show an empty space until the URL is fixed.

### Supported styling hooks (stable)

**Naming pattern:** hook names follow one grammar, so once you know it you can predict a name before you look it up. A block starts with the `trn-` prefix, e.g. `.trn-product-card`; a part inside it is the block name plus `__element`, e.g. `.trn-product-card__image`; a variant or state is the block name plus `--modifier`, e.g. `.trn-button--outlined` or `.trn-option--selected`. States are always modifiers, never bare classes — look for `--selected` / `--open` / `--active`, not `.selected` / `.open` / `.active`.

**Finding the right hook:** you don't have to work from this page alone. In the Tern admin, the **CSS tab** of the Copy Customisation screen has a built-in hook viewer — hover over any part of the preview and it highlights that element and the containers around it, listing the hooks on each, so you can see exactly what to target before you write the rule.

The hooks are grouped in the order your customers meet them — the path through the widget first, then the pieces that appear on every page. Each area separates **layout wrappers** — structural containers you'd target for spacing, width or positioning — from **components** — the visible pieces you'd restyle. Every hook has its own entry; simple ones list their parts inline, and anything richer gets a small table breaking its parts down by kind: **Element** — a `__` part inside it, **Variant** — a `--` kind it comes in, **State** — a `--` condition it can enter.

#### The widget at a glance

```
#trn-app                          widget root — start every rule here
└─ .trn-app-wrapper               main layout
   ├─ .trn-navbar                 top navigation
   ├─ (the current page)
   │   ├─ start page:             .trn-start-cards › .trn-start-card
   │   ├─ item picker:            .trn-pick-your-items › .trn-card-grid › .trn-product-card
   │   ├─ checkout steps:         .trn-wizard › .trn-checkout (one step modifier each)
   │   │                             ├─ .trn-cart-items / .trn-checkout-summary
   │   │                             └─ .trn-summary-sidebar
   │   ├─ outcome pages:          .trn-checkout--outcome › .trn-outcome
   │   └─ history & details:      .trn-history-header, .trn-accordion, .trn-details-card
   └─ .trn-footer                 footer

overlays:          .trn-modal — dialog chrome; the questions flow (.trn-questions) opens in one
outside #trn-app:  .trn-refund-modal — the refund widget's popup chrome (see below)
```

The nesting here is indicative — it shows which hooks you'll find within which regions, not the exact markup (extra wrappers between them can change; see [What not to target](#what-not-to-target)). The class names themselves are the stable contract.

#### Start page

**Layout wrappers**

**`.trn-start-cards`** — the row holding the start-page cards

* **Variants:** `.trn-start-cards--trade-in` / `.trn-start-cards--repairs` — which section's start page the row is on

**Components**

**`.trn-start-card`** — one start-page card

* **Elements:** `.trn-start-card__icon` — the card's illustration
* **Variants:** `.trn-start-card--register` — the registered-products card

#### Picking items

**Layout wrappers**

**`.trn-pick-your-items`** — the item-picker page

| Kind    | Hook                            | What it is                 |
| ------- | ------------------------------- | -------------------------- |
| Variant | `.trn-pick-your-items--orders`  | The order-history view     |
| Variant | `.trn-pick-your-items--catalog` | The product-catalogue view |
| Variant | `.trn-pick-your-items--repairs` | The repairs view           |

**`.trn-card-grid`** — the grid a set of cards is laid out in (order history, registered products, repairs)

**`.trn-catalog`** — the register-a-product / product-catalogue browser

**Components**

**`.trn-product-card`** — a product card

| Kind    | Hook                                  | What it is                                        |
| ------- | ------------------------------------- | ------------------------------------------------- |
| Element | `.trn-product-card__image`            | The product image                                 |
| Element | `.trn-product-card__info`             | The card's text area                              |
| Element | `.trn-product-card__title`            | The product title                                 |
| Element | `.trn-product-card__price`            | The price area                                    |
| Element | `.trn-product-card__variant`          | The variant/option title                          |
| Element | `.trn-product-card__date`             | The purchase date                                 |
| Element | `.trn-product-card__check`            | The selected checkmark badge                      |
| Element | `.trn-product-card__unavailable-note` | The "not eligible" note                           |
| Variant | `.trn-product-card--placeholder`      | A loading skeleton card                           |
| Variant | `.trn-product-card--prompt`           | The register-a-product / browse-more prompt cards |
| State   | `.trn-product-card--selected`         | While the card is selected                        |
| State   | `.trn-product-card--unavailable`      | When the item isn't eligible                      |

**`.trn-carousel`** — the featured-products carousel

| Kind    | Hook                       | What it is                  |
| ------- | -------------------------- | --------------------------- |
| Element | `.trn-carousel__title`     | Its heading                 |
| Element | `.trn-carousel__nav`       | The prev/next arrow buttons |
| Variant | `.trn-carousel__nav--prev` | The previous arrow          |
| Variant | `.trn-carousel__nav--next` | The next arrow              |

**`.trn-thumb`** — the square product/item thumbnail, wherever one appears (cards, cart rows, the summary sidebar)

**`.trn-category-card`** — the category browse cards

**`.trn-breadcrumbs`** — the catalogue breadcrumb trail

* **Elements:** `.trn-breadcrumbs__item` — each crumb

**`.trn-search`** — the product search bar

* **Elements:** `.trn-search__suggestions` — the dropdown panel; `.trn-search__suggestion` — each row in it

**`.trn-actions-bar`** — the selection counter / next-step bar under the item-picker grids

* **Elements:** `.trn-actions-bar__info` — its text

#### Condition questions

**Layout wrappers**

**`.trn-option-group`** — a group of answer options

* **Elements:** `.trn-option-group__header` — its heading

**Components**

**`.trn-questions`** — the condition-questions dialog (trade-ins and repairs)

| Kind    | Hook                                    | What it is                           |
| ------- | --------------------------------------- | ------------------------------------ |
| Element | `.trn-questions__progress`              | The step progress bar                |
| Element | `.trn-questions__progress-step`         | One step in the bar                  |
| Variant | `.trn-questions--compact`               | A tighter presentation of the dialog |
| State   | `.trn-questions__progress-step--active` | The current step                     |

**`.trn-question`** — one question

**`.trn-option`** — an individual answer pill or variant swatch

* **Elements:** `.trn-option__check` — the checkmark badge
* **States:** `.trn-option--selected`

**`.trn-item-summary`** — the "your item" recap panel shown alongside the questions

#### Checkout steps

**Layout wrappers**

**`.trn-checkout`** — the two-column shell every checkout step and the outcome pages sit in

| Kind    | Hook                          | What it is               |
| ------- | ----------------------------- | ------------------------ |
| Variant | `.trn-checkout--cart`         | The cart step            |
| Variant | `.trn-checkout--address`      | The contact-details step |
| Variant | `.trn-checkout--shipping`     | The shipping step        |
| Variant | `.trn-checkout--confirmation` | The confirmation step    |
| Variant | `.trn-checkout--outcome`      | The outcome pages        |

> `.trn-shipping`, previously listed on the checkout shell, has been removed — it duplicated `.trn-checkout` on every step. Use `.trn-checkout--shipping` to reach the shipping step only.

**`.trn-cart-items`**, **`.trn-checkout-summary`**, **`.trn-summary-sidebar`** — the three checkout regions inside the shell

**`.trn-summary-list`** — the itemised list inside the order summary and the reward-choice cards

**`.trn-carrier-options`** — the shipping-options list

**Components**

**`.trn-checkout-item`** — a cart/confirmation line-item row

| Kind    | Hook                          | What it is               |
| ------- | ----------------------------- | ------------------------ |
| Element | `.trn-checkout-item__name`    | The item name            |
| Element | `.trn-checkout-item__variant` | The variant/option title |
| Element | `.trn-checkout-item__price`   | The price                |
| Element | `.trn-checkout-item__actions` | The row's actions area   |

**`.trn-summary-total`** — the total row beneath the summary list

**`.trn-reward-choice`** — the cash-or-product reward picker

* **Elements:** `.trn-reward-choice__option` — each choice
* **States:** `.trn-reward-choice__option--selected` — the chosen one

**`.trn-carrier-option`** — one row in the shipping-options list

* **States:** `.trn-carrier-option--selected` — the chosen row

**`.trn-cart-badge`** — the wizard's cart button

* **Elements:** `.trn-cart-badge__count` — the item-count bubble

**`.trn-panel`** — the bordered panel each block of wizard content sits in (cart, contact details, shipping, confirmation); also used by collapsible sections

#### Outcome pages

**`.trn-outcome`** — the block wrapping the "thank you" / "requested" / "rejected" outcome pages (inside the `.trn-checkout--outcome` shell)

| Kind    | Hook                      | What it is                                     |
| ------- | ------------------------- | ---------------------------------------------- |
| Element | `.trn-outcome__title`     | The icon + heading row                         |
| Element | `.trn-outcome__reference` | The trade-in reference row, on all three pages |
| Element | `.trn-outcome__steps`     | The numbered next-steps list                   |
| Element | `.trn-outcome__labels`    | The shipping-label downloads                   |
| Element | `.trn-outcome__contact`   | The "contact us" text on the rejected page     |

#### History & trade-in details

**Layout wrappers**

**`.trn-products-list`** — the item list inside a history accordion entry

**Components**

**`.trn-history-header`** — the page header on the History and trade-in details pages

**`.trn-accordion`** — collapsible sections; each history entry is one

| Kind    | Hook                      | What it is               |
| ------- | ------------------------- | ------------------------ |
| Element | `.trn-accordion__header`  | The clickable header row |
| Element | `.trn-accordion__content` | The collapsible body     |
| State   | `.trn-accordion--open`    | While expanded           |

**`.trn-item-row`** — an item row inside a history accordion entry or a details card

**`.trn-badge`** — the status badge on history/details pages

| Kind    | Hook                  | What it is           |
| ------- | --------------------- | -------------------- |
| Variant | `.trn-badge--success` | Success statuses     |
| Variant | `.trn-badge--error`   | Error statuses       |
| Variant | `.trn-badge--warning` | Waiting statuses     |
| Variant | `.trn-badge--info`    | In-progress statuses |

**`.trn-details-card`** — the info cards on the confirmation, details and summary pages; one purpose variant per card

| Kind    | Hook                          | What it is                |
| ------- | ----------------------------- | ------------------------- |
| Element | `.trn-details-card__header`   | Its title row             |
| Variant | `.trn-details-card--shipping` | The shipping card         |
| Variant | `.trn-details-card--items`    | The items card            |
| Variant | `.trn-details-card--tracking` | The tracking card         |
| Variant | `.trn-details-card--credit`   | The credit/reward card    |
| Variant | `.trn-details-card--exchange` | The exchange card         |
| Variant | `.trn-details-card--service`  | The service-cost card     |
| Variant | `.trn-details-card--summary`  | The summary card          |
| Variant | `.trn-details-card--customer` | The customer-details card |

**`.trn-tracking`** — the return-tracking timeline

| Kind    | Hook                         | What it is              |
| ------- | ---------------------------- | ----------------------- |
| Element | `.trn-tracking__event`       | One timeline entry      |
| Element | `.trn-tracking__status`      | The entry's status      |
| Element | `.trn-tracking__date`        | The entry's date        |
| Element | `.trn-tracking__description` | The entry's description |

**`.trn-tracking-number`** — the tracking-number row above the timeline

The remaining hooks aren't tied to one page — they appear throughout the widget.

#### Buttons & forms

**`.trn-button`** — buttons

| Kind    | Hook                    | What it is                     |
| ------- | ----------------------- | ------------------------------ |
| Element | `.trn-button__spinner`  | The in-button loading spinner  |
| Variant | `.trn-button--outlined` | The secondary (outlined) style |
| Variant | `.trn-button--sm`       | Small                          |
| Variant | `.trn-button--bold`     | Bold label                     |

**`.trn-input-group`** — form fields

**`.trn-checkbox`** — checkboxes

**`.trn-validation`**, **`.trn-validation-error`** — form validation messages

#### Money display

**`.trn-reward-value`** — what you get for an item, however it's expressed: a formatted money amount, or a text description for a non-monetary reward (e.g. "20% off")

* **Elements:** `.trn-reward-value__terms` — the terms-and-conditions asterisk

**`.trn-amount`** — any formatted currency figure, wherever it appears (history credit totals, sidebar totals, item prices)

These two overlap by design: a monetary reward carries both classes at once (`.trn-reward-value.trn-amount`) — one selector styles all money, the other styles all rewards.

#### Dialogs

**`.trn-modal`** — dialogs

| Kind    | Hook                               | What it is                                           |
| ------- | ---------------------------------- | ---------------------------------------------------- |
| Element | `.trn-modal__backdrop`             | The backdrop                                         |
| Element | `.trn-modal__card`                 | The dialog box                                       |
| Element | `.trn-modal__title`                | The title                                            |
| Element | `.trn-modal__content`              | The content area                                     |
| Element | `.trn-modal__footer`               | The footer                                           |
| Element | `.trn-modal__close`                | The close button                                     |
| Variant | `.trn-modal--fixed`                | A dialog fixed to the viewport                       |
| Variant | `.trn-modal--absolute`             | A dialog positioned within the page                  |
| Variant | `.trn-modal__card--fit-content`    | A dialog sized to its content                        |
| Variant | `.trn-modal__content--fit-content` | A dialog sized to its content                        |
| Variant | `.trn-modal--refund-questions`     | The questions dialog opened inside the refund widget |

Use the variants when you want a rule to apply to only one kind of dialog.

#### Chrome & feedback

**Frame**

**`.trn-navbar`** — the widget's top navigation

* **Elements:** `.trn-navbar__inner` — its inner wrapper; `.trn-navbar__item` — each link

**`.trn-footer`** — the widget's footer

**`.trn-wizard`** — the chrome around each wizard step (title, progress bar, back link)

| Kind    | Hook                        | What it is                      |
| ------- | --------------------------- | ------------------------------- |
| Element | `.trn-wizard__title`        | The step title                  |
| Element | `.trn-wizard__progress`     | The progress-bar track          |
| Element | `.trn-wizard__progress-bar` | The filled portion of the track |

**`.trn-back-btn`** — the wizard's Back link

**Feedback**

**`.trn-alert`** — notification banners

| Kind    | Hook                   | What it is             |
| ------- | ---------------------- | ---------------------- |
| Element | `.trn-alert__icon`     | The severity icon      |
| Element | `.trn-alert__content`  | The message body       |
| Variant | `.trn-alert--info`     | Info severity          |
| Variant | `.trn-alert--success`  | Success severity       |
| Variant | `.trn-alert--warning`  | Warning severity       |
| Variant | `.trn-alert--error`    | Error severity         |
| Variant | `.trn-alert--fixed`    | Pinned to the viewport |
| State   | `.trn-alert--disabled` | While disabled         |

**`.trn-info-box`** — inline info callouts

**`.trn-loading`** — the loading state

* **Elements:** `.trn-loading__spinner`, `.trn-loading__text`

**`.trn-pagination-wrapper`** — pagination controls

* **States:** the current page and disabled arrows are targetable as plain `.active` / `.disabled` classes within it

#### Refund widget dialog

**`.trn-refund-modal`** — the refund widget's popup (the "trade in" dialog embedded on a product page); the class sits on the backdrop

| Kind    | Hook                         | What it is                  |
| ------- | ---------------------------- | --------------------------- |
| Element | `.trn-refund-modal__content` | The dialog box              |
| Element | `.trn-refund-modal__close`   | The close button            |
| Element | `.trn-refund-modal__body`    | The scrollable content area |

This dialog's chrome renders *outside* `#trn-app`, so rules for it can't start with `#trn-app` — start them with `.trn-refund-modal` instead. Everything *inside* the body is the normal Returns Centre widget, so any rule targeting something in there starts with `#trn-app` as usual.

#### Text styles

`.h1`, `.h2`, `.h3`, `.h4`, `.subtitle`, `.caption`, `.button-text`, `.strong`.

### What not to target

Alongside the styling hooks above you'll see lots of short, generated utility classes in the widget's HTML — things like `mt-6`, `w-12`, `flex`, `uppercase`. If you use Tailwind CSS yourself you'll recognise these; they are output of our build tooling, **not** a stable API. They can appear, disappear or move to different elements in any release, without notice.

The FilePond file-upload widget's `filepond--*` classes are third-party DOM and are not covered by this contract either.

```css
/* ✗ fragile — can break on any widget update */
#trn-app .mt-6.mb-8 { text-align: center; }

/* ✓ stable — supported hook + plain element */
#trn-app .trn-start-card h2 { text-align: center; }
```

Also avoid relying on the exact wrapper structure *between* supported elements (extra `div`s, element types) — it can change as we improve the widget.

**Missing a hook?** If there's something you want to restyle and no supported hook reaches it, tell us at <support@tern.eco> — adding a stable class for you is quick and safe, and much better than either of us relying on generated names. Check the [component-level theming](#component-level-theming) table too: a lot of what used to need a hook is now a one-line variable instead.

## 3. Tips

* The widget's sizing is pixel-based on purpose — it does not use `rem` units, so it will not be affected by your theme's root `font-size`. We recommend px units in your custom CSS for the same predictability.
* The Copy Customisation screen previews your CSS exactly the way the live storefront applies it — check there before publishing.
* Keep a copy of your custom CSS in your own records; it's the quickest way to review what you've changed if you redesign later.


