n1coePayDocs

Get started

Set up webhooks

Receive your store's notifications and check that they come from n1co.

≈ 5 minutes · curl, Node.js, .NET, PHP or Java

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.

POST /v1/setup
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

webhooks.mjs
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

N1coWebhook.cs
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

webhooks.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)

webhooks.php
<?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

N1coWebhook.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.

POST /v1/webhooks/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.

Type to search the documentation.