Troubleshooting

Find what you're seeing, work out which cause it is, and fix it.

Most integration problems don't produce an error. The widget stays empty, Submit succeeds with nothing to issue, or a webhook never arrives. Find what you're seeing below. Each section starts with a quick check that tells you which cause you have.

If you have a widget error code, go straight to Widget error codes. For an API error, see the endpoint's errors in the API reference and Error handling.

The widget doesn't show

Check first. Open your browser's developer tools on the checkout page and reload.

  1. In the Network tab, look for teak.js from icw.helloteak.com, then a request to api.helloteak.com (or api.sandbox.helloteak.com) for the quote.
  2. In the Console, run customElements.get('teak-widget'). It returns undefined if the widget never started.
What you seeCause
No teak.js request, or it failedThe script didn't load
teak.js loaded, no quote request, customElements.get(...) is undefinedconfigure was rejected
Console says Configuration is not validNo widget element on the page
Console shows E4025, E4026, or E4027An item is missing a field
The quote request returns 401 or 403Keys and environment don't match
The quote request succeeds, but the widget shows nothingA product type isn't mapped
The quote request succeeds, but there's no offerNo offer for this cart
A loading state that never finishesA request never completed
The widget is in the page's HTML, but you can't see itYour CSS hides it

The script didn't load

A blocked or failed teak.js is the one failure the widget can't report, because none of its code runs.

  • If your site sets a Content Security Policy, allow https://icw.helloteak.com in script-src. See Content Security Policy.
  • Browser extensions such as ad blockers can block the script. Test in a private window with extensions off.
  • Use the loader script exactly as given. It loads teak.js with type="module". Without it, the browser reports a SyntaxError and the widget never starts.
  • Detect it in production with the loader's onerror handler, and let checkout continue without protection.

configure was rejected

If configure fails basic checks, the widget doesn't start. It might not log a message or call onErrorCb, so check your call against this list:

  • apiKey is missing or empty.
  • items is missing or empty, for example because configure ran before your cart loaded.
  • An item has no cost, or its cost is 0. Leave free items out of items. Teak doesn't protect items with no price.
  • A cost isn't a number or a numeric string, for example '$120' or '19,99'.
  • The call passes costsOfItems, an option from Teak's earlier widget.

See Item fields for what each item needs.

No widget element on the page

configure needs a <teak-widget> element on the page when it runs. If there isn't one, the console says Configuration is not valid and onErrorCb is called.

  • With a framework such as React or Vue, call configure after the component that contains the element has rendered.
  • Check the tag name. A misspelled tag, such as <element-checkout>, is ignored.

Keys and environment don't match

Sandbox keys need sandbox: true, and production keys need it left out.

sandbox must be the boolean true, not the string 'true'. A string is ignored with a console warning that the option has an invalid type, and the widget calls production with your sandbox key.

A product type isn't mapped

Product types are your inventory system's labels, which Teak maps to protection during onboarding: each item's productTypes, or the id of the widget elements on the page for items without their own. If you send one Teak hasn't mapped for your account, the widget shows nothing, without an error or a console message.

An id you added for another reason, such as styling, is read as a product type too. Style the element with a class instead. If you've started selling a new kind of product, ask your Teak integration contact to map its product type.

While a mismatched element is on the page, teak.isFormComplete() returns false, so a checkout that requires a choice can't continue.

No offer for this cart

Your account may have no offer for these items, for example because they can't be covered or the product isn't set up yet. Check with your Teak integration contact.

After your account's setup changes, a browser that has already loaded the widget can keep the old setup for up to 120 minutes. To see the change straight away, clear the site's data in your browser's developer tools, or test in a private window.

A request never completed

The widget doesn't give up on a slow network, so it can stay loading without calling loadedCb or onErrorCb. Don't let checkout wait for it: add protection only when optInCb fires, and treat "no loadedCb" as "no offer". See When there's no error but no offer.

Your CSS hides it

The widget renders inside its element. If the element or one of its parents has display: none, zero height, or overflow: hidden, the widget is there but invisible. Inspect the element in developer tools to check its size.

Submit returns 204 but the customer opted in

A 204 means that when Submit reached Teak, the session had no saved opt-in. The widget saves the customer's choice to the session in the background, and Teak only knows about choices that were saved.

Check first. In the Network tab, opt in to protection and look for a PATCH request to .../orders/<session ID>/items.

  • No PATCH, or it failed: the choice wasn't saved. See the first two causes below.
  • The PATCH succeeded: compare the session ID in its URL with the value of teak.get('token') your page sends to your server. If they differ, see You submitted a different session.

The choice couldn't be saved

If saving fails, the widget removes itself, then calls optOutCb and onErrorCb. If your page only listens for optInCb, it still shows the customer as protected, and charges them for protection Teak won't issue.

Handle optOutCb as well as optInCb, and remove the protection price when either optOutCb or onErrorCb fires. See Handle the customer's choice.

Submit arrived before the choice was saved

optInCb fires as soon as the customer chooses, before the save finishes. If your server submits the order in that moment, Teak closes the session first, rejects the save, and Submit returns 204.

This is rare in a normal checkout, where payment takes longer. It's more likely if the customer can pay in one click straight after choosing. Submit the order only after your own order is confirmed.

