Authentication

How to authenticate your API requests

API Key Authentication

This API requires Api Key Authentication on every request. This method ensures strong authentication and prevents request tampering by digitally signing every API request.

⚠️ Important:

  • If a request includes the X-Devengo-Api-Key-Signature header, it will be validated using this method.
  • If the signature is not present, authentication will fall back to JWT authentication.
  • In the future, all requests will require a digital signature. We recommend transitioning as soon as possible.

How It Works

Each request must be signed using a Base64-encoded HMAC-SHA256 signature. The signature is generated using a combination of equest data, metadata, and a secret key.

Step 1: Construct the Data to Sign
To generate the signature, the following elements must be concatenated in this exact position:

  1. Base64-encoded Request Payload (if the request has a body)
  2. Nonce. The unique id you must generate for every request.
  3. Request Timestamp. Unix Timestamp format.
  4. API Key Identifier. Provided by Devengo for every api key generated.

The resulting string will look like this:

Base64(REQUEST_BODY) + NONCE + TIMESTAMP + API_KEY_ID

Step 2: Generate the HMAC-SHA256 Signature
Once the data string is created, compute the HMAC-SHA256 digest using the secret provided in the moment of creation of the API Key as the encryption key:

HMAC-SHA256(secret, data_to_sign)

Then, Base64 encode the resulting signature.

Step 3: Send the Signature in the Request

Include the generated signature and required metadata as HTTP headers:

X-Devengo-Api-Key-Signature: {base64_encoded_signature}
X-Devengo-Api-Key-Nonce: {nonce}
X-Devengo-Api-Key-Timestamp: {timestamp}
X-Devengo-Api-Key-Id: {api_key_id}

Timestamp Validation Windows

The signature is only considered valid if the request's timestamp is within a 60-second window from the current server time. During signature validation, we check that the timestamp provided in the request is not older than 60 seconds. If it falls outside this allowed time drift, the signature is deemed invalid and the request is rejected.

Example Implementations

require "base64"
require "json"
require "net/http"
require "openssl"
require "securerandom"
require "uri"

# API credentials
API_KEY_ID = "sbx_apk_XXX"
API_SECRET = "xyz"
HOST = "https://api.sandbox.devengo.com"

