Delivery & Recovery

How webhook payloads are queued, delivered and retried, and how to test, follow, resend and discard them.

This page describes how GAINS Connect queues and delivers webhook payloads, what happens when a delivery fails, and the endpoints to test, follow and recover deliveries. What your endpoint must answer is described in Receiving Webhooks.

Delivery

When a trigger fires, every active configuration that has the trigger enabled, and whose filter a record matches, queues that record. Each configuration sends its queue as payloads of up to PayloadMaxItems records of one trigger, waiting PayloadInterval after each successful payload.

  • Your URL must answer 2xx within 90 seconds, and must tolerate receiving the same record twice; see Answering a webhook.
  • Records waiting in the queue for more than 30 days are discarded and never delivered, for example while a configuration is paused or held back by a failing payload.
  • Each configuration has its own queue, so a destination that is down delays only its own payloads.

Retries

A failed payload is tried again after RetryHandling.Gap minutes, up to RetryHandling.Attempts more times: with Attempts set to 3, a payload is tried at most four times. Each try appears in webhook history, numbered from 1 in Attempt.

  • Later payloads wait behind a failing one. Until it succeeds or its last try fails, nothing queued behind it is delivered.
  • After the last try fails, the payload is dropped from the queue. Find it in webhook history and send it again (see Resending a webhook).

To stop a configuration delivering for a while, see Pausing a configuration.

Monitoring and recovery

TaskEndpoint
Send a sample payload to a configurationPOST /webhook/test
List the webhooks sentGET /webhook/history
Read the payload of one webhookGET /webhook/{WebhookId}/payload
Send a webhook againPOST /webhook/resend
See what is waiting in the queueGET /webhook/status
Discard what is waiting in the queuePOST /webhook/reset, then POST /webhook/reset/confirm

Testing a configuration

POST /webhook/test sends a sample payload of one trigger to a configuration straight away, using its URL, authorization and field naming. The sample holds made-up records, not your data.

{
  "Trigger": "POSubmit",
  "ConfigurationId": 3
}
  • 200 means your URL answered 2xx: Reason reads The webhook was issued successfully.
  • 400 means the test failed, with the reason: the status your URL answered, a redirect included, or why no token could be obtained.
  • The configuration does not need to be active, but it needs a Url.
  • TaskStatusUpdate cannot be tested.

Each test appears in webhook history like any other delivery.

Webhook history

GET /webhook/history lists the webhooks sent, one entry per try. History is kept for 30 days.

  • Without a date range, it returns today's deliveries. Set SearchDateStart and SearchDateEnd to look further back.
  • Filter it by status, trigger or order, and include each payload if you need it; the API Reference describes each parameter.
  • It is paged: pass Paging.NextCursor from one page as Cursor to read the next. See Pagination.
GET /api/v1/webhook/history?Status=failed&SearchDateStart=2026-10-19%2000:00:00

Each entry carries the WebhookId (for reading its payload or resending it), the Status and Attempt, and the HttpStatusCode and Response of that try. When your URL answered, Response holds its Headers and the start of its Body.

{
  "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"
  }
}

To read the payload of a single webhook, call GET /webhook/{WebhookId}/payload. It returns WebhookId and Payload, exactly as sent, or 404 for an unknown WebhookId.

Status codes GAINS Connect records

When your URL 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 (see OAuth 2.0 Webhook Destinations), or re-enter the secret (see Secrets).
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.

Resending a webhook

POST /webhook/resend queues a webhook to be sent again. Send exactly one of:

  • WebhookId to resend a whole payload, taken from webhook history.
  • TrackingNumber to resend one order from a payload already sent. This applies to the order triggers only.
{
  "TrackingNumber": 4821
}

The response is 202 with Reason The webhook to be resent was successfully added to the queue.

  • It goes to the configuration that sent it originally, with that configuration's current URL and authorization.
  • It is sent only while the configuration is active.
  • TaskStatusUpdate webhooks cannot be resent.
  • A resend repeats data you may already hold; see Duplicates.

A resend that cannot be done answers 400 with a Reason saying why.

Queue status

GET /webhook/status shows what is waiting to be delivered, in total and per trigger, added up across all your configurations. It needs webhook read access. Every trigger is listed, with 0 when nothing of it is waiting.

{
  "QueueSummary": {
    "PendingRecords": 240,
    "PendingNotifications": 3,
    "OldestMessageAge": 1820,
    "EstimatedNextPayload": "2026-10-21 10:15:00",
    "DataStart": "2026-10-21 09:44:40",
    "DataEnd": "2026-10-21 10:13:05"
  },
  "Triggers": {
    "ForecastMonthlyUpdate": {
      "PendingRecords": 0,
      "OldestMessageAge": null
    },
    "ForecastWeeklyUpdate": {
      "PendingRecords": 0,
      "OldestMessageAge": null
    },
    "InventoryPolicyUpdate": {
      "PendingRecords": 200,
      "OldestMessageAge": 610
    },
    "POSubmit": {
      "PendingRecords": 40,
      "OldestMessageAge": 1820
    },
    "ProductionOrderSubmit": {
      "PendingRecords": 0,
      "OldestMessageAge": null
    },
    "TOSubmit": {
      "PendingRecords": 0,
      "OldestMessageAge": null
    },
    "TaskStatusUpdate": {
      "PendingRecords": 0,
      "OldestMessageAge": null
    },
    "WOSubmit": {
      "PendingRecords": 0,
      "OldestMessageAge": null
    }
  }
}

When you have no configuration, it answers 400 with Reason Webhook status is unavailable as webhooks are not in use.; when nothing is waiting, 400 with No webhook tasks in the queue.

Discarding queued payloads

Reset discards records waiting in a configuration's queue so they are never delivered, for example after a mass change you do not want sent. It takes two calls, so it cannot happen by accident, and both need webhook update access:

  1. Request a token with POST /webhook/reset, passing ConfigurationId and Trigger as query parameters. Trigger takes one trigger, a comma-separated list, or an empty value (Trigger=) for every trigger.
  2. Confirm with POST /webhook/reset/confirm within 5 minutes.
POST /api/v1/webhook/reset?ConfigurationId=3&Trigger=InventoryPolicyUpdate
{
  "ResetToken": "5b0e4c1a-93d2-4f7e-8a61-2c9d7e3f1b48",
  "Expires": "2026-10-21 10:20:00"
}
{
  "ResetToken": "5b0e4c1a-93d2-4f7e-8a61-2c9d7e3f1b48"
}

The confirmation answers Status success and a Reason giving the number of records discarded. If nothing is queued for those triggers, the first call answers 200 with a Reason saying so instead of a token. An expired or unknown token answers 400; request a new one.

Related


Did this page help you?