Message Signature
To generate a message signature, cryptographic keys of the ED25519 variants must first be created.
Determine Signature Base
Section titled “Determine Signature Base”The example signature base below is determined from the content digest example request.
"content-type": application/json"content-digest": sha-512=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:"content-length": 18"authorization": Bearer 123"x-client-id": 123456"x-idempotency-key": 123456"@method": POST"@target-uri": https://api.pipevest.com/v1/customers?sort=ASC"@path": /v1/customers"@query": ?sort=ASC"@signature-params": ("content-type" "content-digest" "content-length" "authorization" "x-client-id" "x-idempotency-key" "@method" "@target-uri" "@path" "@query");keyid="staging-pipevest-ed25519";created=1732893484;expires=1732893584Signature Parameters
Section titled “Signature Parameters”The field @signature-params contains an ordered list of components that make up the signature base. It is made up of two sections, fields and meta data.
"@signature-params": (fields...);meta
- fields: ordered, space (
) separated and contained within ellipsis(...) - meta: unorder, semi-colon (
;) separated and specify additional information about the cryptographic operation
Generate Message Signature
Section titled “Generate Message Signature”

Retrieve Private Key
Identify the private key for the given environment. You should have one for:
- Staging
- Production
Determine Signature Base
Use the instructions here to determine which fields to include to generate the signature.
Calculate Signature
- Sign the signature base directly with the environment-specific ED25519 private key. ED25519 applies SHA-512 internally as part of the algorithm (RFC 8032) — do not pre-hash the base yourself.
- Base64 encode the raw signature bytes. This is the final computed message signature (
computed-signature).
Signature Input
The signature input is the same as the @signature-params and will be passed along the header. The inputs allow for the signature to be recomputed and verified.
Make Signed Request
Add Signature and Signature-Input to the request header
- Signature:
sig1=:<computed-signature>: - Signature-Input:
sig1=<@signature-params>
curl --request POST \ --url https://api.pipevest.com/v1/customers?sort=ASC \ --header 'Content-Digest: sha-512=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:' \ --header 'Content-Length: 18' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer 123456' \ --header 'Signature: sig1=:OTEyMjY4...A5NTNDMEQ=:' \ --header 'Signature-Input: sig1=("content-type" "content-digest" "content-length" "authorization" "x-client-id" "x-idempotency-key" "@method" "@target-uri" "@path" "@query");keyid="staging-pipevest-ed25519";created=1732893484;expires=1732893584' \ --header 'X-Client-Id: 123456' \ --header 'X-Idempotency-Key: 123456' \ ... --data '{"firstName": "John", "lastName": "Doe"}'Verify Message Signature
Section titled “Verify Message Signature”

Retrieve Public Key
Identify the public key for the given environment. You should have one for:
- Staging
- Production
Rebuild the Signature Base
Use the Signature-Input from the HTTP header to reconstruct the signature base. The covered components and their order come directly from Signature-Input, so the base you rebuild is byte-identical to the one the sender signed.
Identify Header Signature
Strip the sig1=: prefix and the trailing : suffix from the Signature header to obtain the header-base64-signature.
curl --request POST \ --url https://api.staging.pipevest.com/v1/customers \ --header 'Signature: sig1=:OTEyMjY4...A5NTNDMEQ=:' \ ....Decode Signature
Base64-decode the header-base64-signature into the raw signature bytes (header-message-signature).
Verify with the Public Key
Verify the header-message-signature against the rebuilt signature base directly, using the partner’s ED25519 public key. ED25519 verification hashes the base with SHA-512 internally — there is no separate digest step.
verified = ed25519_verify(publicKey, signatureBase, header-message-signature)Accept or Reject
Accept the message only if verification succeeds. Reject it otherwise.
Example Verification Code:
Section titled “Example Verification Code:”import crypto from 'node:crypto'
// Get the ED25519 public key (SPKI PEM)const publicKey = ...
// Get Signature and Signature-Input from the original http message headerconst signature = request.headers['Signature']const signatureInput = request.headers['Signature-Input']
// Rebuild the exact signature base using the covered components in `signatureInput`const signatureBase = `...` // determined using `signatureInput`
// Strip `sig1=:` / `:` and base64-decode to the raw signature bytesconst headerBase64Signature = signature.match(/:(.*):/).pop()const headerMessageSignature = Buffer.from(headerBase64Signature, 'base64')
// ED25519 verifies the signature over the signature base directly.// Pass `null` as the algorithm (SHA-512 is internal to ED25519); do NOT pre-hash.const isSignatureVerified = crypto.verify( null, Buffer.from(signatureBase, 'utf8'), publicKey, headerMessageSignature,) import base64 import re from cryptography.hazmat.primitives.serialization import load_pem_public_key from cryptography.exceptions import InvalidSignature
# Get the ED25519 public key (loaded from SPKI PEM) public_key = load_pem_public_key(public_key_pem)
# Get Signature and Signature-Input from the original http message header signature = request.headers.get("Signature", "") signature_input = request.headers.get("Signature-Input", "")
# Rebuild the exact signature base using the covered components in `signature_input` signature_base = "..." # determined using `signature_input`
# Strip `sig1=:` / `:` and base64-decode to the raw signature bytes header_base64_signature = re.search(r"\:(.*)\:", signature).group(1) header_message_signature = base64.b64decode(header_base64_signature)
# ED25519 verifies the signature over the signature base directly # (SHA-512 is internal to ED25519 — do NOT pre-hash the base). try: public_key.verify(header_message_signature, signature_base.encode("utf-8")) is_signature_verified = True except InvalidSignature: is_signature_verified = FalseFields
Section titled “Fields”| Fields | Request Types | Required | Note |
|---|---|---|---|
Content-Type |
POST, PUT, PATCH |
Yes | |
Content-Digest |
POST, PUT, PATCH |
Yes | |
Content-Length |
POST, PUT, PATCH |
Yes | |
Authorization |
GET, POST, PUT, PATCH, DELETE |
Yes | See note below |
X-Client-Id |
GET, POST, PUT, PATCH, DELETE |
Yes | See note below |
X-Idempotency-Key |
POST, PUT, PATCH, DELETE |
Yes | |
@method |
GET, POST, PUT, PATCH, DELETE |
Yes | |
@target-uri |
GET, POST, PUT, PATCH, DELETE |
Yes | |
@path |
GET, POST, PUT, PATCH, DELETE |
Yes | |
@query |
GET, POST, PUT, PATCH, DELETE |
Yes | |
@signature-params |
GET, POST, PUT, PATCH, DELETE |
Yes |
| Name | Description | Required | Note |
|---|---|---|---|
keyid |
The id of the public key registered with Pipevest. | Yes | ex: staging-pipevest-ed25519 |
created |
The Unix timestamp (seconds) when the cryptographic operation took place | Yes | |
expires |
Unix timestamp (seconds) after which the signature is rejected — e.g. created + 100 |
No | Not needed, but recommended |