OAuth 2.0 Webhook Destinations

Authenticate webhook deliveries with OAuth 2.0 client credentials or a private-key JWT, with Entra ID and NetSuite examples.

When the system receiving your webhooks is protected by an OAuth 2.0 identity provider, GAINS Connect can obtain an access token itself and send it with every delivery. This page explains the two OAuth 2.0 schemes of a webhook configuration's Authorization, how tokens are obtained and renewed, and how to set up two common identity providers. The rest of a webhook configuration is described in Configuring Webhooks. The other two schemes, Header and Basic, and how to choose between all four, are in Authorization.

How GAINS Connect obtains a token

Both OAuth 2.0 schemes use the client credentials grant. Before a delivery, GAINS Connect:

  1. Sends a POST to TokenUrl with a form body (application/x-www-form-urlencoded) carrying grant_type=client_credentials, the client's credentials and the other parameters your settings call for, such as scope.
  2. Reads the token from access_token, its type from token_type and its lifetime from expires_in in the JSON response.
  3. Sends the delivery with the header Authorization: <token_type> <access_token>. The type is normally Bearer, which is also what GAINS Connect uses when the response has no token_type.
POST /oauth2/token HTTP/1.1
Host: login.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic YzFmM2E5ZTJiN2Q4NGIwZjllNmEyZDVjOGI3ZjRlMTA6a1g5Mi1wUXI3LVp0MzE=

grant_type=client_credentials&scope=webhooks.write&audience=https%3A%2F%2Ferp.example.com%2Fapi
{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjNGOEMxIn0.eyJhdWQiOiJlcnAtYXBpIn0.c2lnbmF0dXJl",
  "token_type": "Bearer",
  "expires_in": 3600
}
  • A token is reused until shortly before it expires, as expires_in reports.
  • A change to the Authorization settings takes effect on the next delivery. A token obtained with the previous settings is not reused.
  • If your URL answers 401, GAINS Connect obtains a new token and sends the payload once more straight away.
  • If no token can be obtained, nothing is sent. The delivery is recorded in webhook history as failed with status code 502, and retried like any other failure (see Retries).

Use your provider's public https token endpoint as TokenUrl, at its final address: redirects are not followed. Never put credentials in it.

Saving a configuration checks the settings and, for OAuth2PrivateKeyJwt, the key, but does not contact your identity provider. To confirm a token can be obtained, use POST /webhook/test: when it cannot, the test answers 400 with the identity provider's error.

OAuth2ClientCredentials

The client authenticates to the token endpoint with a client ID and secret.

Every field, with whether it is required and its default, is listed under OAuth2ClientCredentials in the API Reference for POST /webhook/settings. The example below produces the token request shown above: Audience is sent as the audience form parameter, which some identity providers require to choose the API the token is for.

{
  "Authorization": {
    "OAuth2ClientCredentials": {
      "TokenUrl": "https://login.example.com/oauth2/token",
      "ClientId": "c1f3a9e2b7d84b0f9e6a2d5c8b7f4e10",
      "ClientSecret": "kX92-pQr7-Zt31",
      "ClientAuthenticationMethod": "CLIENT_SECRET_BASIC",
      "Scope": "webhooks.write",
      "Audience": "https://erp.example.com/api"
    }
  }
}

OAuth2PrivateKeyJwt

The client authenticates to the token endpoint with a JSON Web Token it signs with a private key, known as a client assertion. Your identity provider holds the matching certificate or public key, so no shared secret is sent.

Every field, with whether it is required and its default, is listed under OAuth2PrivateKeyJwt in the API Reference for POST /webhook/settings. ClientId becomes the assertion's iss claim, and Audience its aud claim.

{
  "Authorization": {
    "OAuth2PrivateKeyJwt": {
      "TokenUrl": "https://login.example.com/oauth2/token",
      "ClientId": "c1f3a9e2b7d84b0f9e6a2d5c8b7f4e10",
      "PrivateKey": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7...\n-----END PRIVATE KEY-----\n",
      "KeyId": "gains-webhooks-2026",
      "SignatureAlgorithm": "PS256",
      "Scope": "webhooks.write",
      "IncludeSubjectClaim": true
    }
  }
}

Private keys

  • Send an unencrypted PKCS#8 key in PEM form, the block starting -----BEGIN PRIVATE KEY-----. A PKCS#1 key (BEGIN RSA PRIVATE KEY) is refused; convert it with openssl pkcs8 -topk8 -nocrypt -in old.pem -out key.pem. A password-protected key (BEGIN ENCRYPTED PRIVATE KEY) cannot be read.
  • Match the key to the algorithm. An RSA key signs RS256 to RS512 and PS256 to PS512; an EC key signs ES256 to ES512. A key that cannot sign with the chosen algorithm is refused when you save it.
  • Write the line breaks as \n inside the JSON string, as in the example above.

To create a key and a self-signed certificate to register with your identity provider:

openssl req -x509 -newkey rsa:3072 -nodes -days 730 \
  -subj "/CN=gains-webhooks" -keyout key.pem -out certificate.pem

key.pem is the PrivateKey you send to GAINS Connect; certificate.pem is what you upload to your identity provider. Keep key.pem nowhere else.

Extra parameters

Some identity providers need a parameter or claim the fields above do not cover. Form adds form parameters to the token request, and Claim (for OAuth2PrivateKeyJwt only) adds claims to the assertion. Each is an object of names and text values, sent exactly as you write them:

{
  "Form": {
    "resource": "https://erp.example.com"
  },
  "Claim": {
    "tenant": "acme-prod"
  }
}
  • Names GAINS Connect sets itself, such as grant_type, scope or iss, cannot be added. Use the matching field instead; the error names the parameter.
  • Values are always text. A claim is added as a JSON string.

Secrets on reads and updates

ClientSecret and PrivateKey follow the same rules as every webhook secret: never returned, kept by a PATCH that leaves them out; see Secrets.

To rotate a client secret, send only the new one:

{
  "Authorization": {
    "OAuth2ClientCredentials": {
      "ClientSecret": "Hm4!wQ8z-Rt52"
    }
  }
}

Create the new secret at your identity provider first, update GAINS Connect, and delete the old secret only once deliveries succeed with the new one. A secret that reads ******(re-enter) can no longer be used; see Secrets.

Example: Microsoft Entra ID

📘

Illustrative example

This example shows how the fields map to one identity provider's terms. Check your provider's current documentation and your own tenant's values before you use it.

Your receiving API is registered in Entra ID, and GAINS Connect is a client application with a client secret that is allowed to call it. Create the configuration with POST /webhook/settings:

  1. Register an application for GAINS Connect in your tenant, and note its Application (client) ID and your Directory (tenant) ID.
  2. Under Certificates & secrets, create a client secret and copy its value.
  3. Grant the application the application permission (app role) your receiving API expects, and give admin consent.
  4. Note the Application ID URI of your receiving API, for example api://erp-webhooks.
[
  {
    "Name": "ERP via Entra ID",
    "Active": true,
    "Url": "https://erp.example.com/gains/webhooks",
    "Authorization": {
      "OAuth2ClientCredentials": {
        "TokenUrl": "https://login.microsoftonline.com/2b7e4f1a-9c3d-4e8b-a5f6-0d1c2e3f4a5b/oauth2/v2.0/token",
        "ClientId": "6a1d9c3e-4b2f-47a8-9e0d-5c7b8a6f3e21",
        "ClientSecret": "Xq8~kP2.mN7_rT4-vW9zL3sJ6hG1fD5aB0cE",
        "ClientAuthenticationMethod": "CLIENT_SECRET_POST",
        "Scope": "api://erp-webhooks/.default"
      }
    },
    "Triggers": {
      "POSubmit": {
        "Enabled": true
      }
    }
  }
]
  • Scope is the API's Application ID URI followed by /.default. Entra ID then issues a token carrying the app roles you granted.
  • Your receiving API validates the token it receives: its issuer, its audience (your API) and the app role.

Example: Oracle NetSuite

📘

Illustrative example

This example shows how the fields map to one identity provider's terms. Check NetSuite's current documentation and your own account's values before you use it.

NetSuite's OAuth 2.0 client credentials flow (machine to machine) authenticates the client with a signed assertion whose scope is a list inside the assertion, so it uses OAuth2PrivateKeyJwt with ScopeLocation ASSERTION and ScopeFormat ARRAY:

  1. Create an integration record with the client credentials grant enabled, and note its Client ID.
  2. In the OAuth 2.0 client credentials setup, map that integration to the role and user deliveries should run as, upload certificate.pem, and note the Certificate ID NetSuite assigns.
  3. Deploy the RESTlet that receives the webhooks, and note its external URL.
  4. Create the configuration with POST /webhook/settings:
[
  {
    "Name": "NetSuite RESTlet",
    "Active": true,
    "Url": "https://1234567.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=1842&deploy=1",
    "Authorization": {
      "OAuth2PrivateKeyJwt": {
        "TokenUrl": "https://1234567.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token",
        "ClientId": "3e9f1c7a5b2d48e6a0c4f8b1d7e3a9c5f2b6d8e0a4c1f7b3e9d5a2c8f6b0e4d1",
        "PrivateKey": "-----BEGIN PRIVATE KEY-----\nMIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQDF...\n-----END PRIVATE KEY-----\n",
        "KeyId": "f3Hk8Qm2Zr6Wt1Yp9Lx4Nb7Vc0Js5Dg3Ae8Ru2Ko6Ti",
        "SignatureAlgorithm": "PS256",
        "Scope": "restlets",
        "ScopeLocation": "ASSERTION",
        "ScopeFormat": "ARRAY"
      }
    },
    "Triggers": {
      "POSubmit": {
        "Enabled": true
      }
    }
  }
]
  • KeyId is the Certificate ID from the client credentials setup, not the certificate's file name.
  • Leave Audience out. NetSuite expects the assertion's aud to be the token URL, which is what GAINS Connect sends without it.
  • Replace 1234567 with your account ID in both URLs, as NetSuite shows it for your production or sandbox account.
  • Renew the certificate before it expires. Upload the new certificate, then send the new PrivateKey and KeyId together with PATCH.

Errors

When you save a configuration, a problem with the OAuth 2.0 settings answers 400 with a message naming the field and what is wrong with it, for example Authorization.OAuth2ClientCredentials.ClientSecret is required.

When a token cannot be obtained at delivery time, webhook history records the delivery with status code 502 and the identity provider's answer in Response:

{
  "tokenEndpointStatus": 401,
  "error": "invalid_client",
  "errorDescription": "Token request refused (HTTP 401 invalid_client: Client authentication failed)"
}
errorUsual cause
invalid_clientWrong ClientId or ClientSecret, an expired secret, the wrong ClientAuthenticationMethod, or a certificate the provider does not hold for KeyId.
invalid_scopeScope names a scope the client is not allowed, or is written in the wrong place or form for the provider.
unauthorized_clientThe client is not allowed the client credentials grant.
cool_offThe token endpoint refused a recent request; the delivery is retried. Correcting the Authorization settings makes the next delivery try at once.
nullThe token endpoint could not be reached, answered with an error of its own, or answered without a token at AccessTokenPath. errorDescription says which.

See Webhook history for the other status codes webhook history records.

Related


Did this page help you?