Integrations

Go

Server-side steps in Go.

Prerequisites

  • Go with the standard library only.
  • The secret key of the scenario and a permanent API token, read from environment variables. Never put them in browser code or in a repository.
  • The API address provided with your access.

The call sequence

  1. Your server encrypts the raw client key with the secret key of the scenario.
  2. Your page opens the widget with the scenario identifier and the encrypted client key.
  3. The user completes the steps in the widget.
  4. Your server learns of the result from the SESSION_COMPLETE webhook, or reads session status.
  5. Your server decides on the customer from the server-side status.

Encrypt the client key

The secret key of the scenario never leaves your server. See working with clientKey for the algorithm.

package verification

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
)

func pkcs7Pad(data []byte, blockSize int) []byte {
	n := blockSize - len(data)%blockSize
	return append(data, bytes.Repeat([]byte{byte(n)}, n)...)
}

// EncryptClientKey returns the clientKey for the widget.
func EncryptClientKey(clientKeyRaw, secret string) (string, error) {
	// First 32 characters of the SHA-256 hash of the secret, written as hex.
	sum := sha256.Sum256([]byte(secret))
	key := []byte(hex.EncodeToString(sum[:])[:32])

	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	iv := make([]byte, aes.BlockSize)
	if _, err := rand.Read(iv); err != nil {
		return "", err
	}

	plain := pkcs7Pad([]byte(clientKeyRaw), aes.BlockSize)
	encrypted := make([]byte, len(plain))
	cipher.NewCBCEncrypter(block, iv).CryptBlocks(encrypted, plain)

	return base64.StdEncoding.EncodeToString(append(iv, encrypted...)), nil
}

Store the raw value with your user or order, so that you can match the result later.

Create a session

package verification

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func apiPost(path string, body map[string]any) (map[string]any, error) {
	payload, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	reply, err := http.Post(os.Getenv("API_BASE_URL")+path, "application/json", bytes.NewReader(payload))
	if err != nil {
		return nil, err
	}
	defer reply.Body.Close()

	var out map[string]any
	if err := json.NewDecoder(reply.Body).Decode(&out); err != nil {
		return nil, err
	}
	return out, nil
}

func CreateSession() (map[string]any, error) {
	session, err := apiPost("/kyc/session/create", map[string]any{
		"token":    os.Getenv("API_TOKEN"),   // permanent API token
		"schemaId": os.Getenv("SCENARIO_ID"), // scenario identifier
	})
	if err != nil {
		return nil, err
	}
	if _, ok := session["sessionId"]; !ok {
		// A failure arrives as a body with status "error" or "failed".
		return nil, fmt.Errorf("session was not created: %v", session["status"])
	}
	return session, nil // session["status"] is the session status, "idle"
}

Read the result with apiPost("/kyc/session/status", ...) and branch on status, never on message text. See response format and errors and session statuses.

Troubleshooting

SymptomWhat to check
The widget does not openThe domain is in the allowed domains, the page is on HTTPS, and the widget is not inside a cross-origin iframe. See installation.
The widget reports that decryption failedThe client key was not encrypted, or a different secret key was used, or the string was cut. See working with clientKey.
A call returns success but the body has an errorBranch on the status field of the body. See response format and errors.
The result never reaches your serverCheck the callback URL in the scenario. See webhooks.

Next: receive the result

Set up a webhook endpoint on your backend.

Receiving webhooks