Cloudhooks Help

OAuth connections

Many APIs no longer accept a static API key. They want an OAuth access token that expires after an hour or a few days, and has to be requested again from a token endpoint. OAuth connections let Cloudhooks do that work for you: you enter the OAuth client's details once, and every run of your hook receives a valid access token in context.oauth.

Note: OAuth connections are available on the plans that include Environment variables: paid plans and the trial.

How OAuth connections work

An OAuth connection is a named OAuth credential. Cloudhooks uses it to request an access token with the client credentials grant, caches the token, renews it in the background before it expires, and hands it to your hook on every run.

You can define connections at two levels, just like environment variables:

Store connections: Defined in Settings and available to all hooks in your store. Use these for an API that several hooks call.

Hook connections: Defined on a single hook. A hook connection overrides the store connection of the same name, for example when one hook needs a different OAuth client or scope.

Your hook reads the token by the connection's name:

module.exports = async function(payload, actions, context) {
  const { data } = await actions.http.get('https://api.example.com/v1/items', {
    headers: { Authorization: `Bearer ${context.oauth.myapi.accessToken}` }
  });
}

Your hook code never fetches or renews tokens itself, and it never sees the client secret. Cloudhooks doesn't add the token to your API requests automatically: your code decides where it goes, usually the Authorization header. Reading context.oauth doesn't count as an action.

Only the client credentials grant is supported. This is the flow for server-to-server access, where Cloudhooks authenticates as your OAuth client. There is no "Connect account" or sign-in step, and no redirect or callback URL to register with the service. See Supported setups.

Setting up a connection

Store connections

  1. Navigate to your Settings page
  2. Find the "OAuth connections" section, below "Environment variables"
  3. Click Add connection and fill in the form

Hook connections

  1. Open the hook and go to the Settings tab
  2. Scroll to the "OAuth connections" section. The section is available once the hook has been saved at least once.
  3. Click Add connection and fill in the form

To override a store connection for this hook, give the hook connection the same name. You can do that with Add connection, or with Override for this hook on the store connection, which fills in its name and locks it.

  • The hook connection gets an Overrides store connection badge, so you can see that a store connection of the same name exists. Its first action, Manage the store connection in Settings (the gear icon), takes you to that store connection, followed by the usual actions of a hook connection.
  • The store connection it overrides is no longer listed on the hook. Delete the hook connection, and the store connection is listed and used again.
  • The other store connections are listed under Inherited from store connections, with the Manage in Settings, Show auth header code snippet and Override for this hook actions. This section is hidden when there are none.

Hook connections are saved on their own when you save the connection form. You don't need to save the hook.

The connection form

The form marks the fields you have to fill in with an asterisk. Extra parameters is optional, and the two dropdowns always have a value. When you edit a connection, the client secret is required only in the case described below the table.

FieldWhat to enter
NameThe name your hook uses: context.oauth.<name>. It must be a valid JavaScript identifier (letters, digits and _, not starting with a digit, at most 64 characters), so use zendesk_eu, not zendesk-eu. Names are case-sensitive and must be unique at their level.
Token URLThe service's token endpoint, for example https://<subdomain>.zendesk.com/oauth/tokens. It must start with https://, point to a public address, and can't contain a user name or password.
Client IDThe OAuth client's ID.
Client secretThe OAuth client's secret, at least 8 characters. It is stored encrypted and never shown again after saving.
ScopeThe scopes to request, separated by spaces, as the service names them, for example tickets:read tickets:write. Required: without a scope, many services, Zendesk among them, issue a token that can't call anything.
Send the client credentials inWhere the client ID and secret go: the Request body (default) or an HTTP Basic header. Check the service's documentation.
Request encodingHow the token request is encoded: Form-encoded (OAuth standard) (default, used by most services) or JSON (for example Zendesk).
Extra parametersOptional. Additional parameters for the token request, one name=value per line, for example expires_in=172800. At most 20 parameters and 4,096 characters. The names grant_type, client_id, client_secret and scope are set by Cloudhooks and can't be used. Extra parameters stay visible in the form and are not treated as secrets: put secrets in the client secret field.
Fallback token lifetime (seconds)How long a token lives when the service doesn't say: between 60 seconds and 30 days (2,592,000 seconds), 3,600 by default. Used only when the token response has no expires_in.

When you edit a connection, leave the client secret empty to keep the stored one. If you change the token URL, the client ID or where the credentials are sent, you have to enter the client secret again, so a stored secret is never sent somewhere it wasn't entered for.

Verification and Save anyway

Saving a connection, new or edited, verifies it right away by requesting a token. If the service issues one, the connection is saved and shows as Working.

