Receivers

Webhook receivers, whose secret it is, the map, the filter and payment time.

A receiver is a URL of the form POST /hooks/<appId>/<name>. Receivers are standards, not providers: any sender that follows one of these schemes can call it. Pick the scheme from the sender's own webhook docs. verify names it:

SchemeFieldsWhat is checked
signatureThe signature recipe: header or signatureField, and the optional fields in The signature recipeA signature over the event, checked with the sender’s algorithm and key. Left at its defaults, an HMAC-SHA256 of the raw body, hex, in the header you name
stripenoneThe stripe-signature header, five-minute window
svixnoneStandard Webhooks: webhook-id, webhook-timestamp, webhook-signature (or the same three as svix-*), a whsec_… secret, five-minute window. Clerk and Resend send Standard Webhooks
shared_tokenheader? (default x-webhook-token), query? (default token)The token matches in either place. A query token travels in URLs, so prefer the header. A shared token doesn’t sign the event: a relay on it only writes records and calls your URL unless its trigger sets "trustUnsignedEvents": true (see Unsigned events)

Whose secret it is. Each relay holds one value the sender’s signatures are checked with, and on every scheme you choose whose it is, on the relay’s page in Webhooks:

  • New secret for the sender to sign with. Gemmein makes it and shows it once (on svix, a Standard Webhooks whsec_…); give it to the sender in its webhook settings. Use it when the sender lets you set the secret. It is not offered for Stripe’s format, whose sender always makes its own. The signature and shared_token schemes start with one.
  • Paste the sender’s signing secret. When the sender shows its own (Stripe and Standard Webhooks senders always do; their relays start waiting for it). At least 24 plain ASCII characters (22 when it mixes upper- and lower-case letters and digits), long, and not one pattern repeated. Stripe’s starts whsec_; a Standard Webhooks secret is whsec_ and base64. It is stored and never shown again.
  • Paste the sender’s public key. On an ed25519 or ecdsa-p256-sha256 recipe (Discord, SendGrid). The sender signs with its private key and shows the public one: paste it as shown, in hex, base64 or a PEM block. Gemmein never makes this key, so the relay starts waiting for it; asking for one answers 400 provider_secret_required.

Your AI stores a sender’s value in Development by piping it: printf '%s' "$SECRET" | npx gemmein relays secret <name>. It is read from stdin only, never typed on the command line, printed or logged. In Live, your human pastes it on the relay’s page, after the code Gemmein emails. Storing a value replaces the current one at once and is a row in Logs; the value itself never is. A relay that calls your URL also has a signing secret of its own (see Calling your URL); a relay with no call_url has none.

The map. Up to twenty names, each a dotted path into the provider's body, such as events.0.details.customer_email. Three names have a special meaning:

  • event_id is the dedupe key. A repeat answers duplicate: true and runs nothing. Without it, the body's hash deduplicates within the same UTC day.
  • event_type is what when usually reads.
  • One of three keys names the person (table below). A map that carries two of them is refused when the relay is saved: the id and the token could name different people, and the event would land on whichever was tried first.

Every other name is available in templates as {{mapped.name}}.

KeyWhat it isCreates a person?
person_emailTheir addressYes. Created or fetched the same way as invitePerson: the same daily cap, shown as invited until their first sign-in, and on the record. It invites someone who has never signed in
person_idA Gemmein person idNo, a read-only lookup
person_tokenThe store account token their app filed with a store (currentUser().storeAccountToken)No, a read-only lookup

Only an address can create a person, because an address reaches someone who can be told they have an account. A value a third party chose that matches nothing names no one. A key that resolves to nobody is not a refusal: the event is recorded and answered 200, and the run result reads person_not_found with the key that was tried, never its value.

A person who deletes their account and buys again is a new customer; a payment from before the deletion grants nothing. Map the provider's payment-time field as paid_at on every relay that grants (access, credits, a product or a plan) so Gemmein can tell before from after. It takes unix seconds, milliseconds or an ISO date; a time later than the event's arrival counts as its arrival. Without paid_at, a payment for a deleted account's address invites no one and grants nothing. It shows on the Errors page for you to decide: refund it, or grant it by hand if it is a new purchase. The error never contains the address.

The filter. when filters the mapped fields. Per field, it takes a literal (an exact match) or an object of operators in the list's filter grammar (see Data). Its keys must be in the map. A non-match answers 200 with ignored: "when" and records no event.

A typo in when looks like the provider never called. Nothing is recorded for an ignored event. Test a new receiver with a real event before relying on it.

The receiver answers 200 with { received: true, eventId } only once the event is stored. Storing the event is the acknowledgement, and nothing runs before it.

CodeStatusMeaning
unknown_receiver404No receiver relay with this name on this app. A deleted one no longer receives; a paused one still does
bad_signature401The signature did not verify against this receiver's secret. Check the secret and the header the scheme expects
body_too_large413The body is over 256 KB
invalid_json400The body is not JSON
receiver_capped429More than 120 verified events in a minute for this app; resetAt says when. Unverified traffic never counts against this limit; it is capped per address instead, and the sender gets source_capped (60 a minute from one address)
not_recorded503The event could not be stored. Nothing ran; the provider sends it again

The URL names no environment. The secret decides which environment's relay receives, so Development and Live may share a name, each with its own secret.

Was this page helpful?

Last updated