Get started
Set up webhooks
Receive your store's notifications and check that they come from n1co.
Step 1Register your URL
Your webhook_url is set with POST /v1/setup when you get your keys. To change it, repeat the call with the new URL; the response brings a new webhook.secret.
The URL uses https, a public host and at most 2048 characters.
curl -s -X POST https://api-epay-sandbox.n1co.shophttps://api-epay.n1co.com/v1/setup \
-H 'Content-Type: application/json' \
-d '{
"client_id": "<your application's client id>",
"client_secret": "<your application's client secret>",
"webhook_url": "https://hooks.example.com/n1co"
}'Step 2Verify the signature
Every delivery carries an HMAC-SHA256 signature of the raw body with your webhook.secret. This function verifies it; your card capture and payment intents endpoints use it.
Verify the bytes as they arrive, before parsing the JSON.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
// Signature: HMAC-SHA256 of "id.timestamp.body" with the base64 key of whsec_.
export function verifyWebhook(headers, body) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'] ?? '';
if (!id || !/^\d{1,15}$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(process.env.N1CO_WEBHOOK_SECRET.slice('whsec_'.length), 'base64');
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.`).update(body).digest();
return (headers['webhook-signature'] ?? '').split(' ').some((entry) => {
const given = Buffer.from(entry.slice(3), 'base64');
return entry.startsWith('v1,') && given.length === expected.length && timingSafeEqual(given, expected);
});
}.NET
using System.Security.Cryptography;
using System.Text;
// Signature: HMAC-SHA256 of "id.timestamp.body" with the base64 key of whsec_.
static class N1coWebhook
{
public static bool Verify(string? id, string? timestamp, string? signatures, byte[] body)
{
if (string.IsNullOrEmpty(id) || timestamp is not { Length: > 0 and <= 15 } || !timestamp.All(char.IsAsciiDigit))
return false;
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(timestamp)) > 300) return false;
var secret = Environment.GetEnvironmentVariable("N1CO_WEBHOOK_SECRET")!;
var key = Convert.FromBase64String(secret["whsec_".Length..]);
byte[] signed = [.. Encoding.UTF8.GetBytes($"{id}.{timestamp}."), .. body];
var expected = HMACSHA256.HashData(key, signed);
var given = new byte[32];
foreach (var entry in (signatures ?? "").Split(' '))
{
if (entry.StartsWith("v1,") && Convert.TryFromBase64String(entry[3..], given, out var n) && n == 32
&& CryptographicOperations.FixedTimeEquals(given, expected))
return true;
}
return false;
}
}PHP
<?php
// Signature: HMAC-SHA256 of "id.timestamp.body" with the base64 key of whsec_.
function verify_webhook(string $body): bool
{
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
if ($id === '' || !preg_match('/^\d{1,15}$/', $timestamp) || abs(time() - (int) $timestamp) > 300) {
return false;
}
$key = base64_decode(substr(getenv('N1CO_WEBHOOK_SECRET'), strlen('whsec_')), true);
$expected = 'v1,' . base64_encode(hash_hmac('sha256', "$id.$timestamp.$body", $key, true));
foreach (explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '') as $entry) {
if (hash_equals($expected, $entry)) {
return true;
}
}
return false;
}PHP (SDK)
<?php
use N1co\EPay\Exception\WebhookSignatureException;
use N1co\EPay\N1co;
$n1co = new N1co((string) getenv('N1CO_SECRET_KEY'), [
'webhook_secret' => (string) getenv('N1CO_WEBHOOK_SECRET'),
'base_url' => 'https://api-epay-sandbox.n1co.shophttps://api-epay.n1co.com',
]);
try {
$event = $n1co->webhooks->constructEventFromRequest(); // reads php://input and the headers
} catch (WebhookSignatureException) {
http_response_code(400);
exit;
}Java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
// Signature: HMAC-SHA256 of "id.timestamp.body" with the base64 key of whsec_.
final class N1coWebhook {
private N1coWebhook() {}
static boolean verify(String id, String timestamp, String signatures, byte[] body) throws Exception {
if (id == null || timestamp == null || signatures == null || !timestamp.matches("\\d{1,15}")) return false;
if (Math.abs(Instant.now().getEpochSecond() - Long.parseLong(timestamp)) > 300) return false;
var key = Base64.getDecoder().decode(System.getenv("N1CO_WEBHOOK_SECRET").substring("whsec_".length()));
var mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
mac.update((id + "." + timestamp + ".").getBytes(StandardCharsets.UTF_8));
var expected = mac.doFinal(body);
for (var entry : signatures.split(" ")) {
try {
if (entry.startsWith("v1,") && MessageDigest.isEqual(Base64.getDecoder().decode(entry.substring(3)), expected)) {
return true;
}
} catch (IllegalArgumentException notBase64) {
// An entry that is not base64 does not match.
}
}
return false;
}
}Step 3Test your endpoint
POST /v1/webhooks/test sends a signed test delivery to your webhook_url and answers 202; your endpoint receives a webhook.test.
curl -s -X POST https://api-epay-sandbox.n1co.shophttps://api-epay.n1co.com/v1/webhooks/test \
-H "Authorization: Bearer $N1CO_SECRET_KEY"Your webhooks are ready.
Use them to confirm card captures and payment intents.