If the service rejects the request, you see what it responded, for example The service responded with HTTP 401: invalid_client - Client authentication failed., with a hint for the usual causes: the token URL for HTTP 404, the client ID and secret for HTTP 401 or invalid_client, the scope for invalid_scope. If the service can't be reached, you see that instead. Either way, you choose what happens:

  • Save anyway: the settings are saved, and the connection shows the failure until a token request succeeds.
  • Keep the previous version (only when editing): your changes are discarded, and the connection keeps working with its previous settings and token.
  • Cancel: nothing is saved, and you go back to the form with your input kept.

A token URL that isn't https, or that points to a private or local address, is refused before any request is sent.

Saved changes take effect for the next run. The cached token is discarded, so runs never use a token requested with the old settings.

Connection status

Each connection shows its latest status and when its token was last refreshed:

  • Never fetched: the connection was saved with Save anyway and has never obtained a token.
  • Working: the most recent token request succeeded.
  • Failing: the most recent token request failed, at verification, at a background renewal or at the start of a run. Hover the badge to see when it failed and the service's error, and when the last token was fetched, if one ever was.

The service's error shows its HTTP status. The response body is shown only when it is a standard OAuth error (error and error_description), shortened, and with any secret or token in it masked.

The list refreshes when you come back to the browser tab.

Using a connection in your hook

Every run receives context.oauth, with one entry per connection available to the hook: the hook's own connections, and the store connections it doesn't override. To get the code with a connection's name filled in, choose Show auth header code snippet on its row, then the copy icon in the top-right corner of the code.

Each entry has these properties:

accessToken: string | nullThe access token, or null when no token is available
tokenType: string | nullThe token type the service returned, usually Bearer
expiresAt: string | nullWhen the token expires, as an ISO date string

Send the token in the Authorization header, and check that the connection and its token are there first:

module.exports = async function(payload, actions, context) {
  const zendesk = context.oauth.zendesk;
  if (!zendesk) {
    // No connection named "zendesk" is available to this hook: nothing else logs this.
    console.error('No "zendesk" OAuth connection is available to this hook.');
    return;
  }
  const token = zendesk.accessToken;
  // No token: Cloudhooks has already logged the reason in the run log.
  if (!token) return;
 
  const { data } = await actions.http.get('https://acme.zendesk.com/api/v2/tickets.json', {
    headers: { Authorization: `Bearer ${token}` }
  });
  console.log(`Found ${data.tickets.length} tickets`);
}

Test runs and replays receive the token the same way as live runs. A test run is a real execution: it calls the API with the token, just like a live run.

When the token is null

A run always starts, even when no token is available. The connection's entry is still there, but accessToken is null:

  • The token couldn't be fetched. If no unexpired token is cached when a run starts, Cloudhooks makes one last attempt to fetch it, unless a retry is already scheduled after recent failures. If that fails, the run's log shows an error that names the connection and shows the service's response, for example OAuth connection "zendesk" has no access token for this run: the token request failed: …. The run is marked failed, even when your hook handles the missing token itself, and you get the hook's failure notification if it's turned on.
  • Your plan doesn't include OAuth connections. No token is requested, and the run's log has a line that names the plan restriction. The run isn't marked failed because of it.

Check the token before you use it, as in the example above, so your hook doesn't send Bearer null to the API. You don't need to log anything yourself when it's null: the run log already says why.

A connection that doesn't exist is different. If no connection with that name is available to the hook, for example because the name is mistyped or the connection was deleted, context.oauth.<name> is undefined and nothing is logged. Log an error for it yourself, as the example does.

Tokens in logs

Access tokens, client secrets and extra parameter values are masked in your run logs, in live runs and in test runs alike. Unlike environment variables, they are not shown in plain text in test runs. See Secret redaction.

Masking is best-effort: if your code transforms a token before logging it, for example by encoding it, the transformed value may not be caught, and extra parameter values shorter than 8 characters are not masked. Don't log tokens.

Token renewal

Cloudhooks renews tokens in the background, whether or not your hooks run, so a hook that runs after a quiet weekend still gets a valid token without waiting for one.

  • Renewal starts at least 10 minutes before the token expires. For long-lived tokens it starts earlier, when a quarter of the lifetime is left: 12 hours before expiry for a 48-hour token. Tokens that live 20 minutes or less are renewed halfway through their lifetime.
  • A failed renewal is retried after 1, 2, 5, 15 and 30 minutes, then every hour, until a request succeeds. The connection's status shows the failures. Runs keep receiving the cached token while it is unexpired, and their logs say nothing about the failed renewal.
  • No email is sent when a renewal fails. Check the connection's status, or rely on the hook's failure notifications, which fire once runs start without a token.
  • Concurrent runs share one token. Cloudhooks doesn't request a token per run.

Rejected tokens and Renew token