def signed_request(method, url, api_key_id, api_secret, body: nil)
  nonce = SecureRandom.uuid
  timestamp = Time.now.to_i.to_s

  encoded_payload = body ? Base64.strict_encode64(body) : ""
  data_to_sign = "#{encoded_payload}#{nonce}#{timestamp}#{api_key_id}"

  signature = Base64.strict_encode64(
    OpenSSL::HMAC.digest("SHA256", api_secret, data_to_sign)
  )

  uri = URI(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  headers = {
    "X-Devengo-Api-Key-Signature" => signature,
    "X-Devengo-Api-Key-Nonce" => nonce,
    "X-Devengo-Api-Key-Timestamp" => timestamp,
    "X-Devengo-Api-Key-Id" => api_key_id
  }
  headers["Content-Type"] = "application/json" unless body.nil?

  request = case method.upcase
            when "GET"  then Net::HTTP::Get.new(uri.path, headers)
            when "POST" then Net::HTTP::Post.new(uri.path, headers)
            end

  request.body = body unless body.nil?

  response = http.request(request)
  puts "Response: #{response.code} #{response.message}"
  puts response.body
end

if __FILE__ == $PROGRAM_NAME
  endpoint = "#{HOST}/v1/auth/api_key_signature/test"

  puts "=== GET sin body ==="
  signed_request("GET", endpoint, API_KEY_ID, API_SECRET)

  puts "\n=== POST sin body ==="
  signed_request("POST", endpoint, API_KEY_ID, API_SECRET)

  puts "\n=== POST con body vacío ==="
  signed_request("POST", endpoint, API_KEY_ID, API_SECRET, body: "")

  puts "\n=== POST con body ==="
  signed_request("POST", endpoint, API_KEY_ID, API_SECRET, body: JSON.generate({ example_key: "example_value" }))
end
const crypto = require("crypto");
const https = require("https");

// API credentials
const API_KEY_ID = "sbx_apk_XXX";
const API_SECRET = "xyz";
const HOST = "https://api.sandbox.devengo.com";

function signedRequest(method, url, apiKeyId, apiSecret, body = undefined) {
  return new Promise((resolve, reject) => {
    const nonce = crypto.randomUUID();
    const timestamp = Math.floor(Date.now() / 1000).toString();

    const encodedPayload =
      body != null ? Buffer.from(body).toString("base64") : "";
    const dataToSign = `${encodedPayload}${nonce}${timestamp}${apiKeyId}`;

    const signature = crypto
      .createHmac("sha256", apiSecret)
      .update(dataToSign)
      .digest("base64");

    const parsedUrl = new URL(url);
    const headers = {
      "X-Devengo-Api-Key-Signature": signature,
      "X-Devengo-Api-Key-Nonce": nonce,
      "X-Devengo-Api-Key-Timestamp": timestamp,
      "X-Devengo-Api-Key-Id": apiKeyId,
    };
    if (body != null) {
      headers["Content-Type"] = "application/json";
    }

    const req = https.request(
      {
        hostname: parsedUrl.hostname,
        path: parsedUrl.pathname,
        method: method.toUpperCase(),
        headers,
      },
      (res) => {
        let data = "";
        res.on("data", (chunk) => (data += chunk));
        res.on("end", () => {
          console.log(`Response: ${res.statusCode} ${res.statusMessage}`);
          console.log(data);
          resolve();
        });
      }
    );

    req.on("error", reject);

    if (body != null) {
      req.write(body);
    }
    req.end();
  });
}

async function main() {
  const endpoint = `${HOST}/v1/auth/api_key_signature/test`;

  console.log("=== GET sin body ===");
  await signedRequest("GET", endpoint, API_KEY_ID, API_SECRET);

  console.log("\n=== POST sin body ===");
  await signedRequest("POST", endpoint, API_KEY_ID, API_SECRET);

  console.log("\n=== POST con body vacío ===");
  await signedRequest("POST", endpoint, API_KEY_ID, API_SECRET, "");

  console.log("\n=== POST con body ===");
  await signedRequest(
    "POST",
    endpoint,
    API_KEY_ID,
    API_SECRET,
    JSON.stringify({ example_key: "example_value" })
  );
}

main();
import base64
import hashlib
import hmac
import json
import time
import urllib.error
import urllib.request
import uuid

# API credentials
API_KEY_ID = "sbx_apk_XXX"
API_SECRET = "xyz"
HOST = "https://api.sandbox.devengo.com"


def signed_request(method, url, api_key_id, api_secret, body=None):
    nonce = str(uuid.uuid4())
    timestamp = str(int(time.time()))

    encoded_payload = base64.b64encode(body.encode()).decode() if body else ""
    data_to_sign = f"{encoded_payload}{nonce}{timestamp}{api_key_id}"

    signature = base64.b64encode(
        hmac.new(api_secret.encode(), data_to_sign.encode(), hashlib.sha256).digest()
    ).decode()

    headers = {
        "X-Devengo-Api-Key-Signature": signature,
        "X-Devengo-Api-Key-Nonce": nonce,
        "X-Devengo-Api-Key-Timestamp": timestamp,
        "X-Devengo-Api-Key-Id": api_key_id,
    }
    if body is not None:
        headers["Content-Type"] = "application/json"

    req = urllib.request.Request(
        url,
        data=body.encode() if body is not None else None,
        headers=headers,
        method=method,
    )

    try:
        with urllib.request.urlopen(req) as response:
            print(f"Response: {response.status} {response.reason}")
            print(response.read().decode())
    except urllib.error.HTTPError as e:
        print(f"Response: {e.code} {e.reason}")
        print(e.read().decode())


if __name__ == "__main__":
    endpoint = f"{HOST}/v1/auth/api_key_signature/test"

    print("=== GET sin body ===")
    signed_request("GET", endpoint, API_KEY_ID, API_SECRET)

    print("\n=== POST sin body ===")
    signed_request("POST", endpoint, API_KEY_ID, API_SECRET)

    print("\n=== POST con body vacío ===")
    signed_request("POST", endpoint, API_KEY_ID, API_SECRET, body="")

    print("\n=== POST con body ===")
    signed_request("POST", endpoint, API_KEY_ID, API_SECRET, body=json.dumps({"example_key": "example_value"}))
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class TestSignature {

    private static final String API_KEY_ID = "sbx_apk_XXX";
    private static final String API_SECRET = "xyz";
    private static final String HOST = "https://api.sandbox.devengo.com";

    public static void signedRequest(String method, String url, String apiKeyId, String apiSecret, String body)
            throws Exception {
        String nonce = UUID.randomUUID().toString();
        String timestamp = String.valueOf(Instant.now().getEpochSecond());

        String encodedPayload = body != null
                ? Base64.getEncoder().encodeToString(body.getBytes(StandardCharsets.UTF_8))
                : "";
        String dataToSign = encodedPayload + nonce + timestamp + apiKeyId;

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        String signature = Base64.getEncoder().encodeToString(mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8)));

        HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
        conn.setRequestMethod(method.toUpperCase());
        conn.setRequestProperty("X-Devengo-Api-Key-Signature", signature);
        conn.setRequestProperty("X-Devengo-Api-Key-Nonce", nonce);
        conn.setRequestProperty("X-Devengo-Api-Key-Timestamp", timestamp);
        conn.setRequestProperty("X-Devengo-Api-Key-Id", apiKeyId);

        if (body != null) {
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setDoOutput(true);
            try (OutputStream os = conn.getOutputStream()) {
                os.write(body.getBytes(StandardCharsets.UTF_8));
            }
        }

        int statusCode = conn.getResponseCode();
        String statusMessage = conn.getResponseMessage();

        java.io.InputStream stream = statusCode >= 400 ? conn.getErrorStream() : conn.getInputStream();
        String responseBody = new String(stream.readAllBytes(), StandardCharsets.UTF_8);

        System.out.println("Response: " + statusCode + " " + statusMessage);
        System.out.println(responseBody);
    }

    public static void main(String[] args) throws Exception {
        String endpoint = HOST + "/v1/auth/api_key_signature/test";

        System.out.println("=== GET sin body ===");
        signedRequest("GET", endpoint, API_KEY_ID, API_SECRET, null);

        System.out.println("\n=== POST sin body ===");
        signedRequest("POST", endpoint, API_KEY_ID, API_SECRET, null);

        System.out.println("\n=== POST con body vacío ===");
        signedRequest("POST", endpoint, API_KEY_ID, API_SECRET, "");

        System.out.println("\n=== POST con body ===");
        signedRequest("POST", endpoint, API_KEY_ID, API_SECRET, "{\"example_key\":\"example_value\"}");
    }
}

JSON Web Token Authentication (deprecated)

Although API key signature authentication is the recommended method and will soon become the only available option, JSON Web Token (JWT) authentication is still temporarily supported as a transitional measure.

You'll have to get a token using the authentication endpoint. Once you have it, please attach the token to your following requests using the Authorization header.

Authorization: Bearer {JWT_TOKEN}

All endpoints that require authentication will respond with a 401 HTTP status code and the following body if you don't provide a valid token:

{
  "error": {
    "message": "Unauthenticated",
    "code": "authorization",
    "type": "invalid_request_error"
  }
}