Webhook validation error

Issue Summary: Validation Failure on All Webhook Events Using Lambda

I’m experiencing consistent issues validating webhook requests from Snipcart across all events (using order.completed as an example). I’ve followed the documentation and examples, testing multiple approaches, but validation hasn’t succeeded.

Setup Details:

  • I implemented an AWS Lambda function in JavaScript, exposed via an API Gateway, and set this URL as the webhook endpoint.
  • I successfully retrieve the X-Snipcart-RequestToken from each event and include the API_SECRET key in the validation request.

Approaches and Code Variants Tried: I tested passing the API_SECRET in both clear text and as a base64-encoded string in each of two code variants. Here’s a sample of each:

  1. Clear Text and Encoded API Key in the auth Field:

javascript

Copy code

async function validateRequestToken(token, apiSecret) {
    try {
        const encodedKey = Buffer.from(`${apiSecret}:`).toString('base64');  // Encode API key with trailing colon
        const url = `https://app.snipcart.com/api/requestvalidation/${token}`;
        console.log("Validating token:", token);
        console.log("Encoded API Key:", encodedKey);

        const response = await axios.get(url, {
            auth: {
                username: encodedKey, // Attempted as both encoded and clear text
                password: '',
            },
        });
        console.log("Received response:", response);
        return response;
    } catch (error) {
        console.error('Token validation error:', error);
        return false;
    }
}
  1. Clear Text and Encoded API Key in the Authorization Header:

javascript

Copy code

async function validateRequestToken(token, apiSecret) {
    try {
        console.log("Validating token:", token);  
        console.log("Using API Secret:", apiSecret);  

        const response = await axios.get(`https://app.snipcart.com/api/requestvalidation/${token}`, {
            headers: {
                Authorization: `Basic ${Buffer.from(apiSecret + ':').toString('base64')}`, // Tried both encoded and clear text
            },
        });
        console.log("Received response:", response);
        return response.data.ok;
    } catch (error) {
        console.error('Token validation error:', error);
        return false;
    }
}

Current Outcome:

  • Neither approach (clear text or encoded key) has yielded a successful validation; errors or no valid response are consistently returned.
  • Webhook events work in test mode without validation but fail when validation is enabled.

I would appreciate any insights into the cause of these issues, particularly with respect to encoding, header structure, or any specific requirements that might be missing. Thank you for your assistance.

here is sample event error from CloudWatch ( when Secretkey is encoded)

