Receiving Webhooks

Build an endpoint for GAINS webhooks: the request it receives, how to answer, duplicates, ordering and troubleshooting failed deliveries.

This page is for whoever builds the endpoint that receives GAINS webhooks. It covers the request GAINS Connect sends, how to answer it, timeouts, duplicates and ordering, and how to find out why a delivery failed. To set up a webhook in the first place, see Webhooks.

The request you receive

Every webhook is an HTTP/1.1 POST to the Url of a webhook configuration, with a JSON body:

POST /gains/webhooks HTTP/1.1
Host: erp.example.com
Content-Type: application/json
Content-Length: 1834
Authorization: Basic Z2FpbnMtd2ViaG9va3M6VnE3I3BMMm0tWHI5
{
  "DataType": "purchase_order",
  "Data": [
    {
      "TrackingNumber": 4821,
      "ReceivingLocation": "CHI01"
    }
  ]
}

The body is UTF-8 JSON in the envelope described in Payload format: DataType names the subject and Data holds the records. Its field names follow the configuration's FieldNamingConvention.

HeaderValue
Content-Typeapplication/json
Content-LengthThe size of the body in bytes.
AuthorizationDepends on the configuration's Authorization scheme; see below. Absent when the configuration has none.

The Authorization header for each scheme:

SchemeAuthorization header
HeaderThe Value you configured, exactly as given: Bearer 8f3c1d025a77.
BasicBasic followed by the base64 of Username:Password, UTF-8 encoded.
OAuth2ClientCredentials, OAuth2PrivateKeyJwtThe token type and access token your identity provider issued, normally Bearer eyJhbGciOi…. See OAuth 2.0 Webhook Destinations.

A webhook carries no signature, event ID or delivery ID header. Check the Authorization header on every request to decide whether to trust it, and identify the records from the payload itself (see Duplicates).

Answering a webhook

What your URL answers decides whether the delivery succeeded:

Your answerRecorded in historyWhat happens next
2xxsuccessThe payload leaves the queue. The next one is sent PayloadInterval seconds later.
3xxfailedThe redirect is not followed, so the payload never reaches the address you redirect to. Retried after RetryHandling.Gap, up to RetryHandling.Attempts times.
401failed, unless the retry below succeedsFor an OAuth 2.0 scheme only, GAINS Connect first obtains a fresh token and sends the payload once more straight away.
408no_responseRetried, as for failed.
Any other 4xx or 5xx, including 429 and 503failedRetried after RetryHandling.Gap, up to RetryHandling.Attempts times.
  • Answer 2xx once you have stored the payload. Anything you answer after that is your own processing, not delivery.
  • Never answer with a redirect. Point Url at the exact address that accepts the POST.
  • The response body is not read. The start of it, and your response headers, are kept so you can see them in webhook history.

How failed deliveries are retried, and how a failing payload holds back the ones behind it, is described in Retries.

Timeouts

Answer within 90 seconds. Otherwise the delivery has failed and is recorded with status code 504.

Respond fast and process asynchronously:

  1. Check the Authorization header.
  2. Store the body as received, for example on a message queue or in a staging table.
  3. Answer 200 or 202.
  4. Process the stored payload separately.

A payload can hold up to PayloadMaxItems records, up to 500, so a large order payload can take a few megabytes. Make sure any gateway or proxy in front of your endpoint accepts a body of that size and does not time out sooner than your own processing.

Duplicates

GAINS Connect delivers each payload at least once, not exactly once. You can receive the same records more than once when:

  • You processed a payload but answered too late, or answered something other than 2xx. The payload is retried.
  • A payload is resent with POST /webhook/resend, by you or by GAINS support.

Make processing a record twice harmless. Neither the request nor the payload carries a delivery ID, so key on the fields that identify each record:

DataTypeIdentify a record byRecognise a newer version by
purchase_order, transfer_order, work_order, production_orderTrackingNumber, and LineNumber within its LineItemsUpdatedTime
inventory_policyItemCode and LocationCodeEventOccurredAt on the location
forecastItemCode and LocationCodeEventOccurredAt on the location
taskTaskIdStatusUpdatedOn

Write records as inserts-or-updates on those keys rather than as plain inserts, and do not let an older version overwrite a newer one. The weekly and monthly forecast triggers share DataType forecast; tell them apart by the length of their buckets, as Webhook Payload Samples describes.

🚧

Test payloads are made up

POST /webhook/test sends sample records to your real URL. Make sure a test cannot create orders or change data in your production system, for example by testing against a non-production URL first.

Ordering

Do not rely on the order in which payloads arrive. A resent payload arrives after newer data for the same records, and several configurations pointing at the same URL send to it in parallel, so your endpoint must accept concurrent requests.

Your endpoint

  • Serve https with a certificate from a public certificate authority. Otherwise delivery fails, and webhook history records it as 503.
  • Do not rely on headers other than those listed above.

The full rules for the Url setting are in Destination URL.

Troubleshooting

Reading webhook history

GET /webhook/history lists every try of every delivery for the last 30 days. To see today's failures:

GET /api/v1/webhook/history?Status=failed&SearchDateStart=2026-10-21 00:00:00

Each entry shows the HttpStatusCode and Response of one try. When your URL answered, Response holds its Headers and the start of its Body. When it could not answer, GAINS Connect records one of these codes itself, and Response has no headers from your server:

HttpStatusCodeMeaningWhat to do
502No credential could be produced, so nothing was sent. Response holds your identity provider's error, or credential_must_be_re_entered.Correct the OAuth 2.0 settings, or re-enter the secret (see below).
503The TLS handshake with your URL failed.Check the certificate and the chain your server presents.
504GAINS Connect could not connect, the host name could not be resolved or is not allowed, or no answer came within 90 seconds.Check that the URL is reachable from the internet and that you answer within 90 seconds.
500Something failed in GAINS Connect before the request was sent.Contact your GAINS representative with the WebhookId.

Your own server can answer 502, 503 or 504 too, for example from a gateway in front of it. Its headers in Response tell the two apart.

{
  "WebhookId": 98412,
  "ConfigurationId": 3,
  "IssuedAt": "2026-10-21 09:15:02",
  "ResponseReceivedAt": "2026-10-21 09:16:32",
  "IssuedTo": "https://erp.example.com/gains/webhooks",
  "Authorization": "Basic",
  "Status": "failed",
  "HttpStatusCode": 504,
  "Attempt": 1,
  "Trigger": "po_submit",
  "Response": {
    "Headers": {},
    "Body": null,
    "StatusCodeValue": 504,
    "StatusCode": "GATEWAY_TIMEOUT"
  }
}

The filters and fields of webhook history, and how to read a payload, are in Webhook history.

A secret reads ******(re-enter)

When GET /webhook/settings shows a secret as ******(re-enter) instead of ******, the stored secret can no longer be used. Every delivery for that configuration then fails with 502, and nothing reaches your URL:

{
  "error": "credential_must_be_re_entered",
  "errorDescription": "The stored credential for this webhook can no longer be decrypted and must be re-entered."
}

Send the secret again with PATCH /webhook/settings/{ConfigurationId}, naming the same scheme and only the secret:

{
  "Authorization": {
    "Basic": {
      "Password": "Vq7#pL2m-Xr9"
    }
  }
}

Testing, resending and discarding

To test a configuration, resend a dropped payload or see and discard what is queued, see Delivery & Recovery.

Related


Did this page help you?