InboundGateway

Turning what the platform sends into authenticated, deduplicated events — the four methods, the InboundEnvelope, and exactly what the core's inbound gate does before and after your route runs.

Subscriby\Connector\Contracts\Ports\InboundGateway. Required of every connector.

The core owns the route's middleware, the idempotency table and (eventually) the queue; the connector owns the platform's wire format and its authenticity check. A gateway-based platform with no HTTP request (Discord's WebSocket) builds envelopes itself in its worker, and the request methods of this port are never called for it.

Methods

MethodCalled whenReturns
authenticate(Request)Every call to a route in your routes/inbound.php, first.bool
decode(Request)After authentication, to record each event once.iterable<InboundEnvelope>
immediateResponse(Request)When every event the call carried was seen before.?Response
shouldDefer(InboundEnvelope)Reserved: the core does not queue events yet.bool

The envelope

new InboundEnvelope(
    connector: 'example',
    installation: $installationRef,          // null when the event names no installation
    idempotencyKey: $botId.':'.$updateId,    // stable per event, unique per connector
    kind: 'message',                          // your own vocabulary
    payload: $update,                         // the platform's event, as an array
    receivedAt: new DateTimeImmutable,
);

idempotencyKey is what the core records before any handler runs, so a webhook retried after a timeout or a replayed gateway batch is processed once. Build it from what the platform guarantees is stable: Telegram's update_id per bot, Discord's event id. kind is yours; the core routes on it only where the SDK names a kind.

What the gate does around your route

Every route you register in routes/inbound.php runs behind the core's connector.inbound:<key> middleware, applied for you by the SDK's service provider. In order:

  1. Unknown connector: 404.
  2. Paused (Subscriby has switched the connector off during an incident): 503 with Retry-After, so the platform keeps the event and retries later. Nothing of yours runs.
  3. authenticate() returns false: 403. Nothing else runs.
  4. decode() yields the envelopes; the gate records each idempotencyKey with an insert that races the unique index, so two copies arriving on two workers cannot both reach a handler. A replayed event is logged and counted.
  5. Every event replayed: the gate answers with your immediateResponse() or an empty 204 and your route never runs. Otherwise your route runs inside the request.

Your route's controller then handles the events. Today that means decoding the request again (or reading what the gate decoded, if your gateway caches it per request) and driving your ManagementSurface, your customer conversation or your access handler yourself. shouldDefer() is on the contract so that when the core moves handling onto its queue your connector need not change; until then it is not called.

authenticate()

Return true only when the call provably came from the platform: a signature over the body, a secret the platform echoes in a header. authenticate() receives only the request, so name the installation from the route (its external id in the path, say), then read its stored bag with Core\Installations::credentials(): it holds what the creator pasted and what you minted at complete() through the summary's meta, so a per-installation signing secret needs no table of yours. A connector-wide secret may live in your configuration instead. Never read a secret from the request. A connector whose platform offers no proof should at least pin the route to an unguessable path and say so on its listing.

immediateResponse()

Some platforms demand an answer inside the request: an interaction acknowledgement, a challenge echo. Return that response; return null and the core answers 204. The gate calls it only on the replay path; your own route returns whatever it needs on the first delivery.

How Telegram does it

Telegram posts one update per call, keyed by a per-bot update_id it re-sends after a 5xx; the envelope's idempotency key is the bot's id and the update id together. Authenticity is the secret token Telegram echoes in a header, the same check the pre-SDK middleware made; with no secret configured every call passes, because bots registered before the secret was set send none.

What the kit checks

inbound.tolerates_empty_request: for a POST with the body {} and a JSON content type, authenticate() returns a boolean, decode() returns an iterable and immediateResponse() returns null or a Response. None of them may throw on an empty request.

Decode is called twice

The gate decodes to record keys; your route decodes to handle. Make decode() cheap and side-effect free, or cache its result on the request instance. Never record anything of your own in decode().

How is this guide?

On this page

Subscriby is a product designed by you — for you.

No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request