Sometimes a service rejects a token before it expires, for example after you revoke it. When an HTTP action sends a connection's token and gets a 401 response, Cloudhooks drops that token, and the next run gets a new one. This happens at most once a minute per connection. The run that got the 401 still sees it and isn't retried.

To replace a token right away, choose Renew token on the connection. Cloudhooks requests a new token immediately and shows the result the way verification does. The cached token is replaced only when the new one arrives, so if the request fails, runs keep using the cached token while it is unexpired.

Deleting a connection

Choose Delete connection on its row. The client secret and the cached token are deleted, and this can't be undone.

  • Deleting a store connection removes context.oauth.<name> from every hook that inherits it.
  • Deleting a hook connection that overrides a store connection makes the hook use the store connection again.
  • Deleting a hook deletes its connections.

Deleting a connection doesn't revoke the OAuth client or its tokens at the service. Revoke the client, or rotate its secret, there when you no longer use it.

Limits and plans

  • A store can have at most 10 store connections, and each hook at most 10 hook connections.
  • Saving a connection and Renew token both request a token, so they are rate-limited: at most 10 per store per minute, and one Renew token per connection every 10 seconds. Over the limit, you see "Too many token requests. Please try again in N seconds."
  • OAuth connections are available on the plans that include Environment variables. On the Free plan, the OAuth connections section shows an upgrade message, and no connection can be created.
  • If your store moves to a plan without the feature, your connections are kept. They are shown read-only, you can still delete them, no tokens are requested, and they work again unchanged after you upgrade.
  • If you uninstall Cloudhooks, cached tokens are deleted. If you chose to preserve your data on uninstall, the connections are kept and fetch new tokens after you reinstall.

Supported setups

OAuth connections support the client credentials grant with:

  • Client authentication: client ID and secret in the request body, or in an HTTP Basic header
  • Request encoding: form-encoded or JSON
  • Extra parameters: added to the token request body
  • Response: JSON with access_token. token_type and expires_in are optional. Without expires_in, the form's Fallback token lifetime applies. Lifetimes are capped at 30 days.

These are not supported:

  • Other grants, such as the authorization code flow where you sign in to "connect" an account
  • Private-key JWT or mutual-TLS client authentication
  • DPoP-bound tokens
  • Token endpoints that answer with something other than JSON
  • Token endpoints that redirect: redirects are not followed
  • Token URLs that aren't https or that point to a private or local address
  • Sharing connections between stores: each store sets up its own

Example: Zendesk

Zendesk is retiring API tokens: new ones can no longer be created, and existing ones stop working on April 30, 2027. Here's how to call the Zendesk API with an OAuth connection instead.

1) Create an OAuth client in Zendesk

  1. In Zendesk Admin Center, go to Apps and integrations → APIs → OAuth clients, and add a client.
  2. Set its kind to Confidential, so it can use the client credentials grant.
  3. Zendesk asks for a Redirect URL. The client credentials grant doesn't use it, so enter any https address, for example https://www.cloudhooks.dev.
  4. Save the client and copy its identifier and secret. Zendesk shows the secret only once.

2) Create the connection in Cloudhooks

In Settings, add an OAuth connection with:

FieldValue
Namezendesk
Token URLhttps://<subdomain>.zendesk.com/oauth/tokens, with your Zendesk subdomain
Client IDThe client's identifier
Client secretThe client's secret
ScopeThe scopes your hook needs, for example tickets:read, or users:write to create and update users. Use resource scopes: Zendesk doesn't grant the broad read and write scopes to this kind of client.
Send the client credentials inRequest body
Request encodingJSON
Extra parametersexpires_in=172800

The expires_in extra parameter asks Zendesk for a token that lives 48 hours (172,800 seconds). Cloudhooks renews it in the background 12 hours before it expires. Without it, Zendesk issues tokens that last 30 minutes.

If an API call fails with a 403 error saying You are missing the following required scopes, the error names the scopes to add to the connection's Scope field.

Save the connection. Verification requests a token from Zendesk, and the connection shows as Working.

3) Call the Zendesk API from your hook

module.exports = async function(payload, actions, context) {
  const zendesk = context.oauth.zendesk;
  if (!zendesk) {
    console.error('No "zendesk" OAuth connection is available to this hook.');
    return;
  }
  const token = zendesk.accessToken;
  // No token: Cloudhooks has already logged the reason in the run log.
  if (!token) return;
 
  const { data } = await actions.http.get('https://<subdomain>.zendesk.com/api/v2/tickets.json', {
    headers: { Authorization: `Bearer ${token}` }
  });
  console.log(`Found ${data.tickets.length} tickets`);
}

Replace <subdomain> with your Zendesk subdomain, or keep the base URL in an environment variable. For a complete hook, see Sync Shopify customer data to Zendesk.