Receive signed webhooks
Use a public HTTPS endpoint on port 443. Save the signing secret shown when you create the destination in Notifications. Changing the destination URL generates a new secret and requires another connection test.
Requests
Requests contain JSON with version (1), id (stable delivery ID), type, created_at and data (message text, link and subject ID). Types are alert.opened, alert.resolved, deployment.verified, deployment.needs_attention, deployment.incomplete and connection.test.
Verify the signature
Verify Upstate-Signature using HMAC-SHA256 over Upstate-Timestamp, a period, and the raw body. Reject timestamps more than five minutes away. Deduplicate with a unique database constraint on Upstate-Id before enqueuing work. Return 2xx after durable acceptance, including for duplicates.
Ruby verifier
# Call with the raw request body, Rack environment, and your stored signing secret.
# After verification, atomically store event.fetch("id") under a UNIQUE index and
# enqueue your work. Return 2xx after that transaction commits. An existing ID is
# a successful duplicate; acknowledge it without repeating the work.
require "openssl"
require "json"
def verify_upstate(body, env, secret, now: Time.now.to_i)
raise ArgumentError, "body too large" if body.bytesize > 65_536
timestamp = env.fetch("HTTP_UPSTATE_TIMESTAMP", "")
signature = env.fetch("HTTP_UPSTATE_SIGNATURE", "")
raise ArgumentError, "invalid timestamp" unless /\A[0-9]{1,12}\z/.match?(timestamp)
raise ArgumentError, "expired signature" if (now - timestamp.to_i).abs > 300
raise ArgumentError, "invalid signature" unless /\Av1=[0-9a-f]{64}\z/.match?(signature)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{body}")
unless OpenSSL.fixed_length_secure_compare(expected, signature.delete_prefix("v1="))
raise ArgumentError, "invalid signature"
end
event = JSON.parse(body)
raise ArgumentError, "unsupported version" unless event["version"] == 1
raise ArgumentError, "invalid event id" unless event["id"].is_a?(String) && event["id"] == env["HTTP_UPSTATE_ID"]
event
endRetries and failures
Transport errors, 429 and 5xx responses retry up to eight attempts. Redirects and other statuses permanently fail. A timed-out response can cause a duplicate even after your receiver accepted it, so keep deduplicating by Upstate-Id.