You submitted a different session

  • You read the token too early. Read teak.get('token') when the customer places the order, not when the page loads. Before loadedCb fires, it can return a new ID that the widget doesn't use.
  • Your cart and checkout are on different domains. The session belongs to one domain, including the subdomain: www.example.com and checkout.example.com each have their own. Read the token on the page where the customer made their choice.
  • You reused the session after clearSession. After clearSession, teak.get('token') can keep returning the previous session until the page reloads. If your checkout is a single-page app, load the page again after resetting the session, before the next purchase.

A cart-line widget doesn't match a cart line

With one widget per cart line, item-index starts at 0. If it doesn't match an item in items, the offer can appear, but loadedCb never fires and the customer's choice isn't saved.

No Contract Issued webhook

Check first.

  1. Did Submit return 202? If it returned 204, nothing was issued. See Submit returns 204.
  2. Wait two minutes. Teak sends webhooks in batches every minute.
  3. Call Retrieve order details with your order number. If it still returns 404 after a few minutes, Teak didn't create the order: contact your Teak integration contact with the session ID and your order number.
  4. Check your server's access logs for requests to your webhook URL. The Teak Dashboard doesn't show delivery attempts.

Your endpoint returned an error

Teak treats any HTTP response as delivered, including 4xx and 5xx, and doesn't retry. If your logs show the request with an error response, the event is lost. Common causes:

  • CSRF protection. Frameworks such as Django and Rails reject POST requests without a CSRF token. Exempt your webhook route.
  • Authentication. Your endpoint requires a login or an API key that Teak doesn't send. Rely on the signature instead.
  • The signature check fails. See Signature never matches.

To stop losing events while you fix it, respond 2xx as soon as you've stored the request, and process it afterwards.

Your URL redirects

If your webhook URL redirects, for example from http to https, to add a trailing slash, or to add www, the POST becomes a GET without the body. Use the final URL exactly.

Teak couldn't reach your endpoint

If your endpoint doesn't respond within 10 seconds, can't be reached, or has an invalid TLS certificate, Teak retries for about 35 minutes and then drops the event. See Delivery.

The subscription doesn't match the event

  • The subscription must include Contract Issued.
  • Sandbox and production have separate dashboards and separate subscriptions. A sandbox order only sends to subscriptions in the sandbox dashboard.
  • The subscription must have a webhook URL, not only an email address.

The protection wasn't issued

Teak can put a contract on hold or reject it instead of issuing it. You then receive Contract On Hold or Contract Void instead of Contract Issued, but only if you subscribe to those events. Teak can also hold an order for review without sending any event. If nothing arrives within 15 minutes, ask your Teak integration contact to check the order.

Signature never matches

Check first. Run your verification function on the test values with the secret so-secret. If that fails, the bug is in your function. If it passes, the problem is the body or the secret your server uses.

The header isn't a signature

If X-Protecht-Signature starts with t: rather than sha256:, the subscription has no secret, so Teak isn't signing requests. Add a secret to the subscription.

Editing a subscription in the dashboard and clearing the hidden secret field removes the secret. Re-enter it before you save.

Your function doesn't use the raw body

Teak signs the body exactly as sent. Anything that changes a single byte breaks the signature:

  • Parsing and re-serializing. JSON.stringify(req.body) doesn't reproduce Teak's spacing: Teak's body has a space after each : and ,. Read the raw bytes before any JSON parser runs.
  • Body parsers that run first. In Express, a global express.json() consumes the body before your route. Use express.raw({ type: 'application/json' }) on the webhook route. In Django, use request.body, not request.POST or Django REST Framework's request.data.
  • Decoding and re-encoding. Non-ASCII characters arrive escaped, for example é. Keep the body as bytes until you've verified it.

Your function reads the header wrongly

  • Remove the sha256: prefix and the space after it, and compare only the value.
  • The value is base64, not hex.

You're using the wrong secret

  • Use the secret exactly as you entered it in the dashboard, as UTF-8 text. Spaces are part of it. Don't base64- or hex-decode it.
  • Each subscription has its own secret, and sandbox and production subscriptions are separate. If several subscriptions send to the same URL, each signs with its own secret.
  • The dashboard doesn't show the secret again. If you aren't sure what it is, set a new one on the subscription and deploy it at the same time.

After you change the secret, retries of earlier events are signed with the new one. There's no overlap period, so deploy the new secret at the same moment you save it.

Other questions

Why do I get several Contract Issued webhooks for one order?

Teak sends one for each protected item. Match them to your items with data.reference_number, and to your order with data.order_number.

Why does Retrieve order details return 404 straight after Submit?

Teak creates the order a few seconds after Submit returns 202. Call it from your Contract Issued handler, or retry after a short wait. See After checkout.

The customer opted in on the cart page. Why do they have to choose again at checkout?

The widget remembers the choice across pages only with persist: true. See Showing the widget on more than one page.

Why does onErrorCb fire only once?

It's called at most once until the widget loads successfully again. If you call configure again and it fails again, you won't get a second call, so don't rely on onErrorCb to detect every failure.

Should checkout stop if the widget fails?

No. Let the customer complete checkout without protection, and don't submit that order to Teak. See Errors.

Next steps

Didn't find what you're seeing? Look up the error code:


Did this page help you?