Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: External services

External services

Webhook replies, handshakes and further signature schemes

This is the detail behind Webhooks that need a reply and Handshakes: how the Twilio, Slack and SNS signatures are checked, how each handshake is answered, and how a reply is made, kept and limited.

  • Twilio. SignatureScheme.twilio: X-Twilio-Signature, the Base64 of the HMAC-SHA1 under the auth token (the connection's secret) of the URL Twilio requested followed by each form member's name and value, sorted by name and, for a name sent twice, by value. The URL is https://, the request's Host and its path and query, tried with :443 written and without, as Twilio's own libraries do; Twilio sends only to HTTPS and a proxy in front of the host ends the TLS. It signs no time, so no age is checked; a repeat is dropped by its id. The body is a form (400 invalid_request otherwise), read as JSON of its members' text, a name sent twice a list, so the places and fields name form members (event_id: "CallSid").
  • Slack. SignatureScheme.slack, since no other scheme checks Slack's events: X-Slack-Signature: v0=<hex>, the HMAC-SHA-256 of v0:<X-Slack-Request-Timestamp>:<body> under the signing secret, with the five-minute tolerance.
  • SNS. SignatureScheme.sns: the message's own Signature, RSA PKCS #1 v1.5 over the members SNS lists for its Type, with SHA-1 (SignatureVersion 1) or SHA-256 (2), by the RSA key (2,048 bits or more) of the certificate at its SigningCertURL, valid at the host's time; the chain isn't checked, since the certificate comes over HTTPS from AWS's own host. Any AWS account can have SNS sign a message from its own topic, so the connection's secret: is the topic's ARN, and a message from any other topic is refused before anything is fetched. The certificate is fetched only from https://sns.<region>.amazonaws.com (or .amazonaws.com.cn), a .pem path with no port, credentials or query, by the host's guard for the runtime's own requests: HTTPS only, no redirect followed, connected only to the public address it checked (never loopback, private, link-local, shared, reserved or multicast, and an IPv4 address carried inside IPv6, by NAT64, 6to4 or Teredo, judged as itself), at most 16 KB within five seconds; each certificate is kept for a day. SNS signs the time it published and its retries keep it, so the five-minute tolerance holds only ahead of the host's clock: a message signed 30 days ago or more, as long as a received id is kept, is refused, so any replay within that is a repeat by its MessageId. A certificate that can't be fetched is 503 unavailable, so SNS sends it again; a host that reaches nothing answers so. The Message, when it holds JSON, is read as that JSON, so places reach inside it ("Message.type"). Anything but a Notification carries no event: answered 204.
  • Handshakes. handshake: names WebhookHandshake.sns, .slack or .microsoft_graph (package.provider_invalid otherwise); SNS's and Slack's need SignatureScheme.sns and .slack, which sign them (package.provider_invalid otherwise). SNS's SubscriptionConfirmation, signed and from the topic, is confirmed by a GET of its SubscribeURL on an SNS host through the same guard (204 when SNS took it, 503 otherwise). Slack's url_verification, once signed, is answered 200 with {"challenge": ...}. Microsoft Graph's validation is the only request answered before a signature is looked for, since Graph signs none: a POST whose query gives one validationToken of at most 1,024 characters, none a control character, answered 200 with the token as text/plain; charset=utf-8 and X-Content-Type-Options: nosniff (400 invalid_request for one that isn't).
  • Replies. reply: in a webhook event a package declares names an Object of the package's own (package.wire_invalid otherwise); on any other event reply is reserved (type.reserved_field_name), so it is never a field. The connection's replies: keys each event needing a reply by its name in lower_snake_case, incoming_call:, naming an action <domain>.<action> (any domain; the app's use line for it, as on_first_sign_in:'s) that takes the one event and returns the reply type: a key no such event has is connection.key_invalid, a value that isn't such an action connection.value_invalid, and an event needing a reply with none connection.key_missing. Subscribing to such an event is subscriber.event_invalid: it isn't kept in the outbox.
  • Answering. After the signature and the body are read, the event's id is claimed in jadpo_received_events (its new reply column empty) in a statement of its own; the action runs as the system, reaching no tenant's records, in its own transaction, as on_first_sign_in:'s does; its reply is encoded with the reply type's codec on the provider's wire and sent 200 as application/json, and kept over the claim once the action has committed. A repeat that finds the reply is answered with it, one that finds a claim younger than a minute 409 conflict ("The webhook is still being answered."), and a claim older than that, left by a process that stopped, is taken over. A failure gives the claim back, so the provider's retry runs the action again: a declared failure answered as a route answers it, unavailable 503, a fault 500. Keeping the reply follows the action's commit rather than joining it, since the action may write another store: if that statement fails, a retry after the claim lapses runs the action again.
  • What a reply may show. What only Access.public lets the reply action in to is policy.system_grant_missing at its replies: entry. The reply goes back to whoever sent the webhook, so each field it carries, or that decided it, is one anyone at all may read (policy.output_field_unproved), a record the action created holding what it set. Whether records are there reaches the sender too: a reply counting or testing for records only the system may read, or a failure decided by it, is refused the same way, and the action's failure, answered to the sender as its reply is, is held to the same proof, what decides it and its client fields (Field restrictions).

Graph's own notification check, its clientState in the body, isn't a signature scheme yet. Twilio's TwiML replies wait for an XML byte format.

Markdown for agents: /docs/webhook-replies-handshakes-and-further-signature-schemes.md.Built from Jadpo main at a707efb.