Gatheragent lines
Docs/Webhook quickstart

Agent webhook quickstart

Connect an HTTPS endpoint to a Gather phone number and handle your first text message.

Open dashboard

Start with your coding agent

Use this repository-aware prompt to implement the endpoint and its tests.

View prompt
Integrate this repository with Gather Agent Lines.

Inspect the existing framework, routing, configuration, persistence, logging, and test patterns. Implement a production-ready POST endpoint at the most idiomatic equivalent of /webhooks/gather.

Gather sends application/json with X-Webhook-ID, X-Webhook-Timestamp, and X-Webhook-Signature headers. The signature is sha256=<hex HMAC> where the HMAC input is timestamp + "." + the exact raw request body.

Requirements:
1. Read the exact raw body before parsing JSON.
2. Read GATHER_WEBHOOK_SECRET from the environment. Return 503 when it is absent.
3. Reject malformed signatures and timestamps more than 5 minutes old or in the future. Compare signatures in constant time.
4. Treat delivery as at-least-once. Deduplicate side effects by X-Webhook-ID in the repository's durable database or cache for at least 24 hours.
5. Never log the secret, signature, authorization headers, or message body. Structured logs may include webhook ID, outcome, and duration.
6. Finish within 10 seconds.
7. Return 200 with {"body":"reply text"} to reply, or 204 for no reply. Replies must fit one GSM-7 segment: 160 septets, with extension characters counting as two.
8. Use the repository's existing agent or service boundary. Do not introduce a parallel framework or configuration convention.
9. Add deterministic tests for a valid signature, tampered body, stale timestamp, missing secret, duplicate webhook ID, 200 reply, and 204 no-reply behavior.

Do not deploy, create credentials, or change external services unless explicitly authorized. Report files changed, tests and results, required environment variables, the final endpoint path, and remaining manual deployment steps.

Before you start

You need a repository that can expose an HTTP route and a place to deploy it over HTTPS. You do not need the webhook secret yet. Gather shows it once after you create the line.

Set up your endpoint

  1. Implement a POST webhook

    Paste the prompt above into the coding agent working in your repository. Review the resulting code and run its tests.

  2. Deploy it over HTTPS

    Publish the endpoint at a stable public URL. Before its secret is configured, it can return 503.

  3. Create an Agent Line

    Open the Gather dashboard, enter the endpoint URL, and create the line. Copy the webhook secret when it appears.

  4. Configure the secret

    Store the value as GATHER_WEBHOOK_SECRET in your deployment’s secret manager, then redeploy.

  5. Send a test message

    Text the assigned number. The dashboard confirms when Gather receives the message, calls your endpoint, and accepts the reply.

Request payload

Gather sends a JSON POST. The value of X-Webhook-ID matches the body’s id and remains stable across retries.

Example request body
{
  "type": "message.received",
  "id": "msg_01JY...",
  "channel": "sms",
  "timestamp": "2026-07-21T23:42:10.000Z",
  "message": {
    "from": "+16504228253",
    "to": "+14244414976",
    "body": "Are you open tomorrow?"
  }
}
HeaderPurpose
X-Webhook-IDDurable idempotency key
X-Webhook-TimestampUnix timestamp in seconds
X-Webhook-Signaturesha256= followed by the HMAC digest

Verify signatures

Read the untouched body before parsing JSON. Compute HMAC-SHA256 over {timestamp}.{rawBody}, reject stale timestamps, and compare the complete signature in constant time.

Node.js verification core
import crypto from "node:crypto";

const rawBody = await request.text();
const timestamp = request.headers.get("x-webhook-timestamp") ?? "";
const supplied = request.headers.get("x-webhook-signature") ?? "";
const expected = "sha256=" + crypto
  .createHmac("sha256", process.env.GATHER_WEBHOOK_SECRET!)
  .update(timestamp + "." + rawBody)
  .digest("hex");

const suppliedBytes = Buffer.from(supplied);
const expectedBytes = Buffer.from(expected);
const valid = suppliedBytes.length === expectedBytes.length &&
  crypto.timingSafeEqual(suppliedBytes, expectedBytes);

Return a response

Your endpoint must finish within 10 seconds. Return one of these responses:

StatusBodyResult
200{"body":"Reply text"}Gather queues the reply
204EmptyNo reply is sent
Non-2xxIgnoredGather retries delivery

Replies currently support one GSM-7 segment. Basic characters count as one septet, extension characters count as two, and the total limit is 160 septets.

Delivery behavior

Webhook delivery is at least once. A request can be repeated after a timeout or interrupted acknowledgement, so side effects must be deduplicated with X-Webhook-ID in durable storage. Gather deterministically deduplicates the outbound SMS reply.