Webhook
Overview
When building integrations with GovEntry, you might want your applications to receive events as they happen, enabling your back-end systems to respond in real-time.
To receive these events, you need to register webhook endpoints. Once registered, GovEntry can push real-time event data to your application’s webhook endpoint whenever specific events occur in your campaign. These events are sent via HTTPS as a JSON payload that includes details about the event.
Subscribing to webhook
Subscribing will require users to input their webhook endpoint and store it in GovEntry. Users may optionally provide an encryption public key generated using NaCl Box to enable payload encryption. If no encryption public key is provided, payloads are sent in plaintext.
Sequence Diagram
Signature Public Key
You can retrieve the signature public key from the following URLs:
- Development: https://dev.entry.gov.sg/portal/webhook/signature/key/v1
- Production: https://app.entry.gov.sg/portal/webhook/signature/key/v1
Generate Encryption Keys
JS
The recommended library is to use is TweetNaCI.js.
An example to generate the keys is available here.
An example implementation for encryption/decryption is available here.
Go
Golang has an experimental package which can be used nacl/box.
An example to generate the keys is available here.
Retry Policy
GovEntry retries failed webhook deliveries with exponential backoff.
| Attempt | Delay |
|---|---|
| 1 | - |
| 2 | 1s |
| 3 | 16s |
Formula: attempt^4 seconds. Max 3 attempts total.
Receive Registration Event
On every registration, a registration event will be sent to the registered webhook for the campaign.
POST /{webhook_url}
{webhook_url} will be the webhook URL that is provided by the webhook recipient.
Request (application/json)
Headers
X-GovEntry-Signature: v1=c9f7f35406ef978f2e5b989d24f6214360eca208d872536e128e406920351ae1b25f831cf039125f5723739442e79dc4bf2be46f7c9d69a775bf0cbdddff060b
X-GovEntry-Timestamp: 1715753037X-GovEntry-Timestamp
The X-GovEntry-Timestamp is an epoch time in seconds. This header is used in the X-GovEntry-Signature and is used for replay prevention.
X-GovEntry-Signature
This contains the signature of the request body. It is prefixed with a signature scheme. The current and ONLY version is v1 at the moment.
To prevent downgrade attacks, ignore all schemes that aren’t v1.
The hash algorithm used is the ED25519 algorithm.
The signature is produced by prefixing the epoch time retrieved from X-GovEntry-Timestamp and separated with a ;.
// generate signature
ts := req.Header.Get("X-GovEntry-Timestamp")
signature := ed25519.Sign(privateKey, []byte(ts+";"+msg))
// header
req.Header.Set("X-GovEntry-Signature") = "v1=" + hex.EncodeToString(s)Body
If no encryption public key was provided, the request body will be the JSON payload in plaintext.
If an encryption public key was provided during subscription, the JSON payload will be encrypted using NaCl Box. A nonce is generated and prepended to the ciphertext before it is hex encoded.
// generate nonce
var nonce [24]byte
_, err := io.ReadFull(cryptorand.Reader, nonce[:])
if err != nil {
// handle err
}
// encrypt
encryptedPayload := box.Seal(
nonce[:], // prepend nonce to encrypted output
[]byte(plaintext),
&nonce,
recipientPublicKey,
senderPrivateKey,c
)GovEntry’s public key will be prefixed to the encodedPayload with a ; separator.
encodedPayload := hex.EncodeToString(senderPublicKey[:]) + ";" +
hex.EncodeToString(encryptedPayload){
"id": "9242701f-0726-411f-b248-24dfe857e3c8",
"why": "attendance",
"how": "attendance-assisted",
"who": {
"id": "T3000001Z",
"metadata": {
"Name": "consectetur adipiscing elit",
"Data 1": "consectetur adipiscing elit",
"Data 2": "consectetur adipiscing elit"
}
},
"what": "ACME",
"when": "2024-05-17T03:20:05.09122+00:00",
"where": {
"id": "AT-001",
"metadata": {
"Data 1": "Lorem ipsum dolor sit amet",
"Data 2": "consectetur adipiscing elit",
"Venue Name": "Attendance Venue 01"
},
"opens_at": "2024-05-17T03:15:05.717541+00:00",
"closes_at": "2024-05-18T03:15:05.717541+00:00"
},
"metadata": {
"Person Name": "MY NAME IS-3000001",
"Official ID": "S1234567D"
}
}Responses
200: Return a 200 status to indicate that the data was received successfully
To Parse request body
Read request body
// var r *http.Request b, err := io.ReadAll(r.Body) if err != nil { // handle err } defer r.Body.Close() requestBody := string(b) // continueRetrieve timestamp header and verify it is not too old to prevent replay attacks
// var r *http.Request ts := r.Header.Get("X-GovEntry-Timestamp") // ts is in epoch seconds sec, err := strconv.ParseInt(ts, 10, 64) if err != nil { // handle err } if sec < 0 { // return err } now := time.Now() // check that request is not older than 5 mins if time.Unix(sec, 0).Add(time.Minute * 5).Before(now) { // return err } // continueRetrieve signature header and verify signature is valid
sig := r.Header.Get("X-GovEntry-Signature") // get version scheme and signature by cutting at `=` // format of signature is `v1={signature}` version, sig, found := strings.Cut(sig, "=") if !found { // return err } if version != "v1" { // return err } // decode signature sigB, err := hex.DecodeString(sig) if err != nil { // handle err } // signPublicKey should be exposed somewhere if !ed25519.Verify(signPublicKey, []byte(ts";"+requestBody), sigB) { // return err } // continueIf signature and timestamp are valid, decrypt the payload
// format of request body is // {sender public key};{nonce+encrypted payload} senderPublicKey, encryptedPayload, found := strings.Cut(requestBody, ";") if !found { // return err } // decode sender's public key decSenderPublicKey := make([]byte, hex.DecodedLen(len(senderPublicKey))) _, err := hex.Decode(decSenderPublicKey, []byte(senderPublicKey)) if err != nil { // handle err } senderPublicKey := ([32]byte)(decSenderPublicKey) // decode payload decEncryptedPayload := make([]byte, hex.DecodedLen(len(encryptedPayload))) _, err = hex.Decode(decEncryptedPayload, []byte(encryptedPayload)) if err != nil { // handle err } // copy nonce at the first 24 bytes of encrypted payload var nonce [24]byte copy(nonce[:], decEncryptedPayload[:24]) // decrypt decrypted, ok := box.Open( nil, dst[24:], &nonce, &senderPublicKey, recipientPrivateKey, ) if !ok { // return err } // all done!