A Singapore Government Agency Website

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

UserAdmin PortalDatabaseSubscribe with Webhook URL + Encryption Public Key (optional)Save Webhook URL + Encryption Public Key (if provided)SuccessSuccessUserAdmin PortalDatabase

Signature Public Key

You can retrieve the signature public key from the following URLs:

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.

OpenAPI 3.1.0 Spec

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: 1715753037
X-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

  1. 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)
    // continue
  2. Retrieve 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
    }
    // continue
  3. Retrieve 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
    }
    // continue
  4. If 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!
Chat with me!
Bot Launcher