2024-10-27T15:16:32.351Z 3a98fd42-0519-475e-b141-168a761c7489 ERROR Token validation error: AxiosError: Request failed with status code 401
at settle (/var/task/node_modules/axios/dist/node/axios.cjs:2019:12)
at IncomingMessage.handleStreamEnd (/var/task/node_modules/axios/dist/node/axios.cjs:3135:11)
at IncomingMessage.emit (node:events:529:35)
at IncomingMessage.emit (node:domain:489:12)
at endReadableNT (node:internal/streams/readable:1400:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
at Axios.request (/var/task/node_modules/axios/dist/node/axios.cjs:4287:41)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async validateRequestToken (/var/task/index.js:48:26)
at async validateEvent (/var/task/index.js:31:12)
at async exports.handler (/var/task/index.js:191:15) {
code: ‘ERR_BAD_REQUEST’,
config: {
transitional: {
silentJSONParsing: true,
forcedJSONParsing: true,
clarifyTimeoutError: false
},
adapter: [ ‘xhr’, ‘http’, ‘fetch’ ],
transformRequest: [ [Function: transformRequest] ],
transformResponse: [ [Function: transformResponse] ],
timeout: 0,
xsrfCookieName: ‘XSRF-TOKEN’,
xsrfHeaderName: ‘X-XSRF-TOKEN’,
maxContentLength: -1,
maxBodyLength: -1,
env: { FormData: [Function], Blob: [class Blob] },
validateStatus: [Function: validateStatus],
headers: Object [AxiosHeaders] {
Accept: ‘application/json, text/plain, /’,
‘Content-Type’: undefined,
‘User-Agent’: ‘axios/1.7.7’,
‘Accept-Encoding’: ‘gzip, compress, deflate, br’
},
auth: {
username: ‘WXpOalpESTROalF0WVdObU1DMDBPV0ZoTFdFNU5UWXROamhsT1dZek5tRmlNMlU1TmpNNE5EZzVOVEUyTXpNMU5qVXdOall6Og==’,
password: ‘’
},
method: ‘get’,
url: ‘https://app.snipcart.com/api/requestvalidation/b1072856-50da-48e4-8eef-76c90ec89304’,
data: undefined
},
request: <ref *1> ClientRequest {
_events: [Object: null prototype] {
abort: [Function (anonymous)],
aborted: [Function (anonymous)],
connect: [Function (anonymous)],
error: [Function (anonymous)],
socket: [Function (anonymous)],
timeout: [Function (anonymous)],
finish: [Function: requestOnFinish]
},
_eventsCount: 7,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: false,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
strictContentLength: false,
_contentLength: 0,
_hasBody: true,
_trailer: ‘’,
finished: true,
_headerSent: true,
_closed: false,
socket: TLSSocket {
_tlsOptions: [Object],
_secureEstablished: true,
_securePending: false,
_newSessionPending: false,
_controlReleased: true,
secureConnecting: false,
_SNICallback: null,
servername: ‘app.snipcart.com’,
alpnProtocol: false,
authorized: true,
authorizationError: null,
encrypted: true,
_events: [Object: null prototype],
_eventsCount: 10,
connecting: false,
_hadError: false,
_parent: null,
_host: ‘app.snipcart.com’,
_closeAfterHandlingError: false,
_readableState: [ReadableState],
_maxListeners: undefined,
_writableState: [WritableState],
allowHalfOpen: false,
_sockname: null,
_pendingData: null,
_pendingEncoding: ‘’,
server: undefined,
_server: null,
ssl: [TLSWrap],
_requestCert: true,
_rejectUnauthorized: true,
parser: null,
_httpMessage: [Circular *1],
[Symbol(alpncallback)]: null,
[Symbol(res)]: [TLSWrap],
[Symbol(verified)]: true,
[Symbol(pendingSession)]: null,
[Symbol(async_id_symbol)]: 108,
[Symbol(kHandle)]: [TLSWrap],
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kSetNoDelay)]: false,
[Symbol(kSetKeepAlive)]: true,
[Symbol(kSetKeepAliveInitialDelay)]: 60,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0,
[Symbol(connect-options)]: [Object]
},
_header: ‘GET /api/requestvalidation/b1072856-50da-48e4-8eef-76c90ec89304 HTTP/1.1\r\n’ +
‘Accept: application/json, text/plain, /\r\n’ +
‘User-Agent: axios/1.7.7\r\n’ +
‘Accept-Encoding: gzip, compress, deflate, br\r\n’ +
‘Host: app.snipcart.com\r\n’ +
‘Authorization: Basic V1hwT2FscEVTVFJPYWxGMFdWZE9iVTFETURCUFYwWm9URmRGTlU1VVdYUk9hbWhzVDFkWmVrNXRSbWxOTWxVMVRtcE5ORTVFWnpWT1ZFVXlUWHBOTVU1cVZYZE9hbGw2T2c9PTo=\r\n’ +
‘Connection: close\r\n’ +
‘\r\n’,
_keepAliveTimeout: 0,
_onPendingData: [Function: nop],
agent: Agent {
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 443,
protocol: ‘https:’,
options: [Object: null prototype],
requests: [Object: null prototype] {},
sockets: [Object: null prototype],
freeSockets: [Object: null prototype] {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256,
scheduling: ‘lifo’,
maxTotalSockets: Infinity,
totalSocketCount: 1,
maxCachedSessions: 100,
_sessionCache: [Object],
[Symbol(kCapture)]: false
},
socketPath: undefined,
method: ‘GET’,
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
joinDuplicateHeaders: undefined,
path: ‘/api/requestvalidation/b1072856-50da-48e4-8eef-76c90ec89304’,
_ended: true,
res: IncomingMessage {
_readableState: [ReadableState],
_events: [Object: null prototype],
_eventsCount: 4,
_maxListeners: undefined,
socket: [TLSSocket],
httpVersionMajor: 1,
httpVersionMinor: 1,
httpVersion: ‘1.1’,
complete: true,
rawHeaders: [Array],
rawTrailers: [],
joinDuplicateHeaders: undefined,
aborted: false,
upgrade: false,
url: ‘’,
method: null,
statusCode: 401,
statusMessage: ‘Unauthorized’,
client: [TLSSocket],
_consuming: false,
_dumped: false,
req: [Circular *1],
responseUrl: ‘https://WXpOalpESTROalF0WVdObU1DMDBPV0ZoTFdFNU5UWXROamhsT1dZek5tRmlNMlU1TmpNNE5EZzVOVEUyTXpNMU5qVXdOall6Og%3D%3D:@app.snipcart.com/api/requestvalidation/b1072856-50da-48e4-8eef-76c90ec89304’,
redirects: [],
[Symbol(kCapture)]: false,
[Symbol(kHeaders)]: [Object],
[Symbol(kHeadersCount)]: 16,
[Symbol(kTrailers)]: null,
[Symbol(kTrailersCount)]: 0
},
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: ‘app.snipcart.com’,
protocol: ‘https:’,
_redirectable: Writable {
_writableState: [WritableState],
_events: [Object: null prototype],
_eventsCount: 3,
_maxListeners: undefined,
_options: [Object],
_ended: true,
_ending: true,
_redirectCount: 0,
_redirects: [],
_requestBodyLength: 0,
_requestBodyBuffers: [],
_onNativeResponse: [Function (anonymous)],
_currentRequest: [Circular *1],
_currentUrl: ‘https://WXpOalpESTROalF0WVdObU1DMDBPV0ZoTFdFNU5UWXROamhsT1dZek5tRmlNMlU1TmpNNE5EZzVOVEUyTXpNMU5qVXdOall6Og%3D%3D:@app.snipcart.com/api/requestvalidation/b1072856-50da-48e4-8eef-76c90ec89304’,
[Symbol(kCapture)]: false
},
[Symbol(kCapture)]: false,
[Symbol(kBytesWritten)]: 0,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype] {
accept: [Array],
‘user-agent’: [Array],
‘accept-encoding’: [Array],
host: [Array],
authorization: [Array]
},
[Symbol(errored)]: null,
[Symbol(kHighWaterMark)]: 16384,
[Symbol(kRejectNonStandardBodyWrites)]: false,
[Symbol(kUniqueHeaders)]: null
},
response: {
status: 401,
statusText: ‘Unauthorized’,
headers: Object [AxiosHeaders] {
‘content-length’: ‘0’,
connection: ‘close’,
date: ‘Sun, 27 Oct 2024 15:16:31 GMT’,
‘access-control-expose-headers’: ‘Request-Context’,
‘cache-control’: ‘no-cache’,
expires: ‘-1’,
pragma: ‘no-cache’,
‘request-context’: ‘appId=cid-v1:48f65c7b-986c-4ec3-89de-eb0160f55639’
},
config: {
transitional: [Object],
adapter: [Array],
transformRequest: [Array],
transformResponse: [Array],
timeout: 0,
xsrfCookieName: ‘XSRF-TOKEN’,
xsrfHeaderName: ‘X-XSRF-TOKEN’,
maxContentLength: -1,
maxBodyLength: -1,
env: [Object],
validateStatus: [Function: validateStatus],
headers: [Object [AxiosHeaders]],
auth: [Object],
method: ‘get’,
url: ‘https://app.snipcart.com/api/requestvalidation/b1072856-50da-48e4-8eef-76c90ec89304’,
data: undefined
},
request: <ref *1> ClientRequest {
_events: [Object: null prototype],
_eventsCount: 7,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: false,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
strictContentLength: false,
_contentLength: 0,
_hasBody: true,
_trailer: ‘’,
finished: true,
_headerSent: true,
_closed: false,
socket: [TLSSocket],
_header: ‘GET /api/requestvalidation/b1072856-50da-48e4-8eef-76c90ec89304 HTTP/1.1\r\n’ +
‘Accept: application/json, text/plain, /\r\n’ +
‘User-Agent: axios/1.7.7\r\n’ +
‘Accept-Encoding: gzip, compress, deflate, br\r\n’ +
‘Host: app.snipcart.com\r\n’ +
‘Authorization: Basic V1hwT2FscEVTVFJPYWxGMFdWZE9iVTFETURCUFYwWm9URmRGTlU1VVdYUk9hbWhzVDFkWmVrNXRSbWxOTWxVMVRtcE5ORTVFWnpWT1ZFVXlUWHBOTVU1cVZYZE9hbGw2T2c9PTo=\r\n’ +
‘Connection: close\r\n’ +
‘\r\n’,
_keepAliveTimeout: 0,
_onPendingData: [Function: nop],
agent: [Agent],
socketPath: undefined,
method: ‘GET’,
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
joinDuplicateHeaders: undefined,
path: ‘/api/requestvalidation/b1072856-50da-48e4-8eef-76c90ec89304’,
_ended: true,
res: [IncomingMessage],
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: ‘app.snipcart.com’,
protocol: ‘https:’,
_redirectable: [Writable],
[Symbol(kCapture)]: false,
[Symbol(kBytesWritten)]: 0,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype],
[Symbol(errored)]: null,
[Symbol(kHighWaterMark)]: 16384,
[Symbol(kRejectNonStandardBodyWrites)]: false,
[Symbol(kUniqueHeaders)]: null
},
data: ‘’
},
status: 401
}

