A secure, standardised channel for exchanging payment data between Holvi and your accounting or ERP software. Upload payment instructions and download bank statements via a REST interface.
Architecture
The API provides a REST interface for exchanging financial files:
PAIN.001
files for payment initiation
CAMT.053,
CAMT.054)
and payment status reports (PAIN.002)
Authentication & Authorisation
Prerequisites
Before accessing the API you must:
- Sign the Holvi Data Transfer Service Agreement
- Provide Holvi with your redirect URL(s) for testing and production
- Holvi will supply you with a
client_idandclient_secretvia secure means
client_secret must never be exposed publicly or committed to source control.OAuth 2.0 Endpoints
https://app.holvi.com/home/oauth/company/authorize/
https://app.holvi.com/home/oauth/company/token/
Required Scopes
Authorisation Flow
The API uses the standard OAuth 2.0 Authorization Code flow. The end user (Holvi customer) must grant your application access to their data.
1. Redirect the user to Holvi for authorisation
Direct the user's browser to the authorisation endpoint with the following parameters:
response_type
code
client_id
redirect_uri
state
scope
company:connections company:webhooks
Example URL:
https://app.holvi.com/home/oauth/company/authorize/?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyourapp.com%2Foauth%2F
&state=random-state-string
&scope=company%3Aconnections%20company%3Awebhooks
The user will see a consent screen where they can allow access to their Holvi data.
2. Receive the authorisation code
If the user grants access, their browser is redirected to your redirect_uri with:
-
state— the same value you provided. Verify this matches to prevent CSRF attacks. -
code— the short-lived authorisation code you will exchange for tokens.
https://yourapp.com/oauth/?state=random-state-string&code=anFVaeQPfl06uXN8ebXS8q5ewJCUNk
3. Exchange the code for tokens
POST https://app.holvi.com/home/oauth/company/token/
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&redirect_uri=<your_redirect_uri>&code=<code>
Response:
{
"access_token": "mfl94pI76O768SWOqePpVyBZLx6EIz",
"token_type": "Bearer",
"expires_in": 36000,
"refresh_token": "Umr0TpIwCpLPOEDmBETM8xqxbVmXc7",
"scope": "company:connections company:webhooks"
}
The access_token expires after 10 hours (expires_in: 36000 seconds).
Refreshing Tokens
When the access token expires, use the refresh token to obtain a new one. Each refresh also issues a new refresh token — the previous one is immediately revoked.
POST https://app.holvi.com/home/oauth/company/token/
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<the_refresh_token_value>
{
"access_token": "new_access_token_value",
"token_type": "Bearer",
"expires_in": 36000,
"refresh_token": "new_refresh_token_value",
"scope": "company:connections company:webhooks"
}refresh_token after every refresh — the old one will no longer work.Formatting the Basic Authorisation Header
Base64-encode client_id:client_secret to construct the header:
from base64 import standard_b64encode
def get_oauth_headers(client_id: str, client_secret: str) - dict:
binary_data = f"{client_id}:{client_secret}".encode("utf-8")
credential = standard_b64encode(binary_data).decode("utf-8")
return {"Authorization": f"Basic {credential}"}Using the Access Token
Include the access token in the `Authorization` header for all API requests:
Authorization: Bearer <access_token>Testing Environment
app.holvi.com
app.staging1.holvi.net
holvi.com/api
staging1.holvi.net/api
Base URL
https://holvi.com/api/connections/All endpoint paths below are relative to this base URL.
Upload Files
Upload a File
Required scope: company:connections
Upload a PAIN.001 file for payment initiation.
Request Headers
Authorization: Bearer <access_token>
Content-Type: multipart/form-dataRequest Body (multipart/form-data)
"pain001"
Response (201 Created)
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"type": "pain001",
"status": "uploaded",
"reference": "your-ref-123",
"filename": "payments_20241201.xml",
"uploaded_at": "2024-12-01T10:30:00Z",
"processed_at": null
}File Status Values
uploaded
processed
processing, failed) are communicated via webhook events, not reflected in the polling status field.HTTP Status Codes
- 201 CreatedFile uploaded successfully
- 400 Bad RequestInvalid file format or missing required fields
- 401 UnauthorisedInvalid or missing access token
- 413 Payload Too LargeFile exceeds the size limit
List Upload Files
Query Parameters
50)
{
"results": [{ "id": "123e4567-...", "type": "pain001", "status": "processed",
"reference": "your-ref-123", "filename": "payments_20241201.xml",
"uploaded_at": "2024-12-01T10:30:00Z", "processed_at": "2024-12-01T10:45:00Z" }],
"pagination": { "limit": 50, "next_cursor": "eyJpZCI6InBhaW5fMTIzNDUifQ", "has_more": true }
}Get Upload File Details
Get details about an uploaded file.
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"type": "pain001", "status": "processed",
"reference": "your-ref-123", "filename": "payments_20241201.xml",
"uploaded_at": "2024-12-01T10:30:00Z", "processed_at": "2024-12-01T10:45:00Z"
}Download Files
List Download Files
Required scope: company:connections
List available files for download (statements, status responses, etc.).
"pain002",
"camt053",
or
"camt054"
50)
{
"results": [
{ "id": "550e8400-...", "type": "camt054", "reference": "ref-456",
"created_at": "2024-12-01T14:30:00Z",
"download_url": "https://holvi.com/api/connections/downloadfiles/550e8400-.../download/" },
{ "id": "987fcdeb-...", "type": "pain002", "reference": "ref-789",
"created_at": "2024-12-01T11:15:00Z",
"download_url": "https://holvi.com/api/connections/downloadfiles/987fcdeb-.../download/" }
],
"pagination": { "limit": 50, "next_cursor": "eyJpZCI6ImNhbXQwNTRfNjc4OTAifQ", "has_more": true }
}Get Download File Details
{
"id": "550e8400-e29b-41d4-a716-446655440000", "type": "camt054", "reference": "ref-456",
"created_at": "2024-12-01T14:30:00Z",
"download_url": "https://holvi.com/api/connections/downloadfiles/550e8400-.../download/"
}Download a File
Content-Type: application/xml
Content-Disposition: attachment; filename="statement_550e8400.xml"Response body is binary XML content.
Webhook System
Webhook Registration
Required scope: company:webhooks
Register a Webhook Endpoint
// Request
{ "url": "https://yourapp.com/webhooks/payments/",
"events": ["pain001.uploaded", "pain001.processed", "camt054.generated"] }
// Response (201 Created)
{ "id": "456e7890-a12b-34c5-d678-901234567def",
"url": "https://yourapp.com/webhooks/payments/",
"events": ["pain001.uploaded", "pain001.processed", "camt054.generated"],
"secret": "whsec_abc123..." }secret is only returned on creation. Store it securely — it cannot be retrieved again. Subsequent GET requests will not include the secret field.Other Webhook Endpoints
Available Webhook Events
Webhook Payload Format
// pain001.uploaded
{ "id": "2ea03d8c-...", "event": "pain001.uploaded", "timestamp": "2024-12-01T10:30:00Z",
"data": { "id": "123e4567-...", "reference": "your-ref-123",
"filename": "payments_20241201.xml", "status": "uploaded" } }
// pain001.processed
{ "id": "2ea03d8c-...", "event": "pain001.processed", "timestamp": "2024-12-01T10:45:00Z",
"data": { "id": "123e4567-...", "reference": "your-ref-123",
"filename": "payments_20241201.xml", "status": "processed",
"processed_at": "2024-12-01T10:45:00Z" } }
// pain001.failed
{ "id": "2ea03d8c-...", "event": "pain001.failed", "timestamp": "2024-12-01T10:40:00Z",
"data": { "id": "123e4567-...", "status": "failed",
"error_code": "UNEXPECTED_ERROR", "error_message": "Unexpected error" } }
// camt053.generated / camt054.generated
{ "id": "2ea03d8c-...", "event": "camt054.generated", "timestamp": "2024-12-01T14:30:00Z",
"data": { "id": "550e8400-...", "filename": "camt054_notification_20241201.xml",
"status": "generated" } }Webhook Security — Signature Verification
Each webhook request includes a signature header for verification:
HOLV-Signature: t=<timestamp>,v1=<calculated-signature>- Parse
t(timestamp) andv1(MAC) from the header - Reject if timestamp is older than 5 minutes (replay protection)
- Compute
HMAC-SHA256(secret, "{timestamp}.{raw_body}") - Use constant-time comparison to verify against
v1
import time, hmac, hashlib
def verify_webhook_signature(body: bytes, signature_header: str, secret: str) - bool:
try:
parts = dict(p.split('=', 1) for p in signature_header.split(','))
timestamp = int(parts['t'])
if abs(int(time.time()) - timestamp) 300:
return False
message = f"{timestamp}.{body.decode()}".encode()
expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts['v1'])
except (ValueError, KeyError, UnicodeDecodeError):
return FalseWebhook Delivery & Retry
200–299
within 10 seconds
id
Event Listing
Required scope: company:connections
List and retrieve webhook events directly via the API — useful for polling or replaying missed deliveries.
List Events
50)
{
"results": [{ "id": "2ea03d8c-...", "event": "pain001.uploaded",
"data": { "id": "123e4567-...", "reference": "your-ref-123",
"filename": "payments_20241201.xml", "status": "uploaded" },
"timestamp": "2024-12-01T10:30:00Z" }],
"pagination": { "limit": 50, "next_cursor": "eyJpZCI6ImV2dF8xMjM0NSJ9", "has_more": true }
}Get Event Details
{ "id": "2ea03d8c-...", "event": "pain001.uploaded",
"data": { "id": "123e4567-...", "reference": "your-ref-123",
"filename": "payments_20241201.xml", "status": "uploaded" },
"timestamp": "2024-12-01T10:30:00Z" }Data Retention
Integration Examples
Example 1 — Basic Payment Flow
1. Upload the PAIN.001 file
curl -X POST "https://holvi.com/api/connections/uploadfiles/" \
-H "Authorization: Bearer <access_token>" \
-F "file=@payments.xml" -F "type=pain001" -F "reference=batch-001"
2. Monitor via webhooks
Listen for pain001.processing → pain001.processed (or pain001.failed)
3. Check for the PAIN.002 status report
curl "https://holvi.com/api/connections/downloadfiles/?type=pain002&reference=batch-001" \
-H "Authorization: Bearer <access_token>"
4. Download the PAIN.002 file
curl "https://holvi.com/api/connections/downloadfiles/987fcdeb-.../download/" \
-H "Authorization: Bearer <access_token>" -o pain002_response.xml
Example 2 — Statement Monitoring
1. Register a webhook for statement events
curl -X POST "https://holvi.com/api/connections/webhooks/" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "url": "https://yourapp.com/webhooks",
"events": ["camt053.generated", "camt054.generated"] }'
2. Receive the webhook notification
Holvi sends a camt053.generated or camt054.generated event. The payload includes the file id needed for download.
3. Download the statement
curl "https://holvi.com/api/connections/downloadfiles/550e8400-.../download/" \
-H "Authorization: Bearer <access_token>" -o statement.xml