Skip to content
Documentation menu

Vouchereto API · v1

Authentication & signing

Every request carries your API key and a signature of the request made with that key's secret. A stolen key alone, or a request changed in transit, is rejected.

Headers on every request

Authentication headers
HeaderValue
AuthorizationBearer vch_test_…
Vouchereto-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Idempotency-KeyEvery POST: 1–128 characters, unique per operation. Details
Content-Typeapplication/json on requests with a body
X-Customer-IpOptional, on POST /v1/redemptions: your customer's IP, for fraud limits

The signature

  1. Take the secret: the part of your API key after the last _ (32 characters).
  2. Take t, the current unix time in seconds.
  3. Build the string t + "." + METHOD + " " + path_with_query + "\n" + raw_body:
    • METHOD in upper case: GET or POST.
    • path_with_query starts with /v1 and includes the query string exactly as sent, for example /v1/redemptions?status=captured&limit=20.
    • raw_body is the exact bytes you send. GET requests and POSTs without a body use an empty string (the newline is still there).
  4. Compute HMAC-SHA256 of that string with the secret, hex-encode it (lower case, 64 characters).
  5. Send Vouchereto-Signature: t=<t>,v1=<hex>.
worked example
API key        vch_test_AbC123dEf456_Q2w3E4r5T6y7U8i9O0p1A2s3D4f5G6h7
secret         Q2w3E4r5T6y7U8i9O0p1A2s3D4f5G6h7         (last "_" segment)
t              1792663200                                (unix seconds, now)
method + path  POST /v1/redemptions
body           {"code":"K7QM-4XPD-9TRW-2WNZ","currency":"USD","customer_ref":"user_8842"}

string to sign (the \n is one newline character):
1792663200.POST /v1/redemptions\n{"code":"K7QM-4XPD-9TRW-2WNZ","currency":"USD","customer_ref":"user_8842"}

Vouchereto-Signature: t=1792663200,v1=<hex HMAC-SHA256(secret, string to sign)>
Sign and send
# Bash + openssl: sign and send a request
KEY="$VOUCHERETO_API_KEY"                    # vch_test_…
API="${VOUCHERETO_API:-https://api.vouchereto.com}"
SECRET="${KEY##*_}"                          # the secret is the last "_" segment of the key
METHOD="POST"
PATH_Q="/v1/sandbox/vouchers"                # path (with /v1 and any ?query)
BODY='{"state":"active","amount_minor":5000,"currency":"USD"}'
T=$(date +%s)
SIG=$(printf '%s.%s %s\n%s' "$T" "$METHOD" "$PATH_Q" "$BODY" \
  | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -sS -X "$METHOD" "$API$PATH_Q" \
  -H "Authorization: Bearer $KEY" \
  -H "Vouchereto-Signature: t=$T,v1=$SIG" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  --data-raw "$BODY"
These samples are executed against the API in our CI on every change, so you can copy them as they are.

Common mistakes

Signature troubleshooting
SymptomCause
401 INVALID_SIGNATURE · MISMATCHBody re-serialised after signing (sign the exact string you send: do not pass an object to your HTTP client after signing a string), path without /v1, query missing or reordered, lower-case method, or the whole key used as the secret.
401 INVALID_SIGNATURE · TIMESTAMP_OUT_OF_RANGEYour clock is more than 300 seconds off, or you reused an old signature. Sign each request just before sending and run NTP.
401 INVALID_SIGNATURE · MALFORMEDHeader missing, or not in the form t=…,v1=<64 hex>.
401 UNAUTHENTICATEDKey missing, mistyped, revoked, or a key of another environment.
403 MERCHANT_NOT_ACTIVEA live key before your account is live, or a suspended account.

Too many failed authentications from one IP return 429 RATE_LIMITED for a minute.

Rotating keys

Create a second key in the portal, deploy it, check traffic in the portal's API log, then revoke the old key. Both work in the meantime. The signature header may carry several v1= values; the request passes if any of them matches.