Hello.

First of all: You just leaked your API key. Make sure to delete it and create a new one!

Can you pass the API key with a trailing colon in clear text to axios? I can see in the request it is double-encoded in the actual request header.

For the header variant we don’t have a log to check. Could you post that and make sure you’re not leaking the keys again?

this is the encrypted Test API Key. How can I replace it anyways?

No, it’s not. It’s just base64-encoded which is absolutely not the same as encryption. You should immediately remove the API key you have and create a new one!

You can check, this is your secret API key, correct?

YzNjZDI4NjQtYWNmMC00OWFhLWE5NTYtNjhlOWYzNmFiM2U5NjM4NDg5NTE2MzM1NjUwNjYz

I just extracted that from your error message you posted by removing the encoding (anyone can do that and abuse your key).

To replace this key, go to “Account” (top right in the dashboard), then “API Keys”, then do this:

This key is the Public Test API Key

in the website it is written that:

The public API key is the key you need to add on your website when including the snipcart.js file. This key can be shared without security issues because it only allows a specific subset of API operations.

Did I miss somthing here

oh OK, if this is your public key, you should read the docs again:

for the API authentication, you need your private API key. Have a look at my screenshot. It should lead you to where you can generate one.

1 Like

My mistake was using the Public API key in the authentication instead of using a secret key

this code worked for me.

async function validateRequestToken(token, apiSecret) {

try {

    const url = `https://app.snipcart.com/api/requestvalidation/${token}`;

    // encoding the key with Base64 format

    const encodedKey = Buffer.from(`${apiSecret}:`).toString('base64');

    const response = await axios.get(url, {

        headers: {

            'Accept': 'application/json',

            'Authorization': `Basic ${encodedKey}`

        },

    });

    console.log("response: ", response);

    return response.status === 200;

} catch (error) {

    console.error('Token validation error:', error);

    return false;

}

}