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:
- Sends a
POSTtoTokenUrlwith a form body (application/x-www-form-urlencoded) carryinggrant_type=client_credentials, the client's credentials and the other parameters your settings call for, such asscope. - Reads the token from
access_token, its type fromtoken_typeand its lifetime fromexpires_inin the JSON response. - Sends the delivery with the header
Authorization: <token_type> <access_token>. The type is normallyBearer, which is also what GAINS Connect uses when the response has notoken_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_inreports. - A change to the
Authorizationsettings 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 withopenssl 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
RS256toRS512andPS256toPS512; an EC key signsES256toES512. A key that cannot sign with the chosen algorithm is refused when you save it. - Write the line breaks as
\ninside 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.pemkey.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,scopeoriss, 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 exampleThis 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:
- Register an application for GAINS Connect in your tenant, and note its Application (client) ID and your Directory (tenant) ID.
- Under Certificates & secrets, create a client secret and copy its value.
- Grant the application the application permission (app role) your receiving API expects, and give admin consent.
- 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
}
}
}
]Scopeis 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 exampleThis 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:
- Create an integration record with the client credentials grant enabled, and note its Client ID.
- 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. - Deploy the RESTlet that receives the webhooks, and note its external URL.
- 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
}
}
}
]KeyIdis the Certificate ID from the client credentials setup, not the certificate's file name.- Leave
Audienceout. NetSuite expects the assertion'saudto be the token URL, which is what GAINS Connect sends without it. - Replace
1234567with 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
PrivateKeyandKeyIdtogether withPATCH.
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)"
}error | Usual cause |
|---|---|
invalid_client | Wrong ClientId or ClientSecret, an expired secret, the wrong ClientAuthenticationMethod, or a certificate the provider does not hold for KeyId. |
invalid_scope | Scope names a scope the client is not allowed, or is written in the wrong place or form for the provider. |
unauthorized_client | The client is not allowed the client credentials grant. |
cool_off | The token endpoint refused a recent request; the delivery is retried. Correcting the Authorization settings makes the next delivery try at once. |
null | The 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
- Configuring Webhooks: the other settings and authorization schemes
- Receiving Webhooks: validating the token your endpoint receives
- Delivery & Recovery: testing a configuration and reading webhook history
Updated 8